Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/src')
-rw-r--r--core/src/OC/admin.js8
-rw-r--r--core/src/OC/appconfig.js11
-rw-r--r--core/src/OC/apps.js25
-rw-r--r--core/src/OC/appsettings.js9
-rw-r--r--core/src/OC/appswebroots.js7
-rw-r--r--core/src/OC/backbone-webdav.js65
-rw-r--r--core/src/OC/backbone.js6
-rw-r--r--core/src/OC/capabilities.js16
-rw-r--r--core/src/OC/config.js7
-rw-r--r--core/src/OC/constants.js14
-rw-r--r--core/src/OC/contactsmenu.js9
-rw-r--r--core/src/OC/currentuser.js8
-rw-r--r--core/src/OC/debug.js7
-rw-r--r--core/src/OC/dialogs.js57
-rw-r--r--core/src/OC/eventsource.js42
-rw-r--r--core/src/OC/get_set.js12
-rw-r--r--core/src/OC/host.js13
-rw-r--r--core/src/OC/index.js18
-rw-r--r--core/src/OC/l10n-registry.js6
-rw-r--r--core/src/OC/l10n.js73
-rw-r--r--core/src/OC/legacy-loader.js14
-rw-r--r--core/src/OC/menu.js7
-rw-r--r--core/src/OC/msg.js21
-rw-r--r--core/src/OC/navigation.js11
-rw-r--r--core/src/OC/notification.js74
-rw-r--r--core/src/OC/password-confirmation.js24
-rw-r--r--core/src/OC/plugins.js19
-rw-r--r--core/src/OC/query-string.js9
-rw-r--r--core/src/OC/requesttoken.js6
-rw-r--r--core/src/OC/routing.js164
-rw-r--r--core/src/OC/theme.js7
-rw-r--r--core/src/OC/util-history.js22
-rw-r--r--core/src/OC/util.js57
-rw-r--r--core/src/OC/webroot.js7
-rw-r--r--core/src/OC/xhr-error.js6
-rw-r--r--core/src/OCA/index.js16
-rw-r--r--core/src/OCA/search.js (renamed from core/src/OC/search.js)33
-rw-r--r--core/src/OCP/appconfig.js9
-rw-r--r--core/src/OCP/collaboration.js1
-rw-r--r--core/src/OCP/comments.js20
-rw-r--r--core/src/OCP/index.js32
-rw-r--r--core/src/OCP/loader.js6
-rw-r--r--core/src/OCP/toast.js114
-rw-r--r--core/src/OCP/whatsnew.js32
-rw-r--r--core/src/Polyfill/closest.js22
-rw-r--r--core/src/Polyfill/console.js8
-rw-r--r--core/src/Polyfill/index.js6
-rw-r--r--core/src/Polyfill/tooltip.js17
-rw-r--r--core/src/Polyfill/windows-phone.js8
-rw-r--r--core/src/Util/get-url-parameter.js12
-rw-r--r--core/src/Util/human-file-size.js51
-rw-r--r--core/src/components/ContactsMenu.js6
-rw-r--r--core/src/components/HeaderMenu.vue215
-rw-r--r--core/src/components/MainMenu.js29
-rw-r--r--core/src/components/UnifiedSearch/SearchResult.vue257
-rw-r--r--core/src/components/UnifiedSearch/SearchResultPlaceholders.vue100
-rw-r--r--core/src/components/UserMenu.js7
-rw-r--r--core/src/components/login/LoginButton.vue56
-rw-r--r--core/src/components/login/LoginForm.vue43
-rw-r--r--core/src/components/login/PasswordLessLoginForm.vue216
-rw-r--r--core/src/components/login/ResetPassword.vue101
-rw-r--r--core/src/components/login/UpdatePassword.vue2
-rw-r--r--core/src/components/setup/RecommendedApps.vue18
-rw-r--r--core/src/files/client.js986
-rw-r--r--core/src/files/fileinfo.js165
-rw-r--r--core/src/files/iedavclient.js173
-rw-r--r--core/src/globals.js31
-rw-r--r--core/src/init.js97
-rw-r--r--core/src/install.js34
-rw-r--r--core/src/jquery/avatar.js12
-rw-r--r--core/src/jquery/contactsmenu.js25
-rw-r--r--core/src/jquery/css/images/ui-icons_1d2d44_256x240.pngbin4196 -> 3550 bytes
-rw-r--r--core/src/jquery/css/images/ui-icons_ffd27a_256x240.pngbin4196 -> 3551 bytes
-rw-r--r--core/src/jquery/css/images/ui-icons_ffffff_256x240.pngbin4196 -> 3574 bytes
-rw-r--r--core/src/jquery/css/jquery-ui-fixes.scss2
-rw-r--r--core/src/jquery/css/jquery.ocdialog.scss8
-rw-r--r--core/src/jquery/exists.js6
-rw-r--r--core/src/jquery/filterattr.js6
-rw-r--r--core/src/jquery/index.js8
-rw-r--r--core/src/jquery/ocdialog.js45
-rw-r--r--core/src/jquery/octemplate.js28
-rw-r--r--core/src/jquery/placeholder.js66
-rw-r--r--core/src/jquery/requesttoken.js8
-rw-r--r--core/src/jquery/selectrange.js6
-rw-r--r--core/src/jquery/showpassword.js26
-rw-r--r--core/src/jquery/tipsy.js85
-rw-r--r--core/src/jquery/ui-fixes.js23
-rw-r--r--core/src/logger.js7
-rw-r--r--core/src/login.js16
-rw-r--r--core/src/main.js10
-rw-r--r--core/src/maintenance.js14
-rw-r--r--core/src/mixins/Nextcloud.js7
-rw-r--r--core/src/recommendedapps.js7
-rw-r--r--core/src/services/UnifiedSearchService.js93
-rw-r--r--core/src/services/WebAuthnAuthenticationService.js38
-rw-r--r--core/src/session-heartbeat.js75
-rw-r--r--core/src/systemtags/merged-systemtags.js29
-rw-r--r--core/src/systemtags/systemtagmodel.js75
-rw-r--r--core/src/systemtags/systemtags.js70
-rw-r--r--core/src/systemtags/systemtagscollection.js102
-rw-r--r--core/src/systemtags/systemtagsinputfield.js463
-rw-r--r--core/src/systemtags/systemtagsmappingcollection.js102
-rw-r--r--core/src/systemtags/templates/result.handlebars13
-rw-r--r--core/src/systemtags/templates/result_form.handlebars7
-rw-r--r--core/src/systemtags/templates/selection.handlebars5
-rw-r--r--core/src/tests/.eslintrc.js31
-rw-r--r--core/src/tests/OC/requesttoken.spec.js36
-rw-r--r--core/src/tests/setup.js36
-rw-r--r--core/src/unified-search.js59
-rw-r--r--core/src/views/Login.vue74
-rw-r--r--core/src/views/UnifiedSearch.vue761
111 files changed, 5262 insertions, 1006 deletions
diff --git a/core/src/OC/admin.js b/core/src/OC/admin.js
index 008016645d9..bb947573490 100644
--- a/core/src/OC/admin.js
+++ b/core/src/OC/admin.js
@@ -1,7 +1,8 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
const isAdmin = !!window._oc_isadmin
diff --git a/core/src/OC/appconfig.js b/core/src/OC/appconfig.js
index 37fc3ca420d..cf70d7b1262 100644
--- a/core/src/OC/appconfig.js
+++ b/core/src/OC/appconfig.js
@@ -1,7 +1,13 @@
-/* eslint-disable */
/**
* @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 GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
@@ -15,10 +21,11 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
+/* eslint-disable */
import { getValue, setValue, getApps, getKeys, deleteKey } from '../OCP/appconfig'
export const appConfig = window.oc_appconfig || {}
diff --git a/core/src/OC/apps.js b/core/src/OC/apps.js
index f55a5d03dcc..d504a99eaac 100644
--- a/core/src/OC/apps.js
+++ b/core/src/OC/apps.js
@@ -1,11 +1,24 @@
/**
- * ownCloud - core
+ * @copyright Bernhard Posselt 2014
*
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @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/>.
*
- * @author Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright Bernhard Posselt 2014
*/
import $ from 'jquery'
@@ -13,7 +26,7 @@ import $ from 'jquery'
let dynamicSlideToggleEnabled = false
const Apps = {
- enableDynamicSlideToggle: function() {
+ enableDynamicSlideToggle() {
dynamicSlideToggleEnabled = true
},
}
diff --git a/core/src/OC/appsettings.js b/core/src/OC/appsettings.js
index 7665d93fb7c..8d200960660 100644
--- a/core/src/OC/appsettings.js
+++ b/core/src/OC/appsettings.js
@@ -1,8 +1,8 @@
-/* eslint-disable */
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -17,8 +17,11 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
+
+/* eslint-disable */
import $ from 'jquery'
import { filePath } from './routing'
diff --git a/core/src/OC/appswebroots.js b/core/src/OC/appswebroots.js
index f4a82384fe6..9a7a24c2aa3 100644
--- a/core/src/OC/appswebroots.js
+++ b/core/src/OC/appswebroots.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +16,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
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 b27d117e568..d7d5c5bf434 100644
--- a/core/src/OC/backbone-webdav.js
+++ b/core/src/OC/backbone-webdav.js
@@ -1,61 +1,30 @@
-/* eslint-disable */
-/*
- * Copyright (c) 2015
- *
- * This file is licensed under the Affero General Public License version 3
- * or later.
- *
- * See the COPYING-README file.
- *
- */
-
/**
- * Webdav transport for Backbone.
- *
- * This makes it possible to use Webdav endpoints when
- * working with Backbone models and collections.
- *
- * Requires the davclient.js library.
- *
- * Usage example:
+ * Copyright (c) 2015
*
- * var PersonModel = OC.Backbone.Model.extend({
- * // make it use the DAV transport
- * sync: OC.Backbone.davSync,
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ * @author Julius Härtl <jus@bitgrid.net>
*
- * // DAV properties mapping
- * davProperties: {
- * 'id': '{http://example.com/ns}id',
- * 'firstName': '{http://example.com/ns}first-name',
- * 'lastName': '{http://example.com/ns}last-name',
- * 'age': '{http://example.com/ns}age'
- * },
+ * @license GNU AGPL version 3 or any later version
*
- * // additional parsing, if needed
- * parse: function(props) {
- * // additional parsing (DAV property values are always strings)
- * props.age = parseInt(props.age, 10);
- * return props;
- * }
- * });
+ * 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.
*
- * var PersonCollection = OC.Backbone.Collection.extend({
- * // make it use the DAV transport
- * sync: OC.Backbone.davSync,
+ * 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.
*
- * // use person model
- * // note that davProperties will be inherited
- * model: PersonModel,
+ * 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/>.
*
- * // DAV collection URL
- * url: function() {
- * return OC.linkToRemote('dav') + '/person/';
- * },
- * });
*/
+/* eslint-disable */
import _ from 'underscore'
-import dav from 'davclient.js'
+import { dav } from 'davclient.js'
const methodMap = {
create: 'POST',
diff --git a/core/src/OC/backbone.js b/core/src/OC/backbone.js
index 86e98ec1b41..23dca847a44 100644
--- a/core/src/OC/backbone.js
+++ b/core/src/OC/backbone.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import VendorBackbone from 'backbone'
diff --git a/core/src/OC/capabilities.js b/core/src/OC/capabilities.js
index 2e0a8da3995..50b919d0ea3 100644
--- a/core/src/OC/capabilities.js
+++ b/core/src/OC/capabilities.js
@@ -1,7 +1,9 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,10 +18,11 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
-const capabilities = window._oc_capabilities || {}
+import { getCapabilities as realGetCapabilities } from '@nextcloud/capabilities'
/**
* Returns the capabilities
@@ -28,4 +31,7 @@ const capabilities = window._oc_capabilities || {}
*
* @since 14.0
*/
-export const getCapabilities = () => capabilities
+export const getCapabilities = () => {
+ console.warn('OC.getCapabilities is deprecated and will be removed in Nextcloud 21. See @nextcloud/capabilities')
+ return realGetCapabilities()
+}
diff --git a/core/src/OC/config.js b/core/src/OC/config.js
index d1a3211cf62..46188e5dea3 100644
--- a/core/src/OC/config.js
+++ b/core/src/OC/config.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +16,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
const config = window._oc_config || {}
diff --git a/core/src/OC/constants.js b/core/src/OC/constants.js
index 972848997ae..300380ce90f 100644
--- a/core/src/OC/constants.js
+++ b/core/src/OC/constants.js
@@ -1,22 +1,24 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @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
+ * 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.
+ * 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
+ * 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
export const coreApps = ['', 'admin', 'log', 'core/search', 'core', '3rdparty']
diff --git a/core/src/OC/contactsmenu.js b/core/src/OC/contactsmenu.js
index e986c46ec20..a4dc3022c3f 100644
--- a/core/src/OC/contactsmenu.js
+++ b/core/src/OC/contactsmenu.js
@@ -1,9 +1,9 @@
-/* eslint-disable */
-
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2017 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 GNU AGPL version 3 or any later version
*
@@ -18,10 +18,11 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
+/* eslint-disable */
import $ from 'jquery'
import { Collection, Model, View } from 'backbone'
diff --git a/core/src/OC/currentuser.js b/core/src/OC/currentuser.js
index 061abba89d6..b6dc900b6f3 100644
--- a/core/src/OC/currentuser.js
+++ b/core/src/OC/currentuser.js
@@ -1,7 +1,8 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
const rawUid = document
diff --git a/core/src/OC/debug.js b/core/src/OC/debug.js
index 15a66c44aed..ec52e470ef4 100644
--- a/core/src/OC/debug.js
+++ b/core/src/OC/debug.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +16,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
const base = window._oc_debug
diff --git a/core/src/OC/dialogs.js b/core/src/OC/dialogs.js
index bf6a92aef8b..1e39a52c2f8 100644
--- a/core/src/OC/dialogs.js
+++ b/core/src/OC/dialogs.js
@@ -1,10 +1,30 @@
-/* eslint-disable */
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @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 GNU AGPL version 3 or any later version
*
@@ -19,9 +39,11 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
+/* eslint-disable */
import _ from 'underscore'
import $ from 'jquery'
@@ -107,7 +129,7 @@ const Dialogs = {
'none',
buttons,
callback,
- modal
+ modal === undefined ? true : modal
)
},
/**
@@ -625,6 +647,7 @@ const Dialogs = {
$(dialogId).ocdialog({
closeOnEscape: true,
+ closeCallback: () => { callback && callback(false) },
modal: modal,
buttons: buttonlist
})
@@ -787,11 +810,11 @@ const Dialogs = {
$conflict.find('.filename').text(original.name)
$originalDiv.find('.size').text(OC.Util.humanFileSize(original.size))
- $originalDiv.find('.mtime').text(formatDate(original.mtime))
+ $originalDiv.find('.mtime').text(OC.Util.formatDate(original.mtime))
// ie sucks
- if (replacement.size && replacement.lastModifiedDate) {
+ if (replacement.size && replacement.lastModified) {
$replacementDiv.find('.size').text(OC.Util.humanFileSize(replacement.size))
- $replacementDiv.find('.mtime').text(formatDate(replacement.lastModifiedDate))
+ $replacementDiv.find('.mtime').text(OC.Util.formatDate(replacement.lastModified))
}
var path = original.directory + '/' + original.name
var urlSpec = {
@@ -822,9 +845,9 @@ const Dialogs = {
// set more recent mtime bold
// ie sucks
- if (replacement.lastModifiedDate && replacement.lastModifiedDate.getTime() > original.mtime) {
+ if (replacement.lastModified > original.mtime) {
$replacementDiv.find('.mtime').css('font-weight', 'bold')
- } else if (replacement.lastModifiedDate && replacement.lastModifiedDate.getTime() < original.mtime) {
+ } else if (replacement.lastModified < original.mtime) {
$originalDiv.find('.mtime').css('font-weight', 'bold')
} else {
// TODO add to same mtime collection?
@@ -929,7 +952,11 @@ const Dialogs = {
closeButton: null,
close: function() {
self._fileexistsshown = false
- $(this).ocdialog('destroy').remove()
+ try {
+ $(this).ocdialog('destroy').remove()
+ } catch (e) {
+ // ignore
+ }
}
})
@@ -1159,6 +1186,8 @@ const Dialogs = {
self.$fileListHeader.show()
}
+ self.$filelist.empty();
+
$.each(files, function(idx, entry) {
entry.icon = OC.MimeType.getIconUrl(entry.mimetype)
var simpleSize, sizeColor
@@ -1218,8 +1247,12 @@ const Dialogs = {
* fills the tree list with directories
*/
_fillSlug: function() {
+ var addButton = this.$dirTree.find('.actions.creatable').detach()
this.$dirTree.empty()
var self = this
+
+ self.$dirTree.append(addButton)
+
var dir
var path = this.$filePicker.data('path')
var $template = $('<div data-dir="{dir}"><a>{name}</a></div>').addClass('crumb')
@@ -1236,10 +1269,12 @@ const Dialogs = {
}))
})
}
+
$template.octemplate({
dir: '',
name: '' // Ugly but works ;)
}, { escapeFunction: null }).prependTo(this.$dirTree)
+
},
/**
* handle selection made in the tree list
diff --git a/core/src/OC/eventsource.js b/core/src/OC/eventsource.js
index cc576d8655a..b4b719d0dd4 100644
--- a/core/src/OC/eventsource.js
+++ b/core/src/OC/eventsource.js
@@ -1,36 +1,32 @@
-/* eslint-disable */
/**
- * ownCloud
- *
- * @author Robin Appelman
* @copyright 2012 Robin Appelman icewind1991@gmail.com
*
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
+ * @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 GNU AGPL version 3 or any later version
*
- * This library is distributed in the hope that it will be useful,
+ * 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 library. If not, see <http://www.gnu.org/licenses/>.
+ * GNU Affero General Public License for more details.
*
- */
-
-/**
- * Wrapper for server side events
- * (http://en.wikipedia.org/wiki/Server-sent_events)
- * includes a fallback for older browsers and IE
+ * 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/>.
*
- * use server side events with caution, too many open requests can hang the
- * server
*/
-/* global EventSource */
-
+/* eslint-disable */
import $ from 'jquery'
import { getToken } from './requesttoken'
diff --git a/core/src/OC/get_set.js b/core/src/OC/get_set.js
index b4dcd563eff..e5d5bd5c5fd 100644
--- a/core/src/OC/get_set.js
+++ b/core/src/OC/get_set.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,15 +17,10 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
-/**
- * Get a variable by name
- * @param {string} context context
- * @returns {Function} getter
- * @deprecated 19.0.0 use https://lodash.com/docs#get
- */
export const get = context => name => {
const namespaces = name.split('.')
const tail = namespaces.pop()
diff --git a/core/src/OC/host.js b/core/src/OC/host.js
index f90ca65b4da..44623b27399 100644
--- a/core/src/OC/host.js
+++ b/core/src/OC/host.js
@@ -1,7 +1,8 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,14 +17,10 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
-/**
- * Protocol that is used to access this Nextcloud instance
- * @returns {string} Used protocol
- * @deprecated 17.0.0 use window.location.protocol directly
- */
export const getProtocol = () => window.location.protocol.split(':')[0]
/**
diff --git a/core/src/OC/index.js b/core/src/OC/index.js
index 59abe046769..c7d2ba2c33d 100644
--- a/core/src/OC/index.js
+++ b/core/src/OC/index.js
@@ -1,7 +1,9 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +18,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import { subscribe } from '@nextcloud/event-bus'
@@ -79,10 +82,12 @@ import {
} from './menu'
import { isUserAdmin } from './admin'
import L10N, {
- getCanonicalLocale,
getLanguage,
getLocale,
} from './l10n'
+import {
+ getCanonicalLocale,
+} from '@nextcloud/l10n'
import {
generateUrl,
@@ -101,7 +106,6 @@ import msg from './msg'
import Notification from './notification'
import PasswordConfirmation from './password-confirmation'
import Plugins from './plugins'
-import search from './search'
import { theme } from './theme'
import Util from './util'
import { debug } from './debug'
@@ -177,9 +181,8 @@ export default {
* Capabilities
*
* @type {Array}
- * @deprecated 17.0.0 use OC.getCapabilities() instead
+ * @deprecated 20.0.0 use @nextcloud/capabilities instead
*/
- _capabilities: getCapabilities(),
getCapabilities,
/*
@@ -223,7 +226,7 @@ export default {
getProtocol,
/**
- * L10n
+ * @deprecated 20.0.0 use `getCanonicalLocale` from https://www.npmjs.com/package/@nextcloud/l10n
*/
getCanonicalLocale,
getLocale,
@@ -248,7 +251,6 @@ export default {
Notification,
PasswordConfirmation,
Plugins,
- search,
theme,
Util,
debug,
diff --git a/core/src/OC/l10n-registry.js b/core/src/OC/l10n-registry.js
index dc353902337..5ed463275a9 100644
--- a/core/src/OC/l10n-registry.js
+++ b/core/src/OC/l10n-registry.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
// This var is global because it's shared across webpack bundles
diff --git a/core/src/OC/l10n.js b/core/src/OC/l10n.js
index 5ada257d858..0704269ce4b 100644
--- a/core/src/OC/l10n.js
+++ b/core/src/OC/l10n.js
@@ -1,10 +1,30 @@
/**
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.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>
*
- * This file is licensed under the Affero General Public License version 3
- * or later.
+ * @license GNU AGPL version 3 or any later version
*
- * See the COPYING-README file.
+ * 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/>.
*
*/
@@ -12,7 +32,9 @@ import _ from 'underscore'
import $ from 'jquery'
import DOMPurify from 'dompurify'
import Handlebars from 'handlebars'
+import identity from 'lodash/fp/identity'
import escapeHTML from 'escape-html'
+import { generateFilePath } from '@nextcloud/router'
import OC from './index'
import {
@@ -37,7 +59,7 @@ const L10n = {
* the translations are loaded
* @returns {Promise} promise
*/
- load: function(appName, callback) {
+ load(appName, callback) {
// already available ?
if (hasAppTranslations(appName) || OC.getLocale() === 'en') {
const deferred = $.Deferred()
@@ -48,7 +70,7 @@ const L10n = {
}
const self = this
- const url = OC.filePath(appName, 'l10n', OC.getLocale() + '.json')
+ const url = generateFilePath(appName, 'l10n', OC.getLocale() + '.json')
// load JSON translation bundle per AJAX
return $.get(url)
@@ -67,7 +89,7 @@ const L10n = {
* @param {String} appName name of the app
* @param {Object<String,String>} bundle bundle
*/
- register: function(appName, bundle) {
+ register(appName, bundle) {
registerAppTranslations(appName, bundle, this._getPlural)
},
@@ -84,15 +106,20 @@ const L10n = {
* @param {number} [count] number to replace %n with
* @param {array} [options] options array
* @param {bool} [options.escape=true] enable/disable auto escape of placeholders (by default enabled)
+ * @param {bool} [options.sanitize=true] enable/disable sanitization (by default enabled)
* @returns {string}
*/
- translate: function(app, text, vars, count, options) {
+ translate(app, text, vars, count, options) {
const defaultOptions = {
escape: true,
+ sanitize: true,
}
const allOptions = options || {}
_.defaults(allOptions, defaultOptions)
+ const optSanitize = allOptions.sanitize ? DOMPurify.sanitize : identity
+ const optEscape = allOptions.escape ? escapeHTML : identity
+
// TODO: cache this function to avoid inline recreation
// of the same function over and over again in case
// translate() is used in a loop
@@ -101,13 +128,9 @@ const L10n = {
function(a, b) {
const r = vars[b]
if (typeof r === 'string' || typeof r === 'number') {
- if (allOptions.escape) {
- return DOMPurify.sanitize(escapeHTML(r))
- } else {
- return DOMPurify.sanitize(r)
- }
+ return optSanitize(optEscape(r))
} else {
- return DOMPurify.sanitize(a)
+ return optSanitize(a)
}
}
)
@@ -120,9 +143,9 @@ const L10n = {
}
if (typeof vars === 'object' || count !== undefined) {
- return DOMPurify.sanitize(_build(translation, vars, count))
+ return optSanitize(_build(translation, vars, count))
} else {
- return DOMPurify.sanitize(translation)
+ return optSanitize(translation)
}
},
@@ -137,7 +160,7 @@ const L10n = {
* @param {bool} [options.escape=true] enable/disable auto escape of placeholders (by default enabled)
* @returns {string} Translated string
*/
- translatePlural: function(app, textSingular, textPlural, count, vars, options) {
+ translatePlural(app, textSingular, textPlural, count, vars, options) {
const identifier = '_' + textSingular + '_::_' + textPlural + '_'
const bundle = getAppTranslations(app)
const value = bundle.translations[identifier]
@@ -163,9 +186,9 @@ const L10n = {
* @returns {number}
* @private
*/
- _getPlural: function(number) {
+ _getPlural(number) {
let language = OC.getLanguage()
- if (language === 'pt_BR') {
+ if (language === 'pt-BR') {
// temporary set a locale for brazilian
language = 'xbr'
}
@@ -175,7 +198,7 @@ const L10n = {
}
if (language.length > 3) {
- language = language.substring(0, language.lastIndexOf('_'))
+ language = language.substring(0, language.lastIndexOf('-'))
}
/*
@@ -321,21 +344,11 @@ const L10n = {
export default L10n
/**
- * Returns the user's locale as a BCP 47 compliant language tag
- *
- * @returns {String} locale string
- */
-export const getCanonicalLocale = () => {
- const locale = getLocale()
- return typeof locale === 'string' ? locale.replace(/_/g, '-') : locale
-}
-
-/**
* Returns the user's locale
*
* @returns {String} locale string
*/
-export const getLocale = () => $('html').data('locale')
+export const getLocale = () => $('html').data('locale') ?? 'en'
/**
* Returns the user's language
diff --git a/core/src/OC/legacy-loader.js b/core/src/OC/legacy-loader.js
index ece9f3c3fc1..3fedc07a081 100644
--- a/core/src/OC/legacy-loader.js
+++ b/core/src/OC/legacy-loader.js
@@ -1,7 +1,9 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -11,15 +13,17 @@
* 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
+ * 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
+import { generateFilePath } from '@nextcloud/router'
const loadedScripts = {}
const loadedStyles = []
@@ -37,7 +41,7 @@ export const addScript = (app, script, ready) => {
console.warn('OC.addScript is deprecated, use OCP.Loader.loadScript instead')
let deferred
- const path = OC.filePath(app, 'js', script + '.js')
+ const path = generateFilePath(app, 'js', script + '.js')
if (!loadedScripts[path]) {
deferred = $.Deferred()
$.getScript(path, () => deferred.resolve())
@@ -59,7 +63,7 @@ export const addScript = (app, script, ready) => {
export const addStyle = (app, style) => {
console.warn('OC.addStyle is deprecated, use OCP.Loader.loadStylesheet instead')
- const path = OC.filePath(app, 'css', style + '.css')
+ const path = generateFilePath(app, 'css', style + '.css')
if (loadedStyles.indexOf(path) === -1) {
loadedStyles.push(path)
if (document.createStyleSheet) {
diff --git a/core/src/OC/menu.js b/core/src/OC/menu.js
index 82cde9e862d..26d139b5f08 100644
--- a/core/src/OC/menu.js
+++ b/core/src/OC/menu.js
@@ -1,7 +1,9 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +18,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import _ from 'underscore'
diff --git a/core/src/OC/msg.js b/core/src/OC/msg.js
index d84d0503cb3..bb28d873c98 100644
--- a/core/src/OC/msg.js
+++ b/core/src/OC/msg.js
@@ -1,7 +1,9 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +18,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
@@ -34,7 +37,7 @@ export default {
*
* @param {Object} selector Placeholder to display the message in
*/
- startSaving: function(selector) {
+ startSaving(selector) {
this.startAction(selector, t('core', 'Saving …'))
},
@@ -44,7 +47,7 @@ export default {
* @param {Object} selector Placeholder to display the message in
* @param {string} message Plain text message to display (no HTML allowed)
*/
- startAction: function(selector, message) {
+ startAction(selector, message) {
$(selector).text(message)
.removeClass('success')
.removeClass('error')
@@ -62,7 +65,7 @@ export default {
* @param {string} response.status is being used to decide whether the message
* is displayed as an error/success
*/
- finishedSaving: function(selector, response) {
+ finishedSaving(selector, response) {
this.finishedAction(selector, response)
},
@@ -76,7 +79,7 @@ export default {
* @param {string} response.status is being used to decide whether the message
* is displayed as an error/success
*/
- finishedAction: function(selector, response) {
+ finishedAction(selector, response) {
if (response.status === 'success') {
this.finishedSuccess(selector, response.data.message)
} else {
@@ -90,7 +93,7 @@ export default {
* @param {Object} selector Placeholder to display the message in
* @param {string} message Plain text success message to display (no HTML allowed)
*/
- finishedSuccess: function(selector, message) {
+ finishedSuccess(selector, message) {
$(selector).text(message)
.addClass('success')
.removeClass('error')
@@ -106,7 +109,7 @@ export default {
* @param {Object} selector Placeholder to display the message in
* @param {string} message Plain text error message to display (no HTML allowed)
*/
- finishedError: function(selector, message) {
+ finishedError(selector, message) {
$(selector).text(message)
.addClass('error')
.removeClass('success')
diff --git a/core/src/OC/navigation.js b/core/src/OC/navigation.js
index f9e6789950a..a4ccb9ced51 100644
--- a/core/src/OC/navigation.js
+++ b/core/src/OC/navigation.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,14 +17,10 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
-/**
- * Redirect to the target URL, can also be used for downloads.
- * @param {string} targetURL URL to redirect to
- * @deprecated 17.0.0 use window.location directly
- */
export const redirect = targetURL => { window.location = targetURL }
/**
diff --git a/core/src/OC/notification.js b/core/src/OC/notification.js
index 4e95476b69b..a0b289b6a87 100644
--- a/core/src/OC/notification.js
+++ b/core/src/OC/notification.js
@@ -1,7 +1,12 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,15 +21,17 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import _ from 'underscore'
import $ from 'jquery'
+import { showMessage, TOAST_DEFAULT_TIMEOUT, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
/**
* @todo Write documentation
- * @deprecated 17.0.0 use OCP.Toast
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package instead
* @namespace OC.Notification
*/
export default {
@@ -35,9 +42,9 @@ export default {
/**
* @param {Function} callback callback function
- * @deprecated 17.0.0 use OCP.Toast
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
- setDefault: function(callback) {
+ setDefault(callback) {
this.getDefaultNotificationFunction = callback
},
@@ -49,9 +56,9 @@ export default {
*
* @param {jQuery} [$row] notification row
* @param {Function} [callback] callback
- * @deprecated 17.0.0 use OCP.Toast
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
- hide: function($row, callback) {
+ hide($row, callback) {
if (_.isFunction($row)) {
// first arg is the callback
callback = $row
@@ -65,7 +72,11 @@ export default {
// remove the row directly
$row.each(function() {
- $(this)[0].toastify.hideToast()
+ if ($(this)[0].toastify) {
+ $(this)[0].toastify.hideToast()
+ } else {
+ console.error('cannot hide toast because object is not set')
+ }
if (this === this.updatableNotification) {
this.updatableNotification = null
}
@@ -88,13 +99,14 @@ export default {
* @param {string} [options.type] notification type
* @param {int} [options.timeout=0] timeout value, defaults to 0 (permanent)
* @returns {jQuery} jQuery element for notification row
- * @deprecated 17.0.0 use OCP.Toast
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
- showHtml: function(html, options) {
+ showHtml(html, options) {
options = options || {}
options.isHTML = true
- options.timeout = (!options.timeout) ? -1 : options.timeout
- const toast = window.OCP.Toast.message(html, options)
+ options.timeout = (!options.timeout) ? TOAST_PERMANENT_TIMEOUT : options.timeout
+ const toast = showMessage(html, options)
+ toast.toastElement.toastify = toast
return $(toast.toastElement)
},
@@ -106,12 +118,22 @@ export default {
* @param {string} [options.type] notification type
* @param {int} [options.timeout=0] timeout value, defaults to 0 (permanent)
* @returns {jQuery} jQuery element for notification row
- * @deprecated 17.0.0 use OCP.Toast
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
- show: function(text, options) {
+ show(text, options) {
+ const escapeHTML = function(text) {
+ return text.toString()
+ .split('&').join('&amp;')
+ .split('<').join('&lt;')
+ .split('>').join('&gt;')
+ .split('"').join('&quot;')
+ .split('\'').join('&#039;')
+ }
+
options = options || {}
- options.timeout = (!options.timeout) ? -1 : options.timeout
- const toast = window.OCP.Toast.message(text, options)
+ options.timeout = (!options.timeout) ? TOAST_PERMANENT_TIMEOUT : options.timeout
+ const toast = showMessage(escapeHTML(text), options)
+ toast.toastElement.toastify = toast
return $(toast.toastElement)
},
@@ -120,13 +142,14 @@ export default {
*
* @param {string} text Message to display
* @returns {jQuery} JQuery element for notificaiton row
- * @deprecated 17.0.0 use OCP.Toast
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
- showUpdate: function(text) {
+ showUpdate(text) {
if (this.updatableNotification) {
this.updatableNotification.hideToast()
}
- this.updatableNotification = OCP.Toast.message(text, { timeout: -1 })
+ this.updatableNotification = showMessage(text, { timeout: TOAST_PERMANENT_TIMEOUT })
+ this.updatableNotification.toastElement.toastify = this.updatableNotification
return $(this.updatableNotification.toastElement)
},
@@ -140,21 +163,22 @@ export default {
* @param {boolean} [options.isHTML=false] an indicator for HTML notifications (true) or text (false)
* @param {string} [options.type] notification type
* @returns {JQuery<any>} the toast element
- * @deprecated 17.0.0 use OCP.Toast
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
- showTemporary: function(text, options) {
+ showTemporary(text, options) {
options = options || {}
- options.timeout = options.timeout || 7
- const toast = window.OCP.Toast.message(text, options)
+ options.timeout = options.timeout || TOAST_DEFAULT_TIMEOUT
+ const toast = showMessage(text, options)
+ toast.toastElement.toastify = toast
return $(toast.toastElement)
},
/**
* Returns whether a notification is hidden.
* @returns {boolean}
- * @deprecated 17.0.0 use OCP.Toast
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
- isHidden: function() {
+ isHidden() {
return !$('#content').find('.toastify').length
},
}
diff --git a/core/src/OC/password-confirmation.js b/core/src/OC/password-confirmation.js
index f708217fc84..ec70720b4d2 100644
--- a/core/src/OC/password-confirmation.js
+++ b/core/src/OC/password-confirmation.js
@@ -1,7 +1,9 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,12 +18,14 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import _ from 'underscore'
import $ from 'jquery'
import moment from 'moment'
+import { generateUrl } from '@nextcloud/router'
import OC from './index'
@@ -33,12 +37,12 @@ export default {
pageLoadTime: null,
- init: function() {
+ init() {
$('.password-confirm-required').on('click', _.bind(this.requirePasswordConfirmation, this))
this.pageLoadTime = moment.now()
},
- requiresPasswordConfirmation: function() {
+ requiresPasswordConfirmation() {
const serverTimeDiff = this.pageLoadTime - (window.nc_pageLoad * 1000)
const timeSinceLogin = moment.now() - (serverTimeDiff + (window.nc_lastLogin * 1000))
@@ -51,7 +55,7 @@ export default {
* @param {Object} options options
* @param {Function} rejectCallback error callback function
*/
- requirePasswordConfirmation: function(callback, options, rejectCallback) {
+ requirePasswordConfirmation(callback, options, rejectCallback) {
options = typeof options !== 'undefined' ? options : {}
const defaults = {
title: t('core', 'Authentication required'),
@@ -101,23 +105,23 @@ export default {
this.callback = callback
},
- _confirmPassword: function(password, config) {
+ _confirmPassword(password, config) {
const self = this
$.ajax({
- url: OC.generateUrl('/login/confirm'),
+ url: generateUrl('/login/confirm'),
data: {
- password: password,
+ password,
},
type: 'POST',
- success: function(response) {
+ success(response) {
window.nc_lastLogin = response.lastLogin
if (_.isFunction(self.callback)) {
self.callback()
}
},
- error: function() {
+ error() {
config.error = t('core', 'Failed to authenticate, try again')
OC.PasswordConfirmation.requirePasswordConfirmation(self.callback, config)
},
diff --git a/core/src/OC/plugins.js b/core/src/OC/plugins.js
index 1c9b59c7419..dab74db11e9 100644
--- a/core/src/OC/plugins.js
+++ b/core/src/OC/plugins.js
@@ -1,7 +1,8 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,12 +17,10 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
-/**
- * @namespace OC.Plugins
- */
export default {
/**
@@ -35,7 +34,7 @@ export default {
* @param {String} targetName app name / class name to hook into
* @param {OC.Plugin} plugin plugin
*/
- register: function(targetName, plugin) {
+ register(targetName, plugin) {
let plugins = this._plugins[targetName]
if (!plugins) {
plugins = this._plugins[targetName] = []
@@ -50,7 +49,7 @@ export default {
* @param {String} targetName app name / class name to hook into
* @returns {Array.<OC.Plugin>} array of plugins
*/
- getPlugins: function(targetName) {
+ getPlugins(targetName) {
return this._plugins[targetName] || []
},
@@ -61,7 +60,7 @@ export default {
* @param {Object} targetObject to be extended
* @param {Object} [options] options
*/
- attach: function(targetName, targetObject, options) {
+ attach(targetName, targetObject, options) {
const plugins = this.getPlugins(targetName)
for (let i = 0; i < plugins.length; i++) {
if (plugins[i].attach) {
@@ -77,7 +76,7 @@ export default {
* @param {Object} targetObject to be extended
* @param {Object} [options] options
*/
- detach: function(targetName, targetObject, options) {
+ detach(targetName, targetObject, options) {
const plugins = this.getPlugins(targetName)
for (let i = 0; i < plugins.length; i++) {
if (plugins[i].detach) {
diff --git a/core/src/OC/query-string.js b/core/src/OC/query-string.js
index 502fa15e474..3b71ebb5a8d 100644
--- a/core/src/OC/query-string.js
+++ b/core/src/OC/query-string.js
@@ -1,7 +1,9 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,9 +18,12 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
+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
diff --git a/core/src/OC/requesttoken.js b/core/src/OC/requesttoken.js
index 6c011879b8b..06ccdffd00d 100644
--- a/core/src/OC/requesttoken.js
+++ b/core/src/OC/requesttoken.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import { emit } from '@nextcloud/event-bus'
diff --git a/core/src/OC/routing.js b/core/src/OC/routing.js
index 0e8fcecd80d..0531a6776a4 100644
--- a/core/src/OC/routing.js
+++ b/core/src/OC/routing.js
@@ -1,7 +1,9 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,165 +18,19 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
-import _ from 'underscore'
-
-import OC from './index'
-import { coreApps } from './constants'
-
-/**
- * Get an absolute url to a file in an app
- * @param {string} app the id of the app the file belongs to
- * @param {string} file the file path relative to the app folder
- * @returns {string} Absolute URL to a file
- */
-export const linkTo = (app, file) => filePath(app, '', file)
+import {
+ getRootUrl as realGetRootUrl,
+} from '@nextcloud/router'
/**
* Creates a relative url for remote use
* @param {string} service id
* @returns {string} the url
*/
-export const linkToRemoteBase = service => getRootPath() + '/remote.php/' + service
-
-/**
- * @brief Creates an absolute url for remote use
- * @param {string} service id
- * @returns {string} the url
- */
-export const linkToRemote = service => window.location.protocol + '//' + window.location.host + linkToRemoteBase(service)
-
-/**
- * Gets the base path for the given OCS API service.
- * @param {string} service name
- * @param {int} version OCS API version
- * @returns {string} OCS API base path
- */
-export const linkToOCS = (service, version) => {
- version = (version !== 2) ? 1 : 2
- return window.location.protocol + '//' + window.location.host + getRootPath() + '/ocs/v' + version + '.php/' + service + '/'
-}
-
-/**
- * Generates the absolute url for the given relative url, which can contain parameters.
- * Parameters will be URL encoded automatically.
- * @param {string} url the url
- * @param {Object} [params] params
- * @param {Object} [options] destructuring object
- * @param {bool} [options.escape=true] enable/disable auto escape of placeholders (by default enabled)
- * @returns {string} Absolute URL for the given relative URL
- */
-export const generateUrl = (url, params, options) => {
- const defaultOptions = {
- escape: true,
- }
- const allOptions = options || {}
- _.defaults(allOptions, defaultOptions)
-
- const _build = function(text, vars) {
- vars = vars || []
- return text.replace(/{([^{}]*)}/g,
- function(a, b) {
- const r = (vars[b])
- if (allOptions.escape) {
- return (typeof r === 'string' || typeof r === 'number') ? encodeURIComponent(r) : encodeURIComponent(a)
- } else {
- return (typeof r === 'string' || typeof r === 'number') ? r : a
- }
- }
- )
- }
- if (url.charAt(0) !== '/') {
- url = '/' + url
-
- }
-
- if (OC.config.modRewriteWorking === true) {
- return getRootPath() + _build(url, params)
- }
-
- return getRootPath() + '/index.php' + _build(url, params)
-}
-
-/**
- * get the absolute path to an image file
- * if no extension is given for the image, it will automatically decide
- * between .png and .svg based on what the browser supports
- *
- * @param {string} app the app id to which the image belongs
- * @param {string} file the name of the image file
- * @returns {string}
- * @deprecated 19.0.0 use `imagePath` from https://www.npmjs.com/package/@nextcloud/router
- */
-export const imagePath = (app, file) => {
- if (file.indexOf('.') === -1) {
- // if no extension is given, use svg
- return filePath(app, 'img', file + '.svg')
- }
-
- return filePath(app, 'img', file)
-}
-
-/**
- * Get the absolute url for a file in an app
- * @param {string} app the id of the app
- * @param {string} type the type of the file to link to (e.g. css,img,ajax.template)
- * @param {string} file the filename
- * @returns {string} Absolute URL for a file in an app
- * @deprecated 19.0.0 use `generateFilePath` from https://www.npmjs.com/package/@nextcloud/router
- */
-export const filePath = (app, type, file) => {
- const isCore = coreApps.indexOf(app) !== -1
- let link = getRootPath()
- if (file.substring(file.length - 3) === 'php' && !isCore) {
- link += '/index.php/apps/' + app
- if (file !== 'index.php') {
- link += '/'
- if (type) {
- link += encodeURI(type + '/')
- }
- link += file
- }
- } else if (file.substring(file.length - 3) !== 'php' && !isCore) {
- link = OC.appswebroots[app]
- if (type) {
- link += '/' + type + '/'
- }
- if (link.substring(link.length - 1) !== '/') {
- link += '/'
- }
- link += file
- } else {
- if ((app === 'core' || app === 'search') && type === 'ajax') {
- link += '/index.php/'
- } else {
- link += '/'
- }
- if (!isCore) {
- link += 'apps/'
- }
- if (app !== '') {
- app += '/'
- link += app
- }
- if (type) {
- link += type + '/'
- }
- link += file
- }
- return link
+export const linkToRemoteBase = service => {
+ return realGetRootUrl() + '/remote.php/' + service
}
-
-/**
- * Returns the web root path where this Nextcloud instance
- * is accessible, with a leading slash.
- * For example "/nextcloud".
- *
- * @returns {string} web root path
- *
- * @deprecated 19.0.0 use `getRootUrl` from https://www.npmjs.com/package/@nextcloud/router
- * @since 8.2
- */
-export const getRootPath = () => OC.webroot
diff --git a/core/src/OC/theme.js b/core/src/OC/theme.js
index 8e49499f485..1af0f721475 100644
--- a/core/src/OC/theme.js
+++ b/core/src/OC/theme.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +16,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
export const theme = window._theme || {}
diff --git a/core/src/OC/util-history.js b/core/src/OC/util-history.js
index 61fe3fec098..de40a9c4eed 100644
--- a/core/src/OC/util-history.js
+++ b/core/src/OC/util-history.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import _ from 'underscore'
@@ -45,7 +47,7 @@ export default {
* using the params as query string
* @param {boolean} [replace=false] whether to replace instead of pushing
*/
- _pushState: function(params, url, replace) {
+ _pushState(params, url, replace) {
let strParams
if (typeof (params) === 'string') {
strParams = params
@@ -93,7 +95,7 @@ export default {
* @param {Object|string} params to append to the URL, can be either a string or a map
* @param {string} [url] URL to be used, otherwise the current URL will be used, using the params as query string
*/
- pushState: function(params, url) {
+ pushState(params, url) {
this._pushState(params, url, false)
},
@@ -108,7 +110,7 @@ export default {
* @param {string} [url] URL to be used, otherwise the current URL will be used,
* using the params as query string
*/
- replaceState: function(params, url) {
+ replaceState(params, url) {
this._pushState(params, url, true)
},
@@ -117,7 +119,7 @@ export default {
*
* @param {Function} handler handler
*/
- addOnPopStateHandler: function(handler) {
+ addOnPopStateHandler(handler) {
this._handlers.push(handler)
},
@@ -126,7 +128,7 @@ export default {
* (workaround for IE8 / IE9)
* @returns {string}
*/
- _parseHashQuery: function() {
+ _parseHashQuery() {
const hash = window.location.hash
const pos = hash.indexOf('?')
if (pos >= 0) {
@@ -139,7 +141,7 @@ export default {
return ''
},
- _decodeQuery: function(query) {
+ _decodeQuery(query) {
return query.replace(/\+/g, ' ')
},
@@ -149,7 +151,7 @@ export default {
*
* @returns {Object} map of parameters
*/
- parseUrlQuery: function() {
+ parseUrlQuery() {
const query = this._parseHashQuery()
let params
// try and parse from URL hash first
@@ -161,7 +163,7 @@ export default {
return params || {}
},
- _onPopState: function(e) {
+ _onPopState(e) {
if (this._cancelPop) {
this._cancelPop = false
return
diff --git a/core/src/OC/util.js b/core/src/OC/util.js
index f8f7c05e6e8..7f348704831 100644
--- a/core/src/OC/util.js
+++ b/core/src/OC/util.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
@@ -24,7 +26,7 @@ import moment from 'moment'
import History from './util-history'
import OC from './index'
-import humanFileSize from '../Util/human-file-size'
+import { formatFileSize as humanFileSize } from '@nextcloud/files'
function chunkify(t) {
// Adapted from http://my.opera.com/GreyWyvern/blog/show.dml/1671288
@@ -58,6 +60,9 @@ export default {
History,
+ /**
+ * @deprecated use https://nextcloud.github.io/nextcloud-files/modules/_humanfilesize_.html#formatfilesize
+ */
humanFileSize,
/**
@@ -69,7 +74,7 @@ export default {
*
*
*/
- computerFileSize: function(string) {
+ computerFileSize(string) {
if (typeof string !== 'string') {
return null
}
@@ -78,17 +83,17 @@ export default {
let bytes = null
const bytesArray = {
- 'b': 1,
- 'k': 1024,
- 'kb': 1024,
- 'mb': 1024 * 1024,
- 'm': 1024 * 1024,
- 'gb': 1024 * 1024 * 1024,
- 'g': 1024 * 1024 * 1024,
- 'tb': 1024 * 1024 * 1024 * 1024,
- 't': 1024 * 1024 * 1024 * 1024,
- 'pb': 1024 * 1024 * 1024 * 1024 * 1024,
- 'p': 1024 * 1024 * 1024 * 1024 * 1024,
+ b: 1,
+ k: 1024,
+ kb: 1024,
+ mb: 1024 * 1024,
+ m: 1024 * 1024,
+ gb: 1024 * 1024 * 1024,
+ g: 1024 * 1024 * 1024,
+ tb: 1024 * 1024 * 1024 * 1024,
+ t: 1024 * 1024 * 1024 * 1024,
+ pb: 1024 * 1024 * 1024 * 1024 * 1024,
+ p: 1024 * 1024 * 1024 * 1024 * 1024,
}
const matches = s.match(/^[\s+]?([0-9]*)(\.([0-9]+))?( +)?([kmgtp]?b?)$/i)
@@ -113,7 +118,10 @@ export default {
* @param {string} format date format, see momentjs docs
* @returns {string} timestamp formatted as requested
*/
- formatDate: function(timestamp, format) {
+ formatDate(timestamp, format) {
+ if (window.TESTING === undefined) {
+ console.warn('OC.Util.formatDate is deprecated and will be removed in Nextcloud 21. See @nextcloud/moment')
+ }
format = format || 'LLL'
return moment(timestamp).format(format)
},
@@ -122,7 +130,10 @@ export default {
* @param {string|number} timestamp timestamp
* @returns {string} human readable difference from now
*/
- relativeModifiedDate: function(timestamp) {
+ relativeModifiedDate(timestamp) {
+ if (window.TESTING === undefined) {
+ console.warn('OC.Util.relativeModifiedDate is deprecated and will be removed in Nextcloud 21. See @nextcloud/moment')
+ }
const diff = moment().diff(moment(timestamp))
if (diff >= 0 && diff < 45000) {
return t('core', 'seconds ago')
@@ -135,7 +146,7 @@ export default {
*
* @returns {bool} true if this is IE, false otherwise
*/
- isIE: function() {
+ isIE() {
return $('html').hasClass('ie')
},
@@ -144,7 +155,7 @@ export default {
*
* @returns {int} width of scrollbar
*/
- getScrollBarWidth: function() {
+ getScrollBarWidth() {
if (this._scrollBarWidth) {
return this._scrollBarWidth
}
@@ -184,7 +195,7 @@ export default {
* @param {Date} date date
* @returns {Date} date with stripped time
*/
- stripTime: function(date) {
+ stripTime(date) {
// FIXME: likely to break when crossing DST
// would be better to use a library like momentJS
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
@@ -197,7 +208,7 @@ export default {
* @returns {number} -1 if b comes before a, 1 if a comes before b
* or 0 if the strings are identical
*/
- naturalSortCompare: function(a, b) {
+ naturalSortCompare(a, b) {
let x
const aa = chunkify(a)
const bb = chunkify(b)
@@ -224,7 +235,7 @@ export default {
* @param {function} callback function to call on success
* @param {integer} interval in milliseconds
*/
- waitFor: function(callback, interval) {
+ waitFor(callback, interval) {
const internalCallback = function() {
if (callback() !== true) {
setTimeout(internalCallback, interval)
@@ -240,7 +251,7 @@ export default {
* @param {string} value value of the cookie
* @returns {boolean} true if the cookie with the given name has the given value
*/
- isCookieSetToValue: function(name, value) {
+ isCookieSetToValue(name, value) {
const cookies = document.cookie.split(';')
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].split('=')
diff --git a/core/src/OC/webroot.js b/core/src/OC/webroot.js
index 89c04a6bb07..41752bc0c78 100644
--- a/core/src/OC/webroot.js
+++ b/core/src/OC/webroot.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +16,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
let webroot = window._oc_webroot
diff --git a/core/src/OC/xhr-error.js b/core/src/OC/xhr-error.js
index 8d2ad115336..660651dd0d9 100644
--- a/core/src/OC/xhr-error.js
+++ b/core/src/OC/xhr-error.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import _ from 'underscore'
diff --git a/core/src/OCA/index.js b/core/src/OCA/index.js
index 4b87d325a55..2f92eff367b 100644
--- a/core/src/OCA/index.js
+++ b/core/src/OCA/index.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,11 +16,19 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
+import Search from './search'
+
/**
* Namespace for apps
* @namespace OCA
*/
-export default {}
+export default {
+ /**
+ * @deprecated 20.0.0, will be removed in Nextcloud 22
+ */
+ Search,
+}
diff --git a/core/src/OC/search.js b/core/src/OCA/search.js
index 281907a06a7..909ec28480f 100644
--- a/core/src/OC/search.js
+++ b/core/src/OCA/search.js
@@ -1,7 +1,7 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,26 +16,17 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
-import OC from './index'
-
-/**
- * Do a search query and display the results
- * @param {string} query the search query
- */
-const search = function(query) {
- OC.Search.search(query, null, 0, 30)
-}
+export default class Search {
-/**
- * @namespace OC.search
- */
-search.customResults = {}
-/**
- * @deprecated use get/setFormatter() instead
- */
-search.resultTypes = {}
+ /**
+ * @deprecated 20.0.0, will be removed in Nextcloud 22
+ */
+ constructor() {
+ console.warn('OCA.Search is deprecated. Please use the unified search API instead')
+ }
-export default search
+}
diff --git a/core/src/OCP/appconfig.js b/core/src/OCP/appconfig.js
index d7aaf12ddeb..6e18f591894 100644
--- a/core/src/OCP/appconfig.js
+++ b/core/src/OCP/appconfig.js
@@ -1,6 +1,9 @@
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @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
@@ -14,7 +17,7 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
@@ -79,7 +82,7 @@ export function getKeys(app, options) {
export function getValue(app, key, defaultValue, options) {
options = options || {}
options.data = {
- defaultValue: defaultValue,
+ defaultValue,
}
call('get', '/' + app + '/' + key, options)
@@ -97,7 +100,7 @@ export function getValue(app, key, defaultValue, options) {
export function setValue(app, key, value, options) {
options = options || {}
options.data = {
- value: value,
+ value,
}
call('post', '/' + app + '/' + key, options)
diff --git a/core/src/OCP/collaboration.js b/core/src/OCP/collaboration.js
index 73573b3f1f7..06ff7b6b570 100644
--- a/core/src/OCP/collaboration.js
+++ b/core/src/OCP/collaboration.js
@@ -1,6 +1,7 @@
/**
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
diff --git a/core/src/OCP/comments.js b/core/src/OCP/comments.js
index 2e12accddce..5d346a80ba0 100644
--- a/core/src/OCP/comments.js
+++ b/core/src/OCP/comments.js
@@ -1,10 +1,24 @@
/**
* @copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @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/>.
*
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
*/
import $ from 'jquery'
diff --git a/core/src/OCP/index.js b/core/src/OCP/index.js
index d8640b1ff0c..cb524a2be70 100644
--- a/core/src/OCP/index.js
+++ b/core/src/OCP/index.js
@@ -1,13 +1,36 @@
/**
+ * @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 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/>.
*
*/
+
+import { loadState } from '@nextcloud/initial-state'
+
import * as AppConfig from './appconfig'
import * as Comments from './comments'
-import Loader from './loader'
-import { loadState } from '@nextcloud/initial-state'
+import * as WhatsNew from './whatsnew'
+
import Collaboration from './collaboration'
+import Loader from './loader'
import Toast from './toast'
-import * as WhatsNew from './whatsnew'
/** @namespace OCP */
export default {
@@ -21,6 +44,9 @@ export default {
loadState,
},
Loader,
+ /**
+ * @deprecated 19.0.0 use the `@nextcloud/dialogs` package instead
+ */
Toast,
WhatsNew,
}
diff --git a/core/src/OCP/loader.js b/core/src/OCP/loader.js
index 46087fcae19..9a41fa7910f 100644
--- a/core/src/OCP/loader.js
+++ b/core/src/OCP/loader.js
@@ -1,6 +1,8 @@
/**
* @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 GNU AGPL version 3 or any later version
@@ -35,7 +37,7 @@ export default {
* @param {string} file the script file name
* @returns {Promise}
*/
- loadScript: function(app, file) {
+ loadScript(app, file) {
const key = app + file
if (Object.prototype.hasOwnProperty.call(loadedScripts, key)) {
return Promise.resolve()
@@ -59,7 +61,7 @@ export default {
* @param {string} file the script file name
* @returns {Promise}
*/
- loadStylesheet: function(app, file) {
+ loadStylesheet(app, file) {
const key = app + file
if (Object.prototype.hasOwnProperty.call(loadedStylesheets, key)) {
return Promise.resolve()
diff --git a/core/src/OCP/toast.js b/core/src/OCP/toast.js
index 6bb3b130287..9fde3884987 100644
--- a/core/src/OCP/toast.js
+++ b/core/src/OCP/toast.js
@@ -1,6 +1,7 @@
-/*
+/**
* @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 GNU AGPL version 3 or any later version
@@ -20,70 +21,63 @@
*
*/
-import Toastify from 'toastify-js'
+import {
+ showError,
+ showInfo, showMessage,
+ showSuccess,
+ showWarning,
+} from '@nextcloud/dialogs'
-const TOAST_TYPE_CLASES = {
- error: 'toast-error',
- info: 'toast-info',
- warning: 'toast-warning',
- success: 'toast-success',
- permanent: 'permanent',
-}
-
-const Toast = {
-
- success(text, options = {}) {
- options.type = 'success'
- return this.message(text, options)
+export default {
+ /**
+ * @deprecated 19.0.0 use `showSuccess` from the `@nextcloud/dialogs` package instead
+ *
+ * @param {string} text the toast text
+ * @param {object} options options
+ * @returns {Toast}
+ */
+ success(text, options) {
+ return showSuccess(text, options)
},
-
- warning(text, options = {}) {
- options.type = 'warning'
- return this.message(text, options)
+ /**
+ * @deprecated 19.0.0 use `showWarning` from the `@nextcloud/dialogs` package instead
+ *
+ * @param {string} text the toast text
+ * @param {object} options options
+ * @returns {Toast}
+ */
+ warning(text, options) {
+ return showWarning(text, options)
},
-
- error(text, options = {}) {
- options.type = 'error'
- return this.message(text, options)
+ /**
+ * @deprecated 19.0.0 use `showError` from the `@nextcloud/dialogs` package instead
+ *
+ * @param {string} text the toast text
+ * @param {object} options options
+ * @returns {Toast}
+ */
+ error(text, options) {
+ return showError(text, options)
},
-
- info(text, options = {}) {
- options.type = 'info'
- return this.message(text, options)
+ /**
+ * @deprecated 19.0.0 use `showInfo` from the `@nextcloud/dialogs` package instead
+ *
+ * @param {string} text the toast text
+ * @param {object} options options
+ * @returns {Toast}
+ */
+ info(text, options) {
+ return showInfo(text, options)
},
-
+ /**
+ * @deprecated 19.0.0 use `showMessage` from the `@nextcloud/dialogs` package instead
+ *
+ * @param {string} text the toast text
+ * @param {object} options options
+ * @returns {Toast}
+ */
message(text, options) {
- options = options || {}
- _.defaults(options, {
- timeout: 7,
- isHTML: false,
- type: undefined,
- close: true,
- callback: () => {},
- })
- if (!options.isHTML) {
- text = $('<div/>').text(text).html()
- }
- let classes = ''
- if (options.type) {
- classes = TOAST_TYPE_CLASES[options.type]
- }
-
- const toast = Toastify({
- text: text,
- duration: options.timeout ? options.timeout * 1000 : null,
- callback: options.callback,
- close: options.close,
- gravity: 'top',
- selector: !window.TESTING ? 'content' : 'testArea',
- positionLeft: false,
- backgroundColor: '',
- className: 'toast ' + classes,
- })
- toast.showToast()
- // add toastify object to the element for reference in legacy OC.Notification
- toast.toastElement.toastify = toast
- return toast
+ return showMessage(text, options)
},
+
}
-export default Toast
diff --git a/core/src/OCP/whatsnew.js b/core/src/OCP/whatsnew.js
index c0df8af7b3f..afcffb7c34f 100644
--- a/core/src/OCP/whatsnew.js
+++ b/core/src/OCP/whatsnew.js
@@ -1,10 +1,24 @@
/**
* @copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @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/>.
*
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
*/
import _ from 'underscore'
@@ -59,14 +73,14 @@ function onQuerySuccess(data, statusText, xhr, dismissOptions) {
menuItem.className = 'menuitem'
text = document.createElement('span')
- text.innerText = t('core', 'New in') + ' ' + data['ocs']['data']['product']
+ text.innerText = t('core', 'New in') + ' ' + data.ocs.data.product
text.className = 'caption'
menuItem.appendChild(text)
icon = document.createElement('span')
icon.className = 'icon-close'
icon.onclick = function() {
- dismiss(data['ocs']['data']['version'], dismissOptions)
+ dismiss(data.ocs.data.version, dismissOptions)
}
menuItem.appendChild(icon)
@@ -74,8 +88,8 @@ function onQuerySuccess(data, statusText, xhr, dismissOptions) {
list.appendChild(item)
// Highlights
- for (const i in data['ocs']['data']['whatsNew']['regular']) {
- const whatsNewTextItem = data['ocs']['data']['whatsNew']['regular'][i]
+ for (const i in data.ocs.data.whatsNew.regular) {
+ const whatsNewTextItem = data.ocs.data.whatsNew.regular[i]
item = document.createElement('li')
menuItem = document.createElement('span')
@@ -94,11 +108,11 @@ function onQuerySuccess(data, statusText, xhr, dismissOptions) {
}
// Changelog URL
- if (!_.isUndefined(data['ocs']['data']['changelogURL'])) {
+ if (!_.isUndefined(data.ocs.data.changelogURL)) {
item = document.createElement('li')
menuItem = document.createElement('a')
- menuItem.href = data['ocs']['data']['changelogURL']
+ menuItem.href = data.ocs.data.changelogURL
menuItem.rel = 'noreferrer noopener'
menuItem.target = '_blank'
diff --git a/core/src/Polyfill/closest.js b/core/src/Polyfill/closest.js
index 6af05d526a7..059f981e973 100644
--- a/core/src/Polyfill/closest.js
+++ b/core/src/Polyfill/closest.js
@@ -1,3 +1,25 @@
+/**
+ * @copyright Copyright (c) 2016 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/>.
+ *
+ */
+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
if (!Element.prototype.matches) {
diff --git a/core/src/Polyfill/console.js b/core/src/Polyfill/console.js
index 0d60fe6f20d..0659cd6dc42 100644
--- a/core/src/Polyfill/console.js
+++ b/core/src/Polyfill/console.js
@@ -1,8 +1,8 @@
-/* eslint-disable no-console */
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -17,9 +17,11 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
+/* eslint-disable no-console */
if (typeof console === 'undefined' || typeof console.log === 'undefined') {
if (!window.console) {
window.console = {}
diff --git a/core/src/Polyfill/index.js b/core/src/Polyfill/index.js
index 306c72a0777..57a1f516695 100644
--- a/core/src/Polyfill/index.js
+++ b/core/src/Polyfill/index.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import './console'
diff --git a/core/src/Polyfill/tooltip.js b/core/src/Polyfill/tooltip.js
index 08cc58c338c..1001d62bd8d 100644
--- a/core/src/Polyfill/tooltip.js
+++ b/core/src/Polyfill/tooltip.js
@@ -1,7 +1,9 @@
-/*
+/**
* @copyright 2019 Julius Härtl <jus@bitgrid.net>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +18,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
$.prototype.tooltip = (function(tooltip) {
@@ -25,11 +28,15 @@ $.prototype.tooltip = (function(tooltip) {
return tooltip.call(this, config)
} catch (ex) {
if (ex instanceof TypeError && config === 'destroy') {
- console.error('Deprecated call $.tooltip(\'destroy\') has been deprecated and should be removed')
+ if (window.TESTING === undefined) {
+ console.error('Deprecated call $.tooltip(\'destroy\') has been deprecated and should be removed')
+ }
return tooltip.call(this, 'dispose')
}
if (ex instanceof TypeError && config === 'fixTitle') {
- console.error('Deprecated call $.tooltip(\'fixTitle\') has been deprecated and should be removed')
+ if (window.TESTING === undefined) {
+ console.error('Deprecated call $.tooltip(\'fixTitle\') has been deprecated and should be removed')
+ }
return tooltip.call(this, '_fixTitle')
}
}
diff --git a/core/src/Polyfill/windows-phone.js b/core/src/Polyfill/windows-phone.js
index 983e412e453..6bb01888c43 100644
--- a/core/src/Polyfill/windows-phone.js
+++ b/core/src/Polyfill/windows-phone.js
@@ -1,7 +1,8 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
// fix device width on windows phone
diff --git a/core/src/Util/get-url-parameter.js b/core/src/Util/get-url-parameter.js
index 6f809994f10..1bde89ed7b1 100644
--- a/core/src/Util/get-url-parameter.js
+++ b/core/src/Util/get-url-parameter.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,15 +17,10 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
-/**
- * Get the value of a URL parameter
- * @link http://stackoverflow.com/questions/1403888/get-url-parameter-with-jquery
- * @param {string} name URL parameter
- * @returns {string}
- */
export default function getURLParameter(name) {
return decodeURIComponent(
// eslint-disable-next-line no-sparse-arrays
diff --git a/core/src/Util/human-file-size.js b/core/src/Util/human-file-size.js
deleted file mode 100644
index 7f9eb7ab61d..00000000000
--- a/core/src/Util/human-file-size.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * @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/>.
- */
-
-/**
- * Returns a human readable file size
- * @param {number} size Size in bytes
- * @param {boolean} skipSmallSizes return '< 1 kB' for small files
- * @returns {string}
- */
-export default function humanFileSize(size, skipSmallSizes) {
- const humanList = ['B', 'KB', 'MB', 'GB', 'TB']
- // Calculate Log with base 1024: size = 1024 ** order
- let order = size > 0 ? Math.floor(Math.log(size) / Math.log(1024)) : 0
- // Stay in range of the byte sizes that are defined
- order = Math.min(humanList.length - 1, order)
- const readableFormat = humanList[order]
- let relativeSize = (size / Math.pow(1024, order)).toFixed(1)
- if (skipSmallSizes === true && order === 0) {
- if (relativeSize !== '0.0') {
- return '< 1 KB'
- } else {
- return '0 KB'
- }
- }
- if (order < 2) {
- relativeSize = parseFloat(relativeSize).toFixed(0)
- } else if (relativeSize.substr(relativeSize.length - 2, 2) === '.0') {
- relativeSize = relativeSize.substr(0, relativeSize.length - 2)
- } else {
- relativeSize = parseFloat(relativeSize).toLocaleString(OC.getCanonicalLocale())
- }
- return relativeSize + ' ' + readableFormat
-}
diff --git a/core/src/components/ContactsMenu.js b/core/src/components/ContactsMenu.js
index 0791fe83b0c..a2e09341067 100644
--- a/core/src/components/ContactsMenu.js
+++ b/core/src/components/ContactsMenu.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
diff --git a/core/src/components/HeaderMenu.vue b/core/src/components/HeaderMenu.vue
new file mode 100644
index 00000000000..12afa16e091
--- /dev/null
+++ b/core/src/components/HeaderMenu.vue
@@ -0,0 +1,215 @@
+ <!--
+ - @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/>.
+ -
+ -->
+<template>
+ <div
+ :id="id"
+ v-click-outside="clickOutsideConfig"
+ :class="{ 'header-menu--opened': opened }"
+ class="header-menu">
+ <a class="header-menu__trigger"
+ href="#"
+ :aria-controls="`header-menu-${id}`"
+ :aria-expanded="opened"
+ aria-haspopup="true"
+ @click.prevent="toggleMenu">
+ <slot name="trigger" />
+ </a>
+ <div v-if="opened"
+ :id="`header-menu-${id}`"
+ class="header-menu__wrapper"
+ role="menu">
+ <div class="header-menu__carret" />
+ <div class="header-menu__content">
+ <slot />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import { directive as ClickOutside } from 'v-click-outside'
+import excludeClickOutsideClasses from '@nextcloud/vue/dist/Mixins/excludeClickOutsideClasses'
+
+export default {
+ name: 'HeaderMenu',
+
+ directives: {
+ ClickOutside,
+ },
+
+ mixins: [
+ excludeClickOutsideClasses,
+ ],
+
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ open: {
+ type: Boolean,
+ default: false,
+ },
+ },
+
+ data() {
+ return {
+ opened: this.open,
+ clickOutsideConfig: {
+ handler: this.closeMenu,
+ middleware: this.clickOutsideMiddleware,
+ },
+ }
+ },
+
+ watch: {
+ open(newVal) {
+ this.opened = newVal
+ this.$nextTick(() => {
+ if (this.opened) {
+ this.openMenu()
+ } else {
+ this.closeMenu()
+ }
+ })
+ },
+ },
+
+ mounted() {
+ document.addEventListener('keydown', this.onKeyDown)
+ },
+ beforeDestroy() {
+ document.removeEventListener('keydown', this.onKeyDown)
+ },
+
+ methods: {
+ /**
+ * Toggle the current menu open state
+ */
+ toggleMenu() {
+ // Toggling current state
+ if (!this.opened) {
+ this.openMenu()
+ } else {
+ this.closeMenu()
+ }
+ },
+
+ /**
+ * Close the current menu
+ */
+ closeMenu() {
+ if (!this.opened) {
+ return
+ }
+
+ this.opened = false
+ this.$emit('close')
+ this.$emit('update:open', false)
+ },
+
+ /**
+ * Open the current menu
+ */
+ openMenu() {
+ if (this.opened) {
+ return
+ }
+
+ this.opened = true
+ this.$emit('open')
+ this.$emit('update:open', true)
+ },
+
+ onKeyDown(event) {
+ // If opened and escape pressed, close
+ if (event.key === 'Escape' && this.opened) {
+ event.preventDefault()
+
+ /** user cancelled the menu by pressing escape */
+ this.$emit('cancel')
+
+ /** we do NOT fire a close event to differentiate cancel and close */
+ this.opened = false
+ this.$emit('update:open', false)
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.header-menu {
+ &__trigger {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 50px;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ cursor: pointer;
+ opacity: .6;
+ }
+
+ &--opened &__trigger,
+ &__trigger:hover,
+ &__trigger:focus,
+ &__trigger:active {
+ opacity: 1;
+ }
+
+ &__wrapper {
+ position: absolute;
+ z-index: 2000;
+ top: 50px;
+ right: 5px;
+ box-sizing: border-box;
+ margin: 0;
+ border-radius: 0 0 var(--border-radius) var(--border-radius);
+ background-color: var(--color-main-background);
+
+ filter: drop-shadow(0 1px 5px var(--color-box-shadow));
+ }
+
+ &__carret {
+ position: absolute;
+ right: 10px;
+ bottom: 100%;
+ width: 0;
+ height: 0;
+ content: ' ';
+ pointer-events: none;
+ border: 10px solid transparent;
+ border-bottom-color: var(--color-main-background);
+ }
+
+ &__content {
+ overflow: auto;
+ width: 350px;
+ max-width: 350px;
+ min-height: calc(44px * 1.5);
+ max-height: calc(100vh - 50px * 2);
+ }
+}
+
+</style>
diff --git a/core/src/components/MainMenu.js b/core/src/components/MainMenu.js
index d57711010ea..3b5aa19245e 100644
--- a/core/src/components/MainMenu.js
+++ b/core/src/components/MainMenu.js
@@ -1,7 +1,9 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +18,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
@@ -29,6 +32,26 @@ import OC from '../OC'
* If the screen is bigger, the main menu is not a toggle any more.
*/
export const setUp = () => {
+
+ Object.assign(OC, {
+ setNavigationCounter(id, counter) {
+ const appmenuElement = document.getElementById('appmenu').querySelector('[data-id="' + id + '"] svg')
+ const appsElement = document.getElementById('apps').querySelector('[data-id="' + id + '"] svg')
+ if (counter === 0) {
+ appmenuElement.classList.remove('has-unread')
+ appsElement.classList.remove('has-unread')
+ appmenuElement.getElementsByTagName('image')[0].style.mask = ''
+ appsElement.getElementsByTagName('image')[0].style.mask = ''
+ } else {
+ appmenuElement.classList.add('has-unread')
+ appsElement.classList.add('has-unread')
+ appmenuElement.getElementsByTagName('image')[0].style.mask = 'url(#hole)'
+ appsElement.getElementsByTagName('image')[0].style.mask = 'url(#hole)'
+ }
+ document.getElementById('appmenu').querySelector('[data-id="' + id + '"] .unread-counter').textContent = counter
+ document.getElementById('apps').querySelector('[data-id="' + id + '"] .unread-counter').textContent = counter
+ },
+ })
// init the more-apps menu
OC.registerMenu($('#more-apps > a'), $('#navigation'))
diff --git a/core/src/components/UnifiedSearch/SearchResult.vue b/core/src/components/UnifiedSearch/SearchResult.vue
new file mode 100644
index 00000000000..e50cc413d03
--- /dev/null
+++ b/core/src/components/UnifiedSearch/SearchResult.vue
@@ -0,0 +1,257 @@
+ <!--
+ - @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/>.
+ -
+ -->
+<template>
+ <a :href="resourceUrl || '#'"
+ class="unified-search__result"
+ :class="{
+ 'unified-search__result--focused': focused,
+ }"
+ @click="reEmitEvent"
+ @focus="reEmitEvent">
+
+ <!-- Icon describing the result -->
+ <div class="unified-search__result-icon"
+ :class="{
+ 'unified-search__result-icon--rounded': rounded,
+ 'unified-search__result-icon--no-preview': !hasValidThumbnail && !loaded,
+ 'unified-search__result-icon--with-thumbnail': hasValidThumbnail && loaded,
+ [icon]: !loaded && !isIconUrl,
+ }"
+ :style="{
+ backgroundImage: isIconUrl ? `url(${icon})` : '',
+ }"
+ role="img">
+
+ <img v-if="hasValidThumbnail"
+ v-show="loaded"
+ :src="thumbnailUrl"
+ alt=""
+ @error="onError"
+ @load="onLoad">
+ </div>
+
+ <!-- Title and sub-title -->
+ <span class="unified-search__result-content">
+ <h3 class="unified-search__result-line-one" :title="title">
+ <Highlight :text="title" :search="query" />
+ </h3>
+ <h4 v-if="subline" class="unified-search__result-line-two" :title="subline">{{ subline }}</h4>
+ </span>
+ </a>
+</template>
+
+<script>
+import Highlight from '@nextcloud/vue/dist/Components/Highlight'
+
+export default {
+ name: 'SearchResult',
+
+ components: {
+ Highlight,
+ },
+
+ props: {
+ thumbnailUrl: {
+ type: String,
+ default: null,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ subline: {
+ type: String,
+ default: null,
+ },
+ resourceUrl: {
+ type: String,
+ default: null,
+ },
+ icon: {
+ type: String,
+ default: '',
+ },
+ rounded: {
+ type: Boolean,
+ default: false,
+ },
+ query: {
+ type: String,
+ default: '',
+ },
+
+ /**
+ * Only used for the first result as a visual feedback
+ * so we can keep the search input focused but pressing
+ * enter still opens the first result
+ */
+ focused: {
+ type: Boolean,
+ default: false,
+ },
+ },
+
+ data() {
+ return {
+ hasValidThumbnail: this.thumbnailUrl && this.thumbnailUrl.trim() !== '',
+ loaded: false,
+ }
+ },
+
+ computed: {
+ isIconUrl() {
+ // If we're facing an absolute url
+ if (this.icon.startsWith('/')) {
+ return true
+ }
+
+ // Otherwise, let's check if this is a valid url
+ try {
+ // eslint-disable-next-line no-new
+ new URL(this.icon)
+ } catch {
+ return false
+ }
+ return true
+ },
+ },
+
+ watch: {
+ // Make sure to reset state on change even when vue recycle the component
+ thumbnailUrl() {
+ this.hasValidThumbnail = this.thumbnailUrl && this.thumbnailUrl.trim() !== ''
+ this.loaded = false
+ },
+ },
+
+ methods: {
+ reEmitEvent(e) {
+ this.$emit(e.type, e)
+ },
+
+ /**
+ * If the image fails to load, fallback to iconClass
+ */
+ onError() {
+ this.hasValidThumbnail = false
+ },
+
+ onLoad() {
+ this.loaded = true
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+$clickable-area: 44px;
+$margin: 10px;
+
+.unified-search__result {
+ display: flex;
+ height: $clickable-area;
+ padding: $margin;
+ border-bottom: 1px solid var(--color-border);
+
+ // Load more entry,
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &--focused,
+ &:active,
+ &:hover,
+ &:focus {
+ background-color: var(--color-background-hover);
+ }
+
+ * {
+ 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: $clickable-area / 2;
+ }
+ &--no-preview {
+ background-size: 32px;
+ }
+ &--with-thumbnail {
+ background-size: cover;
+ }
+ &--with-thumbnail:not(&--rounded) {
+ // compensate for border
+ max-width: $clickable-area - 2px;
+ max-height: $clickable-area - 2px;
+ border: 1px solid var(--color-border);
+ }
+
+ img {
+ // Make sure to keep ratio
+ width: 100%;
+ height: 100%;
+
+ object-fit: cover;
+ object-position: center;
+ }
+ }
+
+ &-icon,
+ &-actions {
+ flex: 0 0 $clickable-area;
+ }
+
+ &-content {
+ display: flex;
+ align-items: center;
+ flex: 1 1 100%;
+ flex-wrap: wrap;
+ // Set to minimum and gro from it
+ min-width: 0;
+ padding-left: $margin;
+ }
+
+ &-line-one,
+ &-line-two {
+ overflow: hidden;
+ flex: 1 1 100%;
+ margin: 1px 0;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ // Use the same color as the `a`
+ color: inherit;
+ font-size: inherit;
+ }
+ &-line-two {
+ opacity: .7;
+ font-size: var(--default-font-size);
+ }
+}
+
+</style>
diff --git a/core/src/components/UnifiedSearch/SearchResultPlaceholders.vue b/core/src/components/UnifiedSearch/SearchResultPlaceholders.vue
new file mode 100644
index 00000000000..31f85f413d3
--- /dev/null
+++ b/core/src/components/UnifiedSearch/SearchResultPlaceholders.vue
@@ -0,0 +1,100 @@
+<template>
+ <ul>
+ <!-- Placeholder animation -->
+ <svg class="unified-search__result-placeholder-gradient">
+ <defs>
+ <linearGradient id="unified-search__result-placeholder-gradient">
+ <stop offset="0%" :stop-color="light">
+ <animate attributeName="stop-color"
+ :values="`${light}; ${light}; ${dark}; ${dark}; ${light}`"
+ dur="2s"
+ repeatCount="indefinite" />
+ </stop>
+ <stop offset="100%" :stop-color="dark">
+ <animate attributeName="stop-color"
+ :values="`${dark}; ${light}; ${light}; ${dark}; ${dark}`"
+ dur="2s"
+ repeatCount="indefinite" />
+ </stop>
+ </linearGradient>
+ </defs>
+ </svg>
+
+ <!-- Placeholders -->
+ <li v-for="placeholder in [1, 2, 3]" :key="placeholder">
+ <svg
+ class="unified-search__result-placeholder"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="url(#unified-search__result-placeholder-gradient)">
+ <rect class="unified-search__result-placeholder-icon" />
+ <rect class="unified-search__result-placeholder-line-one" />
+ <rect class="unified-search__result-placeholder-line-two" :style="{width: `calc(${randWidth()}%)`}" />
+ </svg>
+ </li>
+ </ul>
+</template>
+
+<script>
+export default {
+ name: 'SearchResultPlaceholders',
+
+ data() {
+ return {
+ light: null,
+ dark: null,
+ }
+ },
+ mounted() {
+ const styles = getComputedStyle(document.documentElement)
+ this.dark = styles.getPropertyValue('--color-placeholder-dark')
+ this.light = styles.getPropertyValue('--color-placeholder-light')
+ },
+
+ methods: {
+ randWidth() {
+ return Math.floor(Math.random() * 20) + 30
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+$clickable-area: 44px;
+$margin: 10px;
+
+.unified-search__result-placeholder-gradient {
+ position: fixed;
+ height: 0;
+ width: 0;
+ z-index: -1;
+}
+
+.unified-search__result-placeholder {
+ width: calc(100% - 2 * #{$margin});
+ height: $clickable-area;
+ margin: $margin;
+
+ &-icon {
+ width: $clickable-area;
+ height: $clickable-area;
+ rx: var(--border-radius);
+ ry: var(--border-radius);
+ }
+
+ &-line-one,
+ &-line-two {
+ width: calc(100% - #{$margin + $clickable-area});
+ height: 1em;
+ x: $margin + $clickable-area;
+ }
+
+ &-line-one {
+ y: 5px;
+ }
+
+ &-line-two {
+ y: 25px;
+ }
+}
+
+</style>
diff --git a/core/src/components/UserMenu.js b/core/src/components/UserMenu.js
index a9e7d8725bb..c4cb6527da3 100644
--- a/core/src/components/UserMenu.js
+++ b/core/src/components/UserMenu.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +16,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import OC from '../OC'
diff --git a/core/src/components/login/LoginButton.vue b/core/src/components/login/LoginButton.vue
new file mode 100644
index 00000000000..f7d426e6c63
--- /dev/null
+++ b/core/src/components/login/LoginButton.vue
@@ -0,0 +1,56 @@
+<!--
+ - @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/>.
+ -->
+
+<template>
+ <div id="submit-wrapper" @click="$emit('click')">
+ <input id="submit-form"
+ type="submit"
+ class="login primary"
+ title=""
+ :value="!loading ? t('core', 'Log in') : t('core', 'Logging in …')">
+ <div class="submit-icon"
+ :class="{
+ 'icon-confirm-white': !loading,
+ 'icon-loading-small': loading && invertedColors,
+ 'icon-loading-small-dark': loading && !invertedColors,
+ }" />
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'LoginButton',
+ props: {
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ invertedColors: {
+ type: Boolean,
+ default: false,
+ },
+ },
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/core/src/components/login/LoginForm.vue b/core/src/components/login/LoginForm.vue
index 687896ceb54..c75b6f2c7ea 100644
--- a/core/src/components/login/LoginForm.vue
+++ b/core/src/components/login/LoginForm.vue
@@ -20,9 +20,10 @@
-->
<template>
- <form method="post"
+ <form ref="loginForm"
+ method="post"
name="login"
- :action="OC.generateUrl('login')"
+ :action="loginActionUrl"
@submit="submit">
<fieldset>
<div v-if="apacheAuthFailed"
@@ -46,7 +47,7 @@
class="hidden">
<img class="float-spinner"
alt=""
- :src="OC.imagePath('core', 'loading-dark.gif')">
+ :src="loadingIcon">
<span id="messageText" />
<!-- the following div ensures that the spinner is always inside the #message div -->
<div style="clear: both;" />
@@ -58,12 +59,13 @@
v-model="user"
type="text"
name="user"
+ autocapitalize="off"
:autocomplete="autoCompleteAllowed ? 'on' : 'off'"
:placeholder="t('core', 'Username or email')"
:aria-label="t('core', 'Username or email')"
required
@change="updateUsername">
- <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
+ <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
</p>
<p class="groupbottom"
@@ -80,23 +82,11 @@
<label for="password"
class="infield">{{ t('Password') }}</label>
<a href="#" class="toggle-password" @click.stop.prevent="togglePassword">
- <img :src="OC.imagePath('core', 'actions/toggle.svg')">
+ <img :src="toggleIcon" :alt="t('core', 'Toggle password visibility')">
</a>
</p>
- <div id="submit-wrapper">
- <input id="submit-form"
- type="submit"
- class="login primary"
- title=""
- :value="!loading ? t('core', 'Log in') : t('core', 'Logging in …')">
- <div class="submit-icon"
- :class="{
- 'icon-confirm-white': !loading,
- 'icon-loading-small': loading && invertedColors,
- 'icon-loading-small-dark': loading && !invertedColors,
- }" />
- </div>
+ <LoginButton :loading="loading" :inverted-colors="invertedColors" />
<p v-if="invalidPassword"
class="warning wrongPasswordMsg">
@@ -104,7 +94,7 @@
</p>
<p v-else-if="userDisabled"
class="warning userDisabledMsg">
- {{ t('lib', 'User disabled') }}
+ {{ t('core', 'User disabled') }}
</p>
<p v-if="throttleDelay && throttleDelay > 5000"
@@ -135,9 +125,15 @@
<script>
import jstz from 'jstimezonedetect'
+import LoginButton from './LoginButton'
+import {
+ generateUrl,
+ imagePath,
+} from '@nextcloud/router'
export default {
name: 'LoginForm',
+ components: { LoginButton },
props: {
username: {
type: String,
@@ -193,6 +189,15 @@ export default {
userDisabled() {
return this.errors.indexOf('userdisabled') !== -1
},
+ toggleIcon() {
+ return imagePath('core', 'actions/toggle.svg')
+ },
+ loadingIcon() {
+ return imagePath('core', 'loading-dark.gif')
+ },
+ loginActionUrl() {
+ return generateUrl('login')
+ },
},
mounted() {
if (this.username === '') {
diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue
new file mode 100644
index 00000000000..0203bd74c48
--- /dev/null
+++ b/core/src/components/login/PasswordLessLoginForm.vue
@@ -0,0 +1,216 @@
+<template>
+ <form v-if="(isHttps || isLocalhost) && hasPublicKeyCredential"
+ ref="loginForm"
+ method="post"
+ name="login"
+ @submit.prevent="submit">
+ <fieldset>
+ <p class="grouptop groupbottom">
+ <input id="user"
+ ref="user"
+ v-model="user"
+ type="text"
+ name="user"
+ :autocomplete="autoCompleteAllowed ? 'on' : 'off'"
+ :placeholder="t('core', 'Username or email')"
+ :aria-label="t('core', 'Username or email')"
+ required
+ @change="$emit('update:username', user)">
+ <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
+ </p>
+
+ <div v-if="!validCredentials">
+ {{ t('core', 'Your account is not setup for passwordless login.') }}
+ </div>
+
+ <LoginButton v-if="validCredentials"
+ :loading="loading"
+ :inverted-colors="invertedColors"
+ @click="authenticate" />
+ </fieldset>
+ </form>
+ <div v-else-if="!hasPublicKeyCredential">
+ {{ t('core', 'Passwordless authentication is not supported in your browser.') }}
+ </div>
+ <div v-else-if="!isHttps && !isLocalhost">
+ {{ t('core', 'Passwordless authentication is only available over a secure connection.') }}
+ </div>
+</template>
+
+<script>
+import {
+ startAuthentication,
+ finishAuthentication,
+} from '../../services/WebAuthnAuthenticationService'
+import LoginButton from './LoginButton'
+
+class NoValidCredentials extends Error {
+
+}
+
+export default {
+ name: 'PasswordLessLoginForm',
+ components: {
+ LoginButton,
+ },
+ props: {
+ username: {
+ type: String,
+ default: '',
+ },
+ redirectUrl: {
+ type: String,
+ },
+ invertedColors: {
+ type: Boolean,
+ default: false,
+ },
+ autoCompleteAllowed: {
+ type: Boolean,
+ default: true,
+ },
+ isHttps: {
+ type: Boolean,
+ default: false,
+ },
+ isLocalhost: {
+ type: Boolean,
+ default: false,
+ },
+ hasPublicKeyCredential: {
+ type: Boolean,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ user: this.username,
+ loading: false,
+ validCredentials: true,
+ }
+ },
+ methods: {
+ authenticate() {
+ 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)
+ })
+ },
+ 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 location = this.redirectUrl
+
+ return finishAuthentication(JSON.stringify(challenge))
+ .then(data => {
+ console.debug('Logged in redirecting')
+ window.location.href = location
+ })
+ .catch(error => {
+ console.debug('GOT AN ERROR WHILE SUBMITTING CHALLENGE!')
+ console.debug(error) // Example: timeout, interaction refused...
+ })
+ },
+ submit() {
+ // noop
+ },
+ },
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/core/src/components/login/ResetPassword.vue b/core/src/components/login/ResetPassword.vue
index 2d045692285..79589a96aca 100644
--- a/core/src/components/login/ResetPassword.vue
+++ b/core/src/components/login/ResetPassword.vue
@@ -21,61 +21,64 @@
<template>
<form @submit.prevent="submit">
- <p>
- <input id="user"
- v-model="user"
- type="text"
- name="user"
- :placeholder="t('core', 'Username or email')"
- :aria-label="t('core', 'Username or email')"
- required
- @change="updateUsername">
- <!--<?php p($_['user_autofocus'] ? 'autofocus' : ''); ?>
- autocomplete="<?php p($_['login_form_autocomplete']); ?>" autocapitalize="none" autocorrect="off"-->
- <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
- </p>
- <div id="reset-password-wrapper">
- <input id="reset-password-submit"
- type="submit"
- class="login primary"
- title=""
- :value="t('core', 'Reset password')">
- <div class="submit-icon"
- :class="{
- 'icon-confirm-white': !loading,
- 'icon-loading-small': loading && invertedColors,
- 'icon-loading-small-dark': loading && !invertedColors,
- }" />
- </div>
- <p v-if="message === 'send-success'"
- class="update">
- {{ t('core', 'A password reset message has been sent to the e-mail address of this account. If you do not receive it, check your spam/junk folders or ask your local administrator for help.') }}
- <br>
- {{ t('core', 'If it is not there ask your local administrator.') }}
- </p>
- <p v-else-if="message === 'send-error'"
- class="update warning">
- {{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }}
- </p>
- <p v-else-if="message === 'reset-error'"
- class="update warning">
- {{ t('core', 'Password can not be changed. Please contact your administrator.') }}
- </p>
- <p v-else-if="message"
- class="update"
- :class="{warning: error}" />
+ <fieldset>
+ <p>
+ <input id="user"
+ v-model="user"
+ type="text"
+ name="user"
+ autocapitalize="off"
+ :placeholder="t('core', 'Username or email')"
+ :aria-label="t('core', 'Username or email')"
+ required
+ @change="updateUsername">
+ <!--<?php p($_['user_autofocus'] ? 'autofocus' : ''); ?>
+ autocomplete="<?php p($_['login_form_autocomplete']); ?>" autocapitalize="none" autocorrect="off"-->
+ <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
+ </p>
+ <div id="reset-password-wrapper">
+ <input id="reset-password-submit"
+ type="submit"
+ class="login primary"
+ title=""
+ :value="t('core', 'Reset password')">
+ <div class="submit-icon"
+ :class="{
+ 'icon-confirm-white': !loading,
+ 'icon-loading-small': loading && invertedColors,
+ 'icon-loading-small-dark': loading && !invertedColors,
+ }" />
+ </div>
+ <p v-if="message === 'send-success'"
+ class="update">
+ {{ t('core', 'A password reset message has been sent to the email address of this account. If you do not receive it, check your spam/junk folders or ask your local administrator for help.') }}
+ <br>
+ {{ t('core', 'If it is not there ask your local administrator.') }}
+ </p>
+ <p v-else-if="message === 'send-error'"
+ class="update warning">
+ {{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }}
+ </p>
+ <p v-else-if="message === 'reset-error'"
+ class="update warning">
+ {{ t('core', 'Password cannot be changed. Please contact your administrator.') }}
+ </p>
+ <p v-else-if="message"
+ class="update"
+ :class="{warning: error}" />
- <a href="#"
- @click.prevent="$emit('abort')">
- {{ t('core', 'Back to login') }}
- </a>
+ <a href="#"
+ @click.prevent="$emit('abort')">
+ {{ t('core', 'Back to login') }}
+ </a>
+ </fieldset>
</form>
</template>
<script>
import axios from '@nextcloud/axios'
-import { generateUrl } from '../../OC/routing'
+import { generateUrl } from '@nextcloud/router'
export default {
name: 'ResetPassword',
@@ -130,7 +133,7 @@ export default {
this.message = 'send-success'
})
.catch(e => {
- console.error('could not send reset e-mail request', e)
+ console.error('could not send reset email request', e)
this.error = true
this.message = 'send-error'
diff --git a/core/src/components/login/UpdatePassword.vue b/core/src/components/login/UpdatePassword.vue
index 3fa3c60773c..cb615a846d2 100644
--- a/core/src/components/login/UpdatePassword.vue
+++ b/core/src/components/login/UpdatePassword.vue
@@ -125,7 +125,7 @@ export default {
}
} catch (e) {
this.error = true
- this.message = e.message ? e.message : t('core', 'Password can not be changed. Please contact your administrator.')
+ this.message = e.message ? e.message : t('core', 'Password cannot be changed. Please contact your administrator.')
} finally {
this.loading = false
}
diff --git a/core/src/components/setup/RecommendedApps.vue b/core/src/components/setup/RecommendedApps.vue
index 550c3aaff70..581fb99582c 100644
--- a/core/src/components/setup/RecommendedApps.vue
+++ b/core/src/components/setup/RecommendedApps.vue
@@ -26,17 +26,17 @@
{{ t('core', 'Loading apps …') }}
</p>
<p v-else-if="loadingAppsError" class="loading-error text-center">
- {{ t('core', 'Could not fetch list of apps from the app store.') }}
+ {{ t('core', 'Could not fetch list of apps from the App Store.') }}
</p>
<p v-else class="text-center">
{{ t('core', 'Installing apps …') }}
</p>
<div v-for="app in recommendedApps" :key="app.id" class="app">
- <img :src="customIcon(app.id)" :alt="t('core', 'Nextcloud {app}', { app: app.name })">
+ <img :src="customIcon(app.id)" alt="">
<div class="info">
<h3>
{{ app.name }}
- <span v-if="app.loading" class="icon icon-loading-small" />
+ <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)" />
@@ -44,10 +44,10 @@
<strong>{{ t('core', 'App download or installation failed') }}</strong>
</p>
<p v-else-if="!app.isCompatible">
- <strong>{{ t('core', 'Can\'t install this app because it is not compatible') }}</strong>
+ <strong>{{ t('core', 'Cannot install this app because it is not compatible') }}</strong>
</p>
<p v-else-if="!app.canInstall">
- <strong>{{ t('core', 'Can\'t install this app') }}</strong>
+ <strong>{{ t('core', 'Cannot install this app') }}</strong>
</p>
</div>
</div>
@@ -82,11 +82,11 @@ const recommended = {
spreed: {
description: t('core', 'Chatting, video calls, screensharing, online meetings and web conferencing – in your browser and with mobile apps.'),
},
- onlyoffice: {
+ richdocuments: {
description: t('core', 'Collaboratively edit office documents.'),
},
- documentserver_community: {
- description: t('core', 'Local document editing back-end used by the OnlyOffice app.'),
+ richdocumentscode: {
+ description: t('core', 'Local document editing back-end used by the Collabora Online app.'),
},
}
const recommendedIds = Object.keys(recommended)
@@ -135,7 +135,7 @@ export default {
.map(app => limit(() => {
logger.info(`installing ${app.id}`)
app.loading = true
- return axios.post(generateUrl(`settings/apps/enable`), { appIds: [app.id], groups: [] })
+ return axios.post(generateUrl('settings/apps/enable'), { appIds: [app.id], groups: [] })
.catch(error => {
logger.error(`could not install ${app.id}`, { error })
app.installationError = true
diff --git a/core/src/files/client.js b/core/src/files/client.js
new file mode 100644
index 00000000000..679457e38cf
--- /dev/null
+++ b/core/src/files/client.js
@@ -0,0 +1,986 @@
+/**
+ * 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 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/>.
+ *
+ */
+
+/* eslint-disable */
+import escapeHTML from 'escape-html'
+
+/* global dav */
+
+(function(OC, FileInfo) {
+ /**
+ * @class OC.Files.Client
+ * @classdesc Client to access files on the server
+ *
+ * @param {Object} options
+ * @param {String} options.host host name
+ * @param {int} [options.port] port
+ * @param {boolean} [options.useHTTPS] whether to use https
+ * @param {String} [options.root] root path
+ * @param {String} [options.userName] user name
+ * @param {String} [options.password] password
+ *
+ * @since 8.2
+ */
+ var Client = function(options) {
+ this._root = options.root
+ if (this._root.charAt(this._root.length - 1) === '/') {
+ this._root = this._root.substr(0, this._root.length - 1)
+ }
+
+ let url = Client.PROTOCOL_HTTP + '://'
+ if (options.useHTTPS) {
+ url = Client.PROTOCOL_HTTPS + '://'
+ }
+
+ url += options.host + this._root
+ this._host = options.host
+ this._defaultHeaders = options.defaultHeaders || {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'requesttoken': OC.requestToken,
+ }
+ this._baseUrl = url
+
+ const clientOptions = {
+ baseUrl: this._baseUrl,
+ xmlNamespaces: {
+ 'DAV:': 'd',
+ 'http://owncloud.org/ns': 'oc',
+ 'http://nextcloud.org/ns': 'nc',
+ 'http://open-collaboration-services.org/ns': 'ocs',
+ },
+ }
+ if (options.userName) {
+ clientOptions.userName = options.userName
+ }
+ if (options.password) {
+ clientOptions.password = options.password
+ }
+ this._client = new dav.Client(clientOptions)
+ this._client.xhrProvider = _.bind(this._xhrProvider, this)
+ this._fileInfoParsers = []
+ }
+
+ Client.NS_OWNCLOUD = 'http://owncloud.org/ns'
+ Client.NS_NEXTCLOUD = 'http://nextcloud.org/ns'
+ Client.NS_DAV = 'DAV:'
+ Client.NS_OCS = 'http://open-collaboration-services.org/ns'
+
+ Client.PROPERTY_GETLASTMODIFIED = '{' + Client.NS_DAV + '}getlastmodified'
+ Client.PROPERTY_GETETAG = '{' + Client.NS_DAV + '}getetag'
+ Client.PROPERTY_GETCONTENTTYPE = '{' + Client.NS_DAV + '}getcontenttype'
+ Client.PROPERTY_RESOURCETYPE = '{' + Client.NS_DAV + '}resourcetype'
+ Client.PROPERTY_INTERNAL_FILEID = '{' + Client.NS_OWNCLOUD + '}fileid'
+ Client.PROPERTY_PERMISSIONS = '{' + Client.NS_OWNCLOUD + '}permissions'
+ Client.PROPERTY_SIZE = '{' + Client.NS_OWNCLOUD + '}size'
+ Client.PROPERTY_GETCONTENTLENGTH = '{' + Client.NS_DAV + '}getcontentlength'
+ Client.PROPERTY_ISENCRYPTED = '{' + Client.NS_DAV + '}is-encrypted'
+ Client.PROPERTY_SHARE_PERMISSIONS = '{' + Client.NS_OCS + '}share-permissions'
+ Client.PROPERTY_QUOTA_AVAILABLE_BYTES = '{' + Client.NS_DAV + '}quota-available-bytes'
+
+ Client.PROTOCOL_HTTP = 'http'
+ Client.PROTOCOL_HTTPS = 'https'
+
+ Client._PROPFIND_PROPERTIES = [
+ /**
+ * Modified time
+ */
+ [Client.NS_DAV, 'getlastmodified'],
+ /**
+ * Etag
+ */
+ [Client.NS_DAV, 'getetag'],
+ /**
+ * Mime type
+ */
+ [Client.NS_DAV, 'getcontenttype'],
+ /**
+ * Resource type "collection" for folders, empty otherwise
+ */
+ [Client.NS_DAV, 'resourcetype'],
+ /**
+ * File id
+ */
+ [Client.NS_OWNCLOUD, 'fileid'],
+ /**
+ * Letter-coded permissions
+ */
+ [Client.NS_OWNCLOUD, 'permissions'],
+ // [Client.NS_OWNCLOUD, 'downloadURL'],
+ /**
+ * Folder sizes
+ */
+ [Client.NS_OWNCLOUD, 'size'],
+ /**
+ * File sizes
+ */
+ [Client.NS_DAV, 'getcontentlength'],
+ [Client.NS_DAV, 'quota-available-bytes'],
+ /**
+ * Preview availability
+ */
+ [Client.NS_NEXTCLOUD, 'has-preview'],
+ /**
+ * Mount type
+ */
+ [Client.NS_NEXTCLOUD, 'mount-type'],
+ /**
+ * Encryption state
+ */
+ [Client.NS_NEXTCLOUD, 'is-encrypted'],
+ /**
+ * Share permissions
+ */
+ [Client.NS_OCS, 'share-permissions'],
+ ]
+
+ /**
+ * @memberof OC.Files
+ */
+ Client.prototype = {
+
+ /**
+ * Root path of the Webdav endpoint
+ *
+ * @type string
+ */
+ _root: null,
+
+ /**
+ * Client from the library
+ *
+ * @type dav.Client
+ */
+ _client: null,
+
+ /**
+ * Array of file info parsing functions.
+ *
+ * @type Array<OC.Files.Client~parseFileInfo>
+ */
+ _fileInfoParsers: [],
+
+ /**
+ * Returns the configured XHR provider for davclient
+ * @returns {XMLHttpRequest}
+ */
+ _xhrProvider: function() {
+ const headers = this._defaultHeaders
+ const xhr = new XMLHttpRequest()
+ const oldOpen = xhr.open
+ // override open() method to add headers
+ xhr.open = function() {
+ const result = oldOpen.apply(this, arguments)
+ _.each(headers, function(value, key) {
+ xhr.setRequestHeader(key, value)
+ })
+ return result
+ }
+
+ OC.registerXHRForErrorProcessing(xhr)
+ return xhr
+ },
+
+ /**
+ * Prepends the base url to the given path sections
+ *
+ * @param {...String} path sections
+ *
+ * @returns {String} base url + joined path, any leading or trailing slash
+ * will be kept
+ */
+ _buildUrl: function() {
+ let path = this._buildPath.apply(this, arguments)
+ if (path.charAt([path.length - 1]) === '/') {
+ path = path.substr(0, path.length - 1)
+ }
+ if (path.charAt(0) === '/') {
+ path = path.substr(1)
+ }
+ return this._baseUrl + '/' + path
+ },
+
+ /**
+ * Append the path to the root and also encode path
+ * sections
+ *
+ * @param {...String} path sections
+ *
+ * @returns {String} joined path, any leading or trailing slash
+ * will be kept
+ */
+ _buildPath: function() {
+ let path = OC.joinPaths.apply(this, arguments)
+ const sections = path.split('/')
+ let i
+ for (i = 0; i < sections.length; i++) {
+ sections[i] = encodeURIComponent(sections[i])
+ }
+ path = sections.join('/')
+ return path
+ },
+
+ /**
+ * Parse headers string into a map
+ *
+ * @param {string} headersString headers list as string
+ *
+ * @returns {Object.<String,Array>} map of header name to header contents
+ */
+ _parseHeaders: function(headersString) {
+ const headerRows = headersString.split('\n')
+ const headers = {}
+ for (let i = 0; i < headerRows.length; i++) {
+ const sepPos = headerRows[i].indexOf(':')
+ if (sepPos < 0) {
+ continue
+ }
+
+ const headerName = headerRows[i].substr(0, sepPos)
+ const headerValue = headerRows[i].substr(sepPos + 2)
+
+ if (!headers[headerName]) {
+ // make it an array
+ headers[headerName] = []
+ }
+
+ headers[headerName].push(headerValue)
+ }
+ return headers
+ },
+
+ /**
+ * Parses the etag response which is in double quotes.
+ *
+ * @param {string} etag etag value in double quotes
+ *
+ * @returns {string} etag without double quotes
+ */
+ _parseEtag: function(etag) {
+ if (etag.charAt(0) === '"') {
+ return etag.split('"')[1]
+ }
+ return etag
+ },
+
+ /**
+ * Parse Webdav result
+ *
+ * @param {Object} response XML object
+ *
+ * @returns {Array.<FileInfo>} array of file info
+ */
+ _parseFileInfo: function(response) {
+ let path = decodeURIComponent(response.href)
+ if (path.substr(0, this._root.length) === this._root) {
+ path = path.substr(this._root.length)
+ }
+
+ if (path.charAt(path.length - 1) === '/') {
+ path = path.substr(0, path.length - 1)
+ }
+
+ if (response.propStat.length === 0 || response.propStat[0].status !== 'HTTP/1.1 200 OK') {
+ return null
+ }
+
+ const props = response.propStat[0].properties
+
+ const data = {
+ id: props[Client.PROPERTY_INTERNAL_FILEID],
+ path: OC.dirname(path) || '/',
+ name: OC.basename(path),
+ mtime: (new Date(props[Client.PROPERTY_GETLASTMODIFIED])).getTime(),
+ }
+
+ const etagProp = props[Client.PROPERTY_GETETAG]
+ if (!_.isUndefined(etagProp)) {
+ data.etag = this._parseEtag(etagProp)
+ }
+
+ let sizeProp = props[Client.PROPERTY_GETCONTENTLENGTH]
+ if (!_.isUndefined(sizeProp)) {
+ data.size = parseInt(sizeProp, 10)
+ }
+
+ sizeProp = props[Client.PROPERTY_SIZE]
+ if (!_.isUndefined(sizeProp)) {
+ data.size = parseInt(sizeProp, 10)
+ }
+
+ const hasPreviewProp = props['{' + Client.NS_NEXTCLOUD + '}has-preview']
+ if (!_.isUndefined(hasPreviewProp)) {
+ data.hasPreview = hasPreviewProp === 'true'
+ } else {
+ data.hasPreview = true
+ }
+
+ const isEncryptedProp = props['{' + Client.NS_NEXTCLOUD + '}is-encrypted']
+ if (!_.isUndefined(isEncryptedProp)) {
+ data.isEncrypted = isEncryptedProp === '1'
+ } else {
+ data.isEncrypted = false
+ }
+
+ const isFavouritedProp = props['{' + Client.NS_OWNCLOUD + '}favorite']
+ if (!_.isUndefined(isFavouritedProp)) {
+ data.isFavourited = isFavouritedProp === '1'
+ } else {
+ data.isFavourited = false
+ }
+
+ const contentType = props[Client.PROPERTY_GETCONTENTTYPE]
+ if (!_.isUndefined(contentType)) {
+ data.mimetype = contentType
+ }
+
+ const resType = props[Client.PROPERTY_RESOURCETYPE]
+ if (!data.mimetype && resType) {
+ const xmlvalue = resType[0]
+ if (xmlvalue.namespaceURI === Client.NS_DAV && xmlvalue.nodeName.split(':')[1] === 'collection') {
+ data.mimetype = 'httpd/unix-directory'
+ }
+ }
+
+ data.permissions = OC.PERMISSION_NONE
+ const permissionProp = props[Client.PROPERTY_PERMISSIONS]
+ if (!_.isUndefined(permissionProp)) {
+ const permString = permissionProp || ''
+ data.mountType = null
+ for (let i = 0; i < permString.length; i++) {
+ const c = permString.charAt(i)
+ switch (c) {
+ // FIXME: twisted permissions
+ case 'C':
+ case 'K':
+ data.permissions |= OC.PERMISSION_CREATE
+ break
+ case 'G':
+ data.permissions |= OC.PERMISSION_READ
+ break
+ case 'W':
+ case 'N':
+ case 'V':
+ data.permissions |= OC.PERMISSION_UPDATE
+ break
+ case 'D':
+ data.permissions |= OC.PERMISSION_DELETE
+ break
+ case 'R':
+ data.permissions |= OC.PERMISSION_SHARE
+ break
+ case 'M':
+ if (!data.mountType) {
+ // TODO: how to identify external-root ?
+ data.mountType = 'external'
+ }
+ break
+ case 'S':
+ // TODO: how to identify shared-root ?
+ data.mountType = 'shared'
+ break
+ }
+ }
+ }
+
+ const sharePermissionsProp = props[Client.PROPERTY_SHARE_PERMISSIONS]
+ if (!_.isUndefined(sharePermissionsProp)) {
+ data.sharePermissions = parseInt(sharePermissionsProp)
+ }
+
+ const mounTypeProp = props['{' + Client.NS_NEXTCLOUD + '}mount-type']
+ if (!_.isUndefined(mounTypeProp)) {
+ data.mountType = mounTypeProp
+ }
+
+ const quotaAvailableBytes = props['{' + Client.NS_DAV + '}quota-available-bytes']
+ if (!_.isUndefined(quotaAvailableBytes)) {
+ data.quotaAvailableBytes = quotaAvailableBytes
+ }
+
+ // extend the parsed data using the custom parsers
+ _.each(this._fileInfoParsers, function(parserFunction) {
+ _.extend(data, parserFunction(response, data) || {})
+ })
+
+ return new FileInfo(data)
+ },
+
+ /**
+ * Parse Webdav multistatus
+ *
+ * @param {Array} responses
+ */
+ _parseResult: function(responses) {
+ const self = this
+ return _.map(responses, function(response) {
+ return self._parseFileInfo(response)
+ })
+ },
+
+ /**
+ * Returns whether the given status code means success
+ *
+ * @param {int} status status code
+ *
+ * @returns true if status code is between 200 and 299 included
+ */
+ _isSuccessStatus: function(status) {
+ return status >= 200 && status <= 299
+ },
+
+ /**
+ * Parse the Sabre exception out of the given response, if any
+ *
+ * @param {Object} response object
+ * @returns {Object} array of parsed message and exception (only the first one)
+ */
+ _getSabreException: function(response) {
+ const result = {}
+ const xml = response.xhr.responseXML
+ if (xml === null) {
+ return result
+ }
+ const messages = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'message')
+ const exceptions = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'exception')
+ if (messages.length) {
+ result.message = messages[0].textContent
+ }
+ if (exceptions.length) {
+ result.exception = exceptions[0].textContent
+ }
+ return result
+ },
+
+ /**
+ * Returns the default PROPFIND properties to use during a call.
+ *
+ * @returns {Array.<Object>} array of properties
+ */
+ getPropfindProperties: function() {
+ if (!this._propfindProperties) {
+ this._propfindProperties = _.map(Client._PROPFIND_PROPERTIES, function(propDef) {
+ return '{' + propDef[0] + '}' + propDef[1]
+ })
+ }
+ return this._propfindProperties
+ },
+
+ /**
+ * Lists the contents of a directory
+ *
+ * @param {String} path path to retrieve
+ * @param {Object} [options] options
+ * @param {boolean} [options.includeParent=false] set to true to keep
+ * the parent folder in the result list
+ * @param {Array} [options.properties] list of Webdav properties to retrieve
+ *
+ * @returns {Promise} promise
+ */
+ getFolderContents: function(path, options) {
+ if (!path) {
+ path = ''
+ }
+ options = options || {}
+ const self = this
+ const deferred = $.Deferred()
+ const promise = deferred.promise()
+ let properties
+ if (_.isUndefined(options.properties)) {
+ properties = this.getPropfindProperties()
+ } else {
+ properties = options.properties
+ }
+
+ this._client.propFind(
+ this._buildUrl(path),
+ properties,
+ 1
+ ).then(function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ const results = self._parseResult(result.body)
+ if (!options || !options.includeParent) {
+ // remove root dir, the first entry
+ results.shift()
+ }
+ deferred.resolve(result.status, results)
+ } else {
+ result = _.extend(result, self._getSabreException(result))
+ deferred.reject(result.status, result)
+ }
+ })
+ return promise
+ },
+
+ /**
+ * Fetches a flat list of files filtered by a given filter criteria.
+ * (currently system tags and circles are supported)
+ *
+ * @param {Object} filter filter criteria
+ * @param {Object} [filter.systemTagIds] list of system tag ids to filter by
+ * @param {bool} [filter.favorite] set it to filter by favorites
+ * @param {Object} [options] options
+ * @param {Array} [options.properties] list of Webdav properties to retrieve
+ *
+ * @returns {Promise} promise
+ */
+ getFilteredFiles: function(filter, options) {
+ options = options || {}
+ const self = this
+ const deferred = $.Deferred()
+ const promise = deferred.promise()
+ let properties
+ if (_.isUndefined(options.properties)) {
+ properties = this.getPropfindProperties()
+ } else {
+ properties = options.properties
+ }
+
+ if (!filter
+ || (!filter.systemTagIds && _.isUndefined(filter.favorite) && !filter.circlesIds)) {
+ throw 'Missing filter argument'
+ }
+
+ // root element with namespaces
+ let body = '<oc:filter-files '
+ let namespace
+ for (namespace in this._client.xmlNamespaces) {
+ body += ' xmlns:' + this._client.xmlNamespaces[namespace] + '="' + namespace + '"'
+ }
+ body += '>\n'
+
+ // properties query
+ body += ' <' + this._client.xmlNamespaces['DAV:'] + ':prop>\n'
+ _.each(properties, function(prop) {
+ const property = self._client.parseClarkNotation(prop)
+ body += ' <' + self._client.xmlNamespaces[property.namespace] + ':' + property.name + ' />\n'
+ })
+
+ body += ' </' + this._client.xmlNamespaces['DAV:'] + ':prop>\n'
+
+ // rules block
+ body += ' <oc:filter-rules>\n'
+ _.each(filter.systemTagIds, function(systemTagIds) {
+ body += ' <oc:systemtag>' + escapeHTML(systemTagIds) + '</oc:systemtag>\n'
+ })
+ _.each(filter.circlesIds, function(circlesIds) {
+ body += ' <oc:circle>' + escapeHTML(circlesIds) + '</oc:circle>\n'
+ })
+ if (filter.favorite) {
+ body += ' <oc:favorite>' + (filter.favorite ? '1' : '0') + '</oc:favorite>\n'
+ }
+ body += ' </oc:filter-rules>\n'
+
+ // end of root
+ body += '</oc:filter-files>\n'
+
+ this._client.request(
+ 'REPORT',
+ this._buildUrl(),
+ {},
+ body
+ ).then(function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ const results = self._parseResult(result.body)
+ deferred.resolve(result.status, results)
+ } else {
+ result = _.extend(result, self._getSabreException(result))
+ deferred.reject(result.status, result)
+ }
+ })
+ return promise
+ },
+
+ /**
+ * Returns the file info of a given path.
+ *
+ * @param {String} path path
+ * @param {Array} [options.properties] list of Webdav properties to retrieve
+ *
+ * @returns {Promise} promise
+ */
+ getFileInfo: function(path, options) {
+ if (!path) {
+ path = ''
+ }
+ options = options || {}
+ const self = this
+ const deferred = $.Deferred()
+ const promise = deferred.promise()
+ let properties
+ if (_.isUndefined(options.properties)) {
+ properties = this.getPropfindProperties()
+ } else {
+ properties = options.properties
+ }
+
+ // TODO: headers
+ this._client.propFind(
+ this._buildUrl(path),
+ properties,
+ 0
+ ).then(
+ function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ deferred.resolve(result.status, self._parseResult([result.body])[0])
+ } else {
+ result = _.extend(result, self._getSabreException(result))
+ deferred.reject(result.status, result)
+ }
+ }
+ )
+ return promise
+ },
+
+ /**
+ * Returns the contents of the given file.
+ *
+ * @param {String} path path to file
+ *
+ * @returns {Promise}
+ */
+ getFileContents: function(path) {
+ if (!path) {
+ throw 'Missing argument "path"'
+ }
+ const self = this
+ const deferred = $.Deferred()
+ const promise = deferred.promise()
+
+ this._client.request(
+ 'GET',
+ this._buildUrl(path)
+ ).then(
+ function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ deferred.resolve(result.status, result.body)
+ } else {
+ result = _.extend(result, self._getSabreException(result))
+ deferred.reject(result.status, result)
+ }
+ }
+ )
+ return promise
+ },
+
+ /**
+ * Puts the given data into the given file.
+ *
+ * @param {String} path path to file
+ * @param {String} body file body
+ * @param {Object} [options]
+ * @param {String} [options.contentType='text/plain'] content type
+ * @param {bool} [options.overwrite=true] whether to overwrite an existing file
+ *
+ * @returns {Promise}
+ */
+ putFileContents: function(path, body, options) {
+ if (!path) {
+ throw 'Missing argument "path"'
+ }
+ const self = this
+ const deferred = $.Deferred()
+ const promise = deferred.promise()
+ options = options || {}
+ const headers = {}
+ let contentType = 'text/plain;charset=utf-8'
+ if (options.contentType) {
+ contentType = options.contentType
+ }
+
+ headers['Content-Type'] = contentType
+
+ if (_.isUndefined(options.overwrite) || options.overwrite) {
+ // will trigger 412 precondition failed if a file already exists
+ headers['If-None-Match'] = '*'
+ }
+
+ this._client.request(
+ 'PUT',
+ this._buildUrl(path),
+ headers,
+ body || ''
+ ).then(
+ function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ deferred.resolve(result.status)
+ } else {
+ result = _.extend(result, self._getSabreException(result))
+ deferred.reject(result.status, result)
+ }
+ }
+ )
+ return promise
+ },
+
+ _simpleCall: function(method, path) {
+ if (!path) {
+ throw 'Missing argument "path"'
+ }
+
+ const self = this
+ const deferred = $.Deferred()
+ const promise = deferred.promise()
+
+ this._client.request(
+ method,
+ this._buildUrl(path)
+ ).then(
+ function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ deferred.resolve(result.status)
+ } else {
+ result = _.extend(result, self._getSabreException(result))
+ deferred.reject(result.status, result)
+ }
+ }
+ )
+ return promise
+ },
+
+ /**
+ * Creates a directory
+ *
+ * @param {String} path path to create
+ *
+ * @returns {Promise}
+ */
+ createDirectory: function(path) {
+ return this._simpleCall('MKCOL', path)
+ },
+
+ /**
+ * Deletes a file or directory
+ *
+ * @param {String} path path to delete
+ *
+ * @returns {Promise}
+ */
+ remove: function(path) {
+ return this._simpleCall('DELETE', path)
+ },
+
+ /**
+ * Moves path to another path
+ *
+ * @param {String} path path to move
+ * @param {String} destinationPath destination path
+ * @param {boolean} [allowOverwrite=false] true to allow overwriting,
+ * false otherwise
+ * @param {Object} [headers=null] additional headers
+ *
+ * @returns {Promise} promise
+ */
+ move: function(path, destinationPath, allowOverwrite, headers) {
+ if (!path) {
+ throw 'Missing argument "path"'
+ }
+ if (!destinationPath) {
+ throw 'Missing argument "destinationPath"'
+ }
+
+ const self = this
+ const deferred = $.Deferred()
+ const promise = deferred.promise()
+ headers = _.extend({}, headers, {
+ 'Destination': this._buildUrl(destinationPath),
+ })
+
+ if (!allowOverwrite) {
+ headers.Overwrite = 'F'
+ }
+
+ this._client.request(
+ 'MOVE',
+ this._buildUrl(path),
+ headers
+ ).then(
+ function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ deferred.resolve(result.status)
+ } else {
+ result = _.extend(result, self._getSabreException(result))
+ deferred.reject(result.status, result)
+ }
+ }
+ )
+ return promise
+ },
+
+ /**
+ * Copies path to another path
+ *
+ * @param {String} path path to copy
+ * @param {String} destinationPath destination path
+ * @param {boolean} [allowOverwrite=false] true to allow overwriting,
+ * false otherwise
+ *
+ * @returns {Promise} promise
+ */
+ copy: function(path, destinationPath, allowOverwrite) {
+ if (!path) {
+ throw 'Missing argument "path"'
+ }
+ if (!destinationPath) {
+ throw 'Missing argument "destinationPath"'
+ }
+
+ const self = this
+ const deferred = $.Deferred()
+ const promise = deferred.promise()
+ const headers = {
+ 'Destination': this._buildUrl(destinationPath),
+ }
+
+ if (!allowOverwrite) {
+ headers.Overwrite = 'F'
+ }
+
+ this._client.request(
+ 'COPY',
+ this._buildUrl(path),
+ headers
+ ).then(
+ function(response) {
+ if (self._isSuccessStatus(response.status)) {
+ deferred.resolve(response.status)
+ } else {
+ deferred.reject(response.status)
+ }
+ }
+ )
+ return promise
+ },
+
+ /**
+ * Add a file info parser function
+ *
+ * @param {OC.Files.Client~parseFileInfo} parserFunction
+ */
+ addFileInfoParser: function(parserFunction) {
+ this._fileInfoParsers.push(parserFunction)
+ },
+
+ /**
+ * Returns the dav.Client instance used internally
+ *
+ * @since 11.0.0
+ * @returns {dav.Client}
+ */
+ getClient: function() {
+ return this._client
+ },
+
+ /**
+ * Returns the user name
+ *
+ * @since 11.0.0
+ * @returns {String} userName
+ */
+ getUserName: function() {
+ return this._client.userName
+ },
+
+ /**
+ * Returns the password
+ *
+ * @since 11.0.0
+ * @returns {String} password
+ */
+ getPassword: function() {
+ return this._client.password
+ },
+
+ /**
+ * Returns the base URL
+ *
+ * @since 11.0.0
+ * @returns {String} base URL
+ */
+ getBaseUrl: function() {
+ return this._client.baseUrl
+ },
+
+ /**
+ * Returns the host
+ *
+ * @since 13.0.0
+ * @returns {String} base URL
+ */
+ getHost: function() {
+ return this._host
+ },
+ }
+
+ /**
+ * File info parser function
+ *
+ * This function receives a list of Webdav properties as input and
+ * should return a hash array of parsed properties, if applicable.
+ *
+ * @callback OC.Files.Client~parseFileInfo
+ * @param {Object} XML Webdav properties
+ * @return {Array} array of parsed property values
+ */
+
+ if (!OC.Files) {
+ /**
+ * @namespace OC.Files
+ *
+ * @since 8.2
+ */
+ OC.Files = {}
+ }
+
+ /**
+ * Returns the default instance of the files client
+ *
+ * @returns {OC.Files.Client} default client
+ *
+ * @since 8.2
+ */
+ OC.Files.getClient = function() {
+ if (OC.Files._defaultClient) {
+ return OC.Files._defaultClient
+ }
+
+ const client = new OC.Files.Client({
+ host: OC.getHost(),
+ port: OC.getPort(),
+ root: OC.linkToRemoteBase('dav') + '/files/' + OC.getCurrentUser().uid,
+ useHTTPS: OC.getProtocol() === 'https',
+ })
+ OC.Files._defaultClient = client
+ return client
+ }
+
+ OC.Files.Client = Client
+})(OC, OC.Files.FileInfo)
diff --git a/core/src/files/fileinfo.js b/core/src/files/fileinfo.js
new file mode 100644
index 00000000000..936c3c0331b
--- /dev/null
+++ b/core/src/files/fileinfo.js
@@ -0,0 +1,165 @@
+/**
+ * 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 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/>.
+ *
+ */
+
+/* eslint-disable */
+(function(OC) {
+
+ /**
+ * @class OC.Files.FileInfo
+ * @classdesc File information
+ *
+ * @param {Object} data file data, see attributes for details
+ *
+ * @since 8.2
+ */
+ const FileInfo = function(data) {
+ const self = this
+ _.each(data, function(value, key) {
+ if (!_.isFunction(value)) {
+ self[key] = value
+ }
+ })
+
+ if (!_.isUndefined(this.id)) {
+ this.id = parseInt(data.id, 10)
+ }
+
+ // TODO: normalize path
+ this.path = data.path || ''
+
+ if (this.type === 'dir') {
+ this.mimetype = 'httpd/unix-directory'
+ } else {
+ this.mimetype = this.mimetype || 'application/octet-stream'
+ }
+
+ if (!this.type) {
+ if (this.mimetype === 'httpd/unix-directory') {
+ this.type = 'dir'
+ } else {
+ this.type = 'file'
+ }
+ }
+ }
+
+ /**
+ * @memberof OC.Files
+ */
+ FileInfo.prototype = {
+ /**
+ * File id
+ *
+ * @type int
+ */
+ id: null,
+
+ /**
+ * File name
+ *
+ * @type String
+ */
+ name: null,
+
+ /**
+ * Path leading to the file, without the file name,
+ * and with a leading slash.
+ *
+ * @type String
+ */
+ path: null,
+
+ /**
+ * Mime type
+ *
+ * @type String
+ */
+ mimetype: null,
+
+ /**
+ * Icon URL.
+ *
+ * Can be used to override the mime type icon.
+ *
+ * @type String
+ */
+ icon: null,
+
+ /**
+ * File type. 'file' for files, 'dir' for directories.
+ *
+ * @type String
+ * @deprecated rely on mimetype instead
+ */
+ type: null,
+
+ /**
+ * Permissions.
+ *
+ * @see OC#PERMISSION_ALL for permissions
+ * @type int
+ */
+ permissions: null,
+
+ /**
+ * Modification time
+ *
+ * @type int
+ */
+ mtime: null,
+
+ /**
+ * Etag
+ *
+ * @type String
+ */
+ etag: null,
+
+ /**
+ * Mount type.
+ *
+ * One of null, "external-root", "shared" or "shared-root"
+ *
+ * @type string
+ */
+ mountType: null,
+
+ /**
+ * @type boolean
+ */
+ hasPreview: true,
+
+ /**
+ * @type int
+ */
+ sharePermissions: null,
+
+ quotaAvailableBytes: -1,
+ }
+
+ if (!OC.Files) {
+ OC.Files = {}
+ }
+ OC.Files.FileInfo = FileInfo
+})(OC)
diff --git a/core/src/files/iedavclient.js b/core/src/files/iedavclient.js
new file mode 100644
index 00000000000..470e34009ab
--- /dev/null
+++ b/core/src/files/iedavclient.js
@@ -0,0 +1,173 @@
+/**
+ * Copyright (c) 2015
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @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/>.
+ *
+ */
+
+/* eslint-disable */
+(function(dav) {
+
+ /**
+ * Override davclient.js methods with IE-compatible logic
+ */
+ dav.Client.prototype = _.extend({}, dav.Client.prototype, {
+
+ /**
+ * Performs a HTTP request, and returns a Promise
+ *
+ * @param {string} method HTTP method
+ * @param {string} url Relative or absolute url
+ * @param {Object} headers HTTP headers as an object.
+ * @param {string} body HTTP request body.
+ * @returns {Promise}
+ */
+ request: function(method, url, headers, body) {
+
+ const self = this
+ const xhr = this.xhrProvider()
+ headers = headers || {}
+
+ if (this.userName) {
+ headers.Authorization = 'Basic ' + btoa(this.userName + ':' + this.password)
+ // xhr.open(method, this.resolveUrl(url), true, this.userName, this.password);
+ }
+ xhr.open(method, this.resolveUrl(url), true)
+ let ii
+ for (ii in headers) {
+ xhr.setRequestHeader(ii, headers[ii])
+ }
+
+ if (body === undefined) {
+ xhr.send()
+ } else {
+ xhr.send(body)
+ }
+
+ return new Promise(function(fulfill, reject) {
+
+ xhr.onreadystatechange = function() {
+
+ if (xhr.readyState !== 4) {
+ return
+ }
+
+ let resultBody = xhr.response
+ if (xhr.status === 207) {
+ resultBody = self.parseMultiStatus(xhr.responseXML)
+ }
+
+ fulfill({
+ body: resultBody,
+ status: xhr.status,
+ xhr: xhr,
+ })
+
+ }
+
+ xhr.ontimeout = function() {
+
+ reject(new Error('Timeout exceeded'))
+
+ }
+
+ })
+
+ },
+
+ _getElementsByTagName: function(node, name, resolver) {
+ const parts = name.split(':')
+ const tagName = parts[1]
+ const namespace = resolver(parts[0])
+ // make sure we can get elements
+ if (typeof node === 'string') {
+ const parser = new DOMParser()
+ node = parser.parseFromString(node, 'text/xml')
+ }
+ if (node.getElementsByTagNameNS) {
+ return node.getElementsByTagNameNS(namespace, tagName)
+ }
+ return node.getElementsByTagName(name)
+ },
+
+ /**
+ * Parses a multi-status response body.
+ *
+ * @param {string} xmlBody
+ * @param {Array}
+ */
+ parseMultiStatus: function(doc) {
+ const result = []
+ const resolver = function(foo) {
+ let ii
+ for (ii in this.xmlNamespaces) {
+ if (this.xmlNamespaces[ii] === foo) {
+ return ii
+ }
+ }
+ }.bind(this)
+
+ const responses = this._getElementsByTagName(doc, 'd:response', resolver)
+ let i
+ for (i = 0; i < responses.length; i++) {
+ const responseNode = responses[i]
+ const response = {
+ href: null,
+ propStat: [],
+ }
+
+ const hrefNode = this._getElementsByTagName(responseNode, 'd:href', resolver)[0]
+
+ response.href = hrefNode.textContent || hrefNode.text
+
+ const propStatNodes = this._getElementsByTagName(responseNode, 'd:propstat', resolver)
+ let j = 0
+
+ for (j = 0; j < propStatNodes.length; j++) {
+ const propStatNode = propStatNodes[j]
+ const statusNode = this._getElementsByTagName(propStatNode, 'd:status', resolver)[0]
+
+ const propStat = {
+ status: statusNode.textContent || statusNode.text,
+ properties: [],
+ }
+
+ const propNode = this._getElementsByTagName(propStatNode, 'd:prop', resolver)[0]
+ if (!propNode) {
+ continue
+ }
+ let k = 0
+ for (k = 0; k < propNode.childNodes.length; k++) {
+ const prop = propNode.childNodes[k]
+ const value = this._parsePropNode(prop)
+ propStat.properties['{' + prop.namespaceURI + '}' + (prop.localName || prop.baseName)] = value
+
+ }
+ response.propStat.push(propStat)
+ }
+
+ result.push(response)
+ }
+
+ return result
+
+ },
+
+ })
+
+})(dav)
diff --git a/core/src/globals.js b/core/src/globals.js
index e7f2b075374..7c5ccd77e25 100644
--- a/core/src/globals.js
+++ b/core/src/globals.js
@@ -1,8 +1,10 @@
-/* eslint-disable @nextcloud/no-deprecations */
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -17,9 +19,11 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
+/* eslint-disable @nextcloud/no-deprecations */
import { initCore } from './init'
import _ from 'underscore'
@@ -36,12 +40,10 @@ import Backbone from 'backbone'
import 'bootstrap/js/dist/tooltip'
import './Polyfill/tooltip'
import ClipboardJS from 'clipboard'
-import dav from 'davclient.js'
-import DOMPurify from 'dompurify'
+import { dav } from 'davclient.js'
import Handlebars from 'handlebars'
import 'jcrop/js/jquery.Jcrop'
import 'jcrop/css/jquery.Jcrop.css'
-import jstimezonedetect from 'jstimezonedetect'
import md5 from 'blueimp-md5'
import moment from 'moment'
import 'select2'
@@ -53,7 +55,6 @@ import 'strengthify/strengthify.css'
import OC from './OC/index'
import OCP from './OCP/index'
import OCA from './OCA/index'
-import escapeHTML from 'escape-html'
import { getToken as getRequestToken } from './OC/requesttoken'
const warnIfNotTesting = function() {
@@ -100,22 +101,19 @@ const setDeprecatedProp = (global, cb, msg) => {
})
}
-window['_'] = _
-setDeprecatedProp(['$', 'jQuery'], () => $, 'The global jQuery is deprecated. It will be updated to v2.4 in Nextcloud 20 and v3.x in Nextcloud 21. In later versions of Nextcloud it might be removed completely. Please ship your own.')
+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('DOMPurify', () => DOMPurify, 'The global DOMPurify is deprecated, this will be removed in Nextcloud 21')
+window.dav = dav
setDeprecatedProp('Handlebars', () => Handlebars, 'please ship your own, this will be removed in Nextcloud 20')
-setDeprecatedProp(['jstz', 'jstimezonedetect'], () => jstimezonedetect, 'please ship your own, this will be removed in Nextcloud 20')
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')
-window['OC'] = OC
+window.OC = OC
setDeprecatedProp('initCore', () => initCore, 'this is an internal function')
setDeprecatedProp('oc_appswebroots', () => OC.appswebroots, 'use OC.appswebroots instead, this will be removed in Nextcloud 20')
-setDeprecatedProp('oc_capabilities', OC.getCapabilities, 'use OC.getCapabilities instead, this will be removed in Nextcloud 20')
setDeprecatedProp('oc_config', () => OC.config, 'use OC.config instead, this will be removed in Nextcloud 20')
setDeprecatedProp('oc_current_user', () => OC.getCurrentUser().uid, 'use OC.getCurrentUser().uid instead, this will be removed in Nextcloud 20')
setDeprecatedProp('oc_debug', () => OC.debug, 'use OC.debug instead, this will be removed in Nextcloud 20')
@@ -124,9 +122,8 @@ setDeprecatedProp('oc_isadmin', OC.isUserAdmin, 'use OC.isUserAdmin() instead, t
setDeprecatedProp('oc_requesttoken', () => getRequestToken(), 'use OC.requestToken instead, this will be removed in Nextcloud 20')
setDeprecatedProp('oc_webroot', () => OC.webroot, 'use OC.getRootPath() instead, this will be removed in Nextcloud 20')
setDeprecatedProp('OCDialogs', () => OC.dialogs, 'use OC.dialogs instead, this will be removed in Nextcloud 20')
-window['OCP'] = OCP
-window['OCA'] = OCA
-window['escapeHTML'] = deprecate(escapeHTML, 'escapeHTML', 19)
+window.OCP = OCP
+window.OCA = OCA
$.fn.select2 = deprecate($.fn.select2, 'select2', 19)
/**
diff --git a/core/src/init.js b/core/src/init.js
index f0102fdde40..f1c3eaa17c1 100644
--- a/core/src/init.js
+++ b/core/src/init.js
@@ -1,8 +1,11 @@
-/* globals Snap */
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -17,9 +20,11 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
+/* globals Snap */
import _ from 'underscore'
import $ from 'jquery'
import moment from 'moment'
@@ -39,8 +44,8 @@ const resizeMenu = () => {
const appList = $('#appmenu li')
const rightHeaderWidth = $('.header-right').outerWidth()
const headerWidth = $('header').outerWidth()
- const usePercentualAppMenuLimit = 0.33
- const minAppsDesktop = 8
+ const usePercentualAppMenuLimit = 0.67
+ const minAppsDesktop = 12
let availableWidth = headerWidth - $('#nextcloud').outerWidth() - (rightHeaderWidth > 210 ? rightHeaderWidth : 210)
const isMobile = $(window).width() < breakpointMobileWidth
if (!isMobile) {
@@ -99,14 +104,14 @@ const initLiveTimestamps = () => {
}
/**
+ * Set users locale to moment.js as soon as possible
+ */
+moment.locale(OC.getLocale())
+
+/**
* Initializes core
*/
export const initCore = () => {
- /**
- * Set users locale to moment.js as soon as possible
- */
- moment.locale(OC.getLocale())
-
const userAgent = window.navigator.userAgent
const msie = userAgent.indexOf('MSIE ')
const trident = userAgent.indexOf('Trident/')
@@ -217,18 +222,74 @@ export const initCore = () => {
$('#app-content').prepend('<div id="app-navigation-toggle" class="icon-menu" style="display:none" tabindex="0"></div>')
- const toggleSnapperOnButton = () => {
- if (snapper.state().state === 'left') {
- snapper.close()
- } else {
- snapper.open('left')
+ // keep track whether snapper is currently animating, and
+ // prevent to call open or close while that is the case
+ // to avoid duplicating events (snap.js doesn't check this)
+ let animating = false
+ snapper.on('animating', () => {
+ // we need this because the trigger button
+ // is also implicitly wired to close by snapper
+ animating = true
+ })
+ snapper.on('animated', () => {
+ animating = false
+ })
+ snapper.on('start', () => {
+ // we need this because dragging triggers that
+ animating = true
+ })
+ snapper.on('end', () => {
+ // we need this because dragging stop triggers that
+ animating = false
+ })
+
+ // These are necessary because calling open or close
+ // on snapper during an animation makes it trigger an
+ // unfinishable animation, which itself will continue
+ // triggering animating events and cause high CPU load,
+ //
+ // Ref https://github.com/jakiestfu/Snap.js/issues/216
+ const oldSnapperOpen = snapper.open
+ const oldSnapperClose = snapper.close
+ const _snapperOpen = () => {
+ if (animating || snapper.state().state !== 'closed') {
+ return
+ }
+ oldSnapperOpen('left')
+ }
+
+ const _snapperClose = () => {
+ if (animating || snapper.state().state === 'closed') {
+ return
+ }
+ oldSnapperClose()
+ }
+
+ // Needs to be deferred to properly catch in-between
+ // events that snap.js is triggering after dragging.
+ //
+ // Skipped when running unit tests as we are not testing
+ // the snap.js workarounds...
+ if (!window.TESTING) {
+ snapper.open = () => {
+ _.defer(_snapperOpen)
+ }
+ snapper.close = () => {
+ _.defer(_snapperClose)
}
}
- $('#app-navigation-toggle').click(toggleSnapperOnButton)
+ $('#app-navigation-toggle').click((e) => {
+ // close is implicit in the button by snap.js
+ if (snapper.state().state !== 'left') {
+ snapper.open()
+ }
+ })
$('#app-navigation-toggle').keypress(e => {
- if (e.which === 13) {
- toggleSnapperOnButton()
+ if (snapper.state().state === 'left') {
+ snapper.close()
+ } else {
+ snapper.open()
}
})
diff --git a/core/src/install.js b/core/src/install.js
index 43957acb9be..a8d3ed7afc0 100644
--- a/core/src/install.js
+++ b/core/src/install.js
@@ -1,16 +1,45 @@
+/**
+ * @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 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/>.
+ *
+ */
+
import $ from 'jquery'
import { translate as t } from '@nextcloud/l10n'
import { getToken } from './OC/requesttoken'
import getURLParameter from './Util/get-url-parameter'
+import './jquery/showpassword'
+
import 'jquery-ui/ui/widgets/button'
import 'jquery-ui/themes/base/theme.css'
import 'jquery-ui/themes/base/button.css'
+import 'bootstrap/js/dist/tooltip'
+import './Polyfill/tooltip'
+
import 'strengthify'
import 'strengthify/strengthify.css'
-$(document).ready(function() {
+window.addEventListener('DOMContentLoaded', function() {
const dbtypes = {
sqlite: !!$('#hasSQLite').val(),
mysql: !!$('#hasMySQL').val(),
@@ -134,4 +163,7 @@ $(document).ready(function() {
drawTitles: true,
nonce: btoa(getToken()),
})
+
+ $('#dbpass').showPassword().keyup()
+ $('#adminpass').showPassword().keyup()
})
diff --git a/core/src/jquery/avatar.js b/core/src/jquery/avatar.js
index 24cef392d34..3849af3b359 100644
--- a/core/src/jquery/avatar.js
+++ b/core/src/jquery/avatar.js
@@ -1,7 +1,8 @@
-/*
+/**
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
@@ -110,7 +112,7 @@ $.fn.avatar = function(user, size, ie8fix, hidedefault, callback, displayname) {
url = OC.generateUrl(
'/avatar/{user}/{size}?v={version}',
{
- user: user,
+ user,
size: Math.ceil(size * window.devicePixelRatio),
version: oc_userconfig.avatar.version,
})
@@ -118,7 +120,7 @@ $.fn.avatar = function(user, size, ie8fix, hidedefault, callback, displayname) {
url = OC.generateUrl(
'/avatar/{user}/{size}',
{
- user: user,
+ user,
size: Math.ceil(size * window.devicePixelRatio),
})
}
diff --git a/core/src/jquery/contactsmenu.js b/core/src/jquery/contactsmenu.js
index ee708dcf982..ed05d4602a7 100644
--- a/core/src/jquery/contactsmenu.js
+++ b/core/src/jquery/contactsmenu.js
@@ -1,7 +1,9 @@
-/*
+/**
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +18,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
@@ -65,8 +68,8 @@ $.fn.contactsMenu = function(shareWith, shareType, appendTo) {
$.ajax(OC.generateUrl('/contactsmenu/findOne'), {
method: 'POST',
data: {
- shareType: shareType,
- shareWith: shareWith,
+ shareType,
+ shareWith,
},
}).then(function(data) {
$list.find('ul').find('li').addClass('hidden')
@@ -82,9 +85,10 @@ $.fn.contactsMenu = function(shareWith, shareType, appendTo) {
}
actions.forEach(function(action) {
- const template = entryTemplate
- $list.find('ul').append(template(action))
+ $list.find('ul').append(entryTemplate(action))
})
+
+ $div.trigger('load')
}, function(jqXHR) {
$list.find('ul').find('li').addClass('hidden')
@@ -95,11 +99,12 @@ $.fn.contactsMenu = function(shareWith, shareType, appendTo) {
title = t('core', 'Error fetching contact actions')
}
- const template = entryTemplate
- $list.find('ul').append(template({
+ $list.find('ul').append(entryTemplate({
hyperlink: '#',
- title: title,
+ title,
}))
+
+ $div.trigger('loaderror', jqXHR)
})
})
diff --git a/core/src/jquery/css/images/ui-icons_1d2d44_256x240.png b/core/src/jquery/css/images/ui-icons_1d2d44_256x240.png
index 1b1474b1fdf..c6435ee6a66 100644
--- a/core/src/jquery/css/images/ui-icons_1d2d44_256x240.png
+++ b/core/src/jquery/css/images/ui-icons_1d2d44_256x240.png
Binary files differ
diff --git a/core/src/jquery/css/images/ui-icons_ffd27a_256x240.png b/core/src/jquery/css/images/ui-icons_ffd27a_256x240.png
index a7ac4ec6580..f3c947bef5d 100644
--- a/core/src/jquery/css/images/ui-icons_ffd27a_256x240.png
+++ b/core/src/jquery/css/images/ui-icons_ffd27a_256x240.png
Binary files differ
diff --git a/core/src/jquery/css/images/ui-icons_ffffff_256x240.png b/core/src/jquery/css/images/ui-icons_ffffff_256x240.png
index 174be7c2847..5fb299e36a0 100644
--- a/core/src/jquery/css/images/ui-icons_ffffff_256x240.png
+++ b/core/src/jquery/css/images/ui-icons_ffffff_256x240.png
Binary files differ
diff --git a/core/src/jquery/css/jquery-ui-fixes.scss b/core/src/jquery/css/jquery-ui-fixes.scss
index 34243062ba6..42c684ad510 100644
--- a/core/src/jquery/css/jquery-ui-fixes.scss
+++ b/core/src/jquery/css/jquery-ui-fixes.scss
@@ -188,7 +188,7 @@
padding: 4px 4px 4px 14px;
&.ui-state-focus, &.ui-state-active {
- box-shadow: inset 4px 0 var(--color-primary);
+ box-shadow: inset 4px 0 var(--color-primary-element);
color: var(--color-main-text);
}
}
diff --git a/core/src/jquery/css/jquery.ocdialog.scss b/core/src/jquery/css/jquery.ocdialog.scss
index 89653ae181a..b7e762f4cb6 100644
--- a/core/src/jquery/css/jquery.ocdialog.scss
+++ b/core/src/jquery/css/jquery.ocdialog.scss
@@ -42,7 +42,9 @@
button {
white-space: nowrap;
overflow: hidden;
- text-overflow: ellipsis;
+ text-overflow: ellipsis;
+ height: 44px;
+ min-width: 44px;
}
}
@@ -72,6 +74,10 @@
height: 100%;
}
+body.theme--dark .oc-dialog-dim {
+ opacity: .8;
+}
+
.oc-dialog-content {
width: 100%;
max-width: 550px;
diff --git a/core/src/jquery/exists.js b/core/src/jquery/exists.js
index 3481faeb6cd..22fed55badb 100644
--- a/core/src/jquery/exists.js
+++ b/core/src/jquery/exists.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
diff --git a/core/src/jquery/filterattr.js b/core/src/jquery/filterattr.js
index 204d04b46a0..2bd4af417ac 100644
--- a/core/src/jquery/filterattr.js
+++ b/core/src/jquery/filterattr.js
@@ -1,7 +1,8 @@
/**
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
diff --git a/core/src/jquery/index.js b/core/src/jquery/index.js
index c0145cb948f..c8ab1a7f673 100644
--- a/core/src/jquery/index.js
+++ b/core/src/jquery/index.js
@@ -1,7 +1,9 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +18,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
@@ -31,7 +34,6 @@ import './placeholder'
import './requesttoken'
import './selectrange'
import './showpassword'
-import './tipsy'
import './ui-fixes'
import './css/jquery-ui-fixes.scss'
diff --git a/core/src/jquery/ocdialog.js b/core/src/jquery/ocdialog.js
index 518a28f75d3..14abd5aa666 100644
--- a/core/src/jquery/ocdialog.js
+++ b/core/src/jquery/ocdialog.js
@@ -1,7 +1,10 @@
-/*
+/**
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +19,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
@@ -27,9 +31,10 @@ $.widget('oc.ocdialog', {
height: 'auto',
closeButton: true,
closeOnEscape: true,
+ closeCallback: null,
modal: false,
},
- _create: function() {
+ _create() {
const self = this
this.originalCss = {
@@ -106,11 +111,11 @@ $.widget('oc.ocdialog', {
this._setOptions(this.options)
this._createOverlay()
},
- _init: function() {
+ _init() {
this.$dialog.focus()
this._trigger('open')
},
- _setOption: function(key, value) {
+ _setOption(key, value) {
const self = this
switch (key) {
case 'title':
@@ -169,6 +174,7 @@ $.widget('oc.ocdialog', {
const $closeButton = $('<a class="oc-dialog-close"></a>')
this.$dialog.prepend($closeButton)
$closeButton.on('click', function() {
+ self.options.closeCallback && self.options.closeCallback()
self.close()
})
} else {
@@ -188,11 +194,11 @@ $.widget('oc.ocdialog', {
// this._super(key, value);
$.Widget.prototype._setOption.apply(this, arguments)
},
- _setOptions: function(options) {
+ _setOptions(options) {
// this._super(options);
$.Widget.prototype._setOptions.apply(this, arguments)
},
- _setSizes: function() {
+ _setSizes() {
let lessHeight = 0
if (this.$title) {
lessHeight += this.$title.outerHeight(true)
@@ -201,18 +207,23 @@ $.widget('oc.ocdialog', {
lessHeight += this.$buttonrow.outerHeight(true)
}
this.element.css({
- 'height': 'calc(100% - ' + lessHeight + 'px)',
+ height: 'calc(100% - ' + lessHeight + 'px)',
})
},
- _createOverlay: function() {
+ _createOverlay() {
if (!this.options.modal) {
return
}
const self = this
+ let contentDiv = $('#content')
+ if (contentDiv.length === 0) {
+ // nextcloud-vue compatibility
+ contentDiv = $('.content')
+ }
this.overlay = $('<div>')
.addClass('oc-dialog-dim')
- .appendTo($('#content'))
+ .appendTo(contentDiv)
this.overlay.on('click keydown keyup', function(event) {
if (event.target !== self.$dialog.get(0) && self.$dialog.find($(event.target)).length === 0) {
event.preventDefault()
@@ -221,7 +232,7 @@ $.widget('oc.ocdialog', {
}
})
},
- _destroyOverlay: function() {
+ _destroyOverlay() {
if (!this.options.modal) {
return
}
@@ -232,16 +243,16 @@ $.widget('oc.ocdialog', {
this.overlay = null
}
},
- widget: function() {
+ widget() {
return this.$dialog
},
- setEnterCallback: function(callback) {
+ setEnterCallback(callback) {
this.enterCallback = callback
},
- unsetEnterCallback: function() {
+ unsetEnterCallback() {
this.enterCallback = null
},
- close: function() {
+ close() {
this._destroyOverlay()
const self = this
// Ugly hack to catch remaining keyup events.
@@ -252,7 +263,7 @@ $.widget('oc.ocdialog', {
self.$dialog.remove()
this.destroy()
},
- destroy: function() {
+ destroy() {
if (this.$title) {
this.$title.remove()
}
diff --git a/core/src/jquery/octemplate.js b/core/src/jquery/octemplate.js
index 27b2dff970b..9e060881e21 100644
--- a/core/src/jquery/octemplate.js
+++ b/core/src/jquery/octemplate.js
@@ -1,3 +1,27 @@
+/**
+ * @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 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/>.
+ *
+ */
+
import $ from 'jquery'
import escapeHTML from 'escape-html'
@@ -56,7 +80,7 @@ import escapeHTML from 'escape-html'
* Inspired by micro templating done by e.g. underscore.js
*/
const Template = {
- init: function(vars, options, elem) {
+ init(vars, options, elem) {
// Mix in the passed in options with the default options
this.vars = vars
this.options = $.extend({}, this.options, options)
@@ -77,7 +101,7 @@ const Template = {
return $(_html)
},
// From stackoverflow.com/questions/1408289/best-way-to-do-variable-interpolation-in-javascript
- _build: function(o) {
+ _build(o) {
const data = this.elem.attr('type') === 'text/template' ? this.elem.html() : this.elem.get(0).outerHTML
try {
return data.replace(/{([^{}]*)}/g,
diff --git a/core/src/jquery/placeholder.js b/core/src/jquery/placeholder.js
index 029071c4d99..001e71f102c 100644
--- a/core/src/jquery/placeholder.js
+++ b/core/src/jquery/placeholder.js
@@ -1,28 +1,32 @@
-/* eslint-disable */
/**
- * ownCloud
- *
- * @author John Molakvoæ
* @copyright 2016-2018 John Molakvoæ <skjnldsv@protonmail.com>
- * @author Morris Jobke
* @copyright 2013 Morris Jobke <morris.jobke@gmail.com>
*
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
+ * @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 GNU AGPL version 3 or any later version
*
- * This library is distributed in the hope that it will be useful,
+ * 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.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ * 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/>.
*
*/
+/* eslint-disable */
import $ from 'jquery'
+import md5 from 'blueimp-md5'
/*
* Adds a background color to the element called on and adds the first character
@@ -62,9 +66,9 @@ import $ from 'jquery'
*
*/
-String.prototype.toRgb = function() {
+const toRgb = (s) => {
// Normalize hash
- var hash = this.toLowerCase()
+ var hash = s.toLowerCase()
// Already a md5 hash?
if (hash.match(/^([0-9a-f]{4}-?){8}$/) === null) {
@@ -88,8 +92,7 @@ String.prototype.toRgb = function() {
}
function mixPalette(steps, color1, color2) {
- var count = steps + 1
- var palette = new Array()
+ var palette = []
palette.push(color1)
var step = stepCalc(steps, [color1, color2])
for (var i = 1; i < steps; i++) {
@@ -101,23 +104,23 @@ String.prototype.toRgb = function() {
return palette
}
- var red = new Color(182, 70, 157)
- var yellow = new Color(221, 203, 85)
- var blue = new Color(0, 130, 201) // Nextcloud blue
+ const red = new Color(182, 70, 157);
+ const yellow = new Color(221, 203, 85);
+ const blue = new Color(0, 130, 201); // Nextcloud blue
// Number of steps to go from a color to another
// 3 colors * 6 will result in 18 generated colors
- var steps = 6
+ const steps = 6;
- var palette1 = mixPalette(steps, red, yellow)
- var palette2 = mixPalette(steps, yellow, blue)
- var palette3 = mixPalette(steps, blue, red)
+ const palette1 = mixPalette(steps, red, yellow);
+ const palette2 = mixPalette(steps, yellow, blue);
+ const palette3 = mixPalette(steps, blue, red);
- var finalPalette = palette1.concat(palette2).concat(palette3)
+ const finalPalette = palette1.concat(palette2).concat(palette3);
// Convert a string to an integer evenly
function hashToInt(hash, maximum) {
var finalInt = 0
- var result = Array()
+ var result = []
// Splitting evenly the string
for (var i = 0; i < hash.length; i++) {
@@ -136,11 +139,17 @@ String.prototype.toRgb = function() {
return finalPalette[hashToInt(hash, steps * 3)]
}
+String.prototype.toRgb = function() {
+ console.warn('String.prototype.toRgb is deprecated! It will be removed in Nextcloud 22.')
+
+ return toRgb(this)
+}
+
$.fn.imageplaceholder = function(seed, text, size) {
text = text || seed
// Compute the hash
- var rgb = seed.toRgb()
+ var rgb = toRgb(seed)
this.css('background-color', 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')')
// Placeholders are square
@@ -158,7 +167,8 @@ $.fn.imageplaceholder = function(seed, text, size) {
this.css('font-size', (height * 0.55) + 'px')
if (seed !== null && seed.length) {
- this.html(text[0].toUpperCase())
+ var placeholderText = text.replace(/\s+/g, ' ').trim().split(' ', 2).map((word) => word[0].toUpperCase()).join('')
+ this.html(placeholderText);
}
}
diff --git a/core/src/jquery/requesttoken.js b/core/src/jquery/requesttoken.js
index 5f15d43aa17..65d4ac01a48 100644
--- a/core/src/jquery/requesttoken.js
+++ b/core/src/jquery/requesttoken.js
@@ -1,7 +1,8 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
diff --git a/core/src/jquery/selectrange.js b/core/src/jquery/selectrange.js
index 3076726024a..914832d4538 100644
--- a/core/src/jquery/selectrange.js
+++ b/core/src/jquery/selectrange.js
@@ -1,7 +1,8 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
diff --git a/core/src/jquery/showpassword.js b/core/src/jquery/showpassword.js
index 393ecac08b5..3423ea5fc17 100644
--- a/core/src/jquery/showpassword.js
+++ b/core/src/jquery/showpassword.js
@@ -1,7 +1,8 @@
-/*
+/**
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +17,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
@@ -36,10 +38,10 @@ import $ from 'jquery'
* @licens MIT License - http://www.opensource.org/licenses/mit-license.php
*/
$.fn.extend({
- showPassword: function(c) {
+ showPassword(c) {
// Setup callback object
- const callback = { 'fn': null, 'args': {} }
+ const callback = { fn: null, args: {} }
callback.fn = c
// Clones passwords and turn the clones into text inputs
@@ -52,13 +54,13 @@ $.fn.extend({
// Name added for JQuery Validation compatibility
// Element name is required to avoid script warning.
$clone.attr({
- 'type': 'text',
- 'class': $element.attr('class'),
- 'style': $element.attr('style'),
- 'size': $element.attr('size'),
- 'name': $element.attr('name') + '-clone',
- 'tabindex': $element.attr('tabindex'),
- 'autocomplete': 'off',
+ type: 'text',
+ class: $element.attr('class'),
+ style: $element.attr('style'),
+ size: $element.attr('size'),
+ name: $element.attr('name') + '-clone',
+ tabindex: $element.attr('tabindex'),
+ autocomplete: 'off',
})
if ($element.attr('placeholder') !== undefined) {
diff --git a/core/src/jquery/tipsy.js b/core/src/jquery/tipsy.js
deleted file mode 100644
index 0ae6ec83af0..00000000000
--- a/core/src/jquery/tipsy.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * @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/>.
- */
-
-import $ from 'jquery'
-
-/**
- * $ tipsy shim for the bootstrap tooltip
- * @param {Object} argument options
- * @returns {Object} this
- * @deprecated
- */
-$.fn.tipsy = function(argument) {
- console.warn('Deprecation warning: tipsy is deprecated. Use tooltip instead.')
- if (typeof argument === 'object' && argument !== null) {
-
- // tipsy defaults
- const options = {
- placement: 'bottom',
- delay: { 'show': 0, 'hide': 0 },
- trigger: 'hover',
- html: false,
- container: 'body',
- }
- if (argument.gravity) {
- switch (argument.gravity) {
- case 'n':
- case 'nw':
- case 'ne':
- options.placement = 'bottom'
- break
- case 's':
- case 'sw':
- case 'se':
- options.placement = 'top'
- break
- case 'w':
- options.placement = 'right'
- break
- case 'e':
- options.placement = 'left'
- break
- }
- }
- if (argument.trigger) {
- options.trigger = argument.trigger
- }
- if (argument.delayIn) {
- options.delay.show = argument.delayIn
- }
- if (argument.delayOut) {
- options.delay.hide = argument.delayOut
- }
- if (argument.html) {
- options.html = true
- }
- if (argument.fallback) {
- options.title = argument.fallback
- }
- // destroy old tooltip in case the title has changed
- $.fn.tooltip.call(this, 'destroy')
- $.fn.tooltip.call(this, options)
- } else {
- this.tooltip(argument)
- $.fn.tooltip.call(this, argument)
- }
- return this
-}
diff --git a/core/src/jquery/ui-fixes.js b/core/src/jquery/ui-fixes.js
index d70c5579f94..39da7084e44 100644
--- a/core/src/jquery/ui-fixes.js
+++ b/core/src/jquery/ui-fixes.js
@@ -1,3 +1,26 @@
+/**
+ * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @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/>.
+ *
+ */
+
import $ from 'jquery'
// Set autocomplete width the same as the related input
diff --git a/core/src/logger.js b/core/src/logger.js
index 4a9c8623a7f..edf2e35e2b7 100644
--- a/core/src/logger.js
+++ b/core/src/logger.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +16,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import { getCurrentUser } from '@nextcloud/auth'
diff --git a/core/src/login.js b/core/src/login.js
index 7270442c83e..9a2507e4798 100644
--- a/core/src/login.js
+++ b/core/src/login.js
@@ -1,7 +1,10 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +19,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import { loadState } from '@nextcloud/initial-state'
@@ -64,5 +68,11 @@ new View({
resetPasswordTarget: fromStateOr('resetPasswordTarget', ''),
resetPasswordUser: fromStateOr('resetPasswordUser', ''),
directLogin: query.direct === '1',
+ hasPasswordless: fromStateOr('webauthn-available', false),
+ countAlternativeLogins: fromStateOr('countAlternativeLogins', false),
+ isHttps: window.location.protocol === 'https:',
+ isLocalhost: window.location.hostname === 'localhost',
+ hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined',
+ hideLoginForm: fromStateOr('hideLoginForm', false),
},
}).$mount('#login')
diff --git a/core/src/main.js b/core/src/main.js
index 21213d7279e..25316952866 100644
--- a/core/src/main.js
+++ b/core/src/main.js
@@ -1,7 +1,10 @@
/**
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,7 +19,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import $ from 'jquery'
@@ -33,7 +37,7 @@ import './jquery/index'
import { initCore } from './init'
import { registerAppsSlideToggle } from './OC/apps'
-$(document).ready(function() {
+window.addEventListener('DOMContentLoaded', function() {
initCore()
registerAppsSlideToggle()
diff --git a/core/src/maintenance.js b/core/src/maintenance.js
index 70b51f7cebc..b49fcd8d83c 100644
--- a/core/src/maintenance.js
+++ b/core/src/maintenance.js
@@ -1,7 +1,9 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,14 +18,14 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import Axios from '@nextcloud/axios'
+import { getRootUrl } from '@nextcloud/router'
-import OC from './OC/index'
-
-const url = `${OC.getRootPath()}/status.php`
+const url = getRootUrl() + '/status.php'
const check = () => {
console.info('checking the Nextcloud maintenance status')
diff --git a/core/src/mixins/Nextcloud.js b/core/src/mixins/Nextcloud.js
index 3ca755b3052..d1307f90e8e 100644
--- a/core/src/mixins/Nextcloud.js
+++ b/core/src/mixins/Nextcloud.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +16,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import L10n from '../OC/l10n'
diff --git a/core/src/recommendedapps.js b/core/src/recommendedapps.js
index aea105a8422..44c424989be 100644
--- a/core/src/recommendedapps.js
+++ b/core/src/recommendedapps.js
@@ -1,7 +1,7 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -16,7 +16,8 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
import { getRequestToken } from '@nextcloud/auth'
diff --git a/core/src/services/UnifiedSearchService.js b/core/src/services/UnifiedSearchService.js
new file mode 100644
index 00000000000..09d0d02fedf
--- /dev/null
+++ b/core/src/services/UnifiedSearchService.js
@@ -0,0 +1,93 @@
+/**
+ * @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 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/>.
+ *
+ */
+
+import { generateOcsUrl } from '@nextcloud/router'
+import { loadState } from '@nextcloud/initial-state'
+import axios from '@nextcloud/axios'
+
+export const defaultLimit = loadState('unified-search', 'limit-default')
+export const minSearchLength = 2
+export const regexFilterIn = /[^-]in:([a-z_-]+)/ig
+export const regexFilterNot = /-in:([a-z_-]+)/ig
+
+/**
+ * Create a cancel token
+ * @returns {CancelTokenSource}
+ */
+const createCancelToken = () => axios.CancelToken.source()
+
+/**
+ * Get the list of available search providers
+ *
+ * @returns {Array}
+ */
+export async function getTypes() {
+ try {
+ const { data } = await axios.get(generateOcsUrl('search', 2) + 'providers', {
+ params: {
+ // Sending which location we're currently at
+ from: window.location.pathname.replace('/index.php', '') + window.location.search,
+ },
+ })
+ if ('ocs' in data && 'data' in data.ocs && Array.isArray(data.ocs.data) && data.ocs.data.length > 0) {
+ // Providers are sorted by the api based on their order key
+ return data.ocs.data
+ }
+ } catch (error) {
+ console.error(error)
+ }
+ return []
+}
+
+/**
+ * Get the list of available search providers
+ *
+ * @param {Object} options destructuring object
+ * @param {string} options.type the type to search
+ * @param {string} options.query the search
+ * @param {int|string|undefined} options.cursor the offset for paginated searches
+ * @returns {Object} {request: Promise, cancel: Promise}
+ */
+export function search({ type, query, cursor }) {
+ /**
+ * Generate an axios cancel token
+ */
+ const cancelToken = createCancelToken()
+
+ const request = async() => axios.get(generateOcsUrl('search', 2) + `providers/${type}/search`, {
+ cancelToken: cancelToken.token,
+ params: {
+ term: query,
+ cursor,
+ // Sending which location we're currently at
+ from: window.location.pathname.replace('/index.php', '') + window.location.search,
+ },
+ })
+
+ return {
+ request,
+ cancel: cancelToken.cancel,
+ }
+}
diff --git a/core/src/services/WebAuthnAuthenticationService.js b/core/src/services/WebAuthnAuthenticationService.js
new file mode 100644
index 00000000000..db05e7ca3fe
--- /dev/null
+++ b/core/src/services/WebAuthnAuthenticationService.js
@@ -0,0 +1,38 @@
+/**
+ * @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @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/>.
+ *
+ */
+
+import Axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+export function startAuthentication(loginName) {
+ const url = generateUrl('/login/webauthn/start')
+
+ return Axios.post(url, { loginName })
+ .then(resp => resp.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/session-heartbeat.js b/core/src/session-heartbeat.js
index a941720d853..828896f9fae 100644
--- a/core/src/session-heartbeat.js
+++ b/core/src/session-heartbeat.js
@@ -1,7 +1,10 @@
-/*
+/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,23 +19,40 @@
* 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/>.
+ * 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 { generateUrl } from './OC/routing'
import OC from './OC'
-import { setToken as setRequestToken } from './OC/requesttoken'
+import { setToken as setRequestToken, getToken as getRequestToken } from './OC/requesttoken'
+
+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)
* @returns {boolean}
*/
const keepSessionAlive = () => {
- return OC.config.session_keepalive === undefined
- || !!OC.config.session_keepalive
+ return config.session_keepalive === undefined
+ || !!config.session_keepalive
}
/**
@@ -41,8 +61,8 @@ const keepSessionAlive = () => {
*/
const getInterval = () => {
let interval = NaN
- if (OC.config.session_lifetime) {
- interval = Math.floor(OC.config.session_lifetime / 2)
+ if (config.session_lifetime) {
+ interval = Math.floor(config.session_lifetime / 2)
}
// minimum one minute, max 24 hours, default 15 minutes
@@ -83,11 +103,48 @@ const startPolling = () => {
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
diff --git a/core/src/systemtags/merged-systemtags.js b/core/src/systemtags/merged-systemtags.js
new file mode 100644
index 00000000000..1dc8be0ae62
--- /dev/null
+++ b/core/src/systemtags/merged-systemtags.js
@@ -0,0 +1,29 @@
+/**
+ * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @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/>.
+ *
+ */
+
+import './systemtags.js'
+import './systemtagmodel.js'
+import './systemtagsmappingcollection.js'
+import './systemtagscollection.js'
+import './systemtagsinputfield.js'
+
+import '../../css/systemtags.scss'
diff --git a/core/src/systemtags/systemtagmodel.js b/core/src/systemtags/systemtagmodel.js
new file mode 100644
index 00000000000..588d6a26fc2
--- /dev/null
+++ b/core/src/systemtags/systemtagmodel.js
@@ -0,0 +1,75 @@
+/**
+ * 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 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/>.
+ *
+ */
+
+(function(OC) {
+
+ _.extend(OC.Files.Client, {
+ PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id',
+ PROPERTY_CAN_ASSIGN: '{' + OC.Files.Client.NS_OWNCLOUD + '}can-assign',
+ PROPERTY_DISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}display-name',
+ PROPERTY_USERVISIBLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-visible',
+ PROPERTY_USERASSIGNABLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-assignable',
+ })
+
+ /**
+ * @class OCA.SystemTags.SystemTagsCollection
+ * @classdesc
+ *
+ * System tag
+ *
+ */
+ const SystemTagModel = OC.Backbone.Model.extend(
+ /** @lends OCA.SystemTags.SystemTagModel.prototype */ {
+ sync: OC.Backbone.davSync,
+
+ defaults: {
+ userVisible: true,
+ userAssignable: true,
+ canAssign: true,
+ },
+
+ davProperties: {
+ id: OC.Files.Client.PROPERTY_FILEID,
+ name: OC.Files.Client.PROPERTY_DISPLAYNAME,
+ userVisible: OC.Files.Client.PROPERTY_USERVISIBLE,
+ userAssignable: OC.Files.Client.PROPERTY_USERASSIGNABLE,
+ // read-only, effective permissions computed by the server,
+ canAssign: OC.Files.Client.PROPERTY_CAN_ASSIGN,
+ },
+
+ parse(data) {
+ return {
+ id: data.id,
+ name: data.name,
+ userVisible: data.userVisible === true || data.userVisible === 'true',
+ userAssignable: data.userAssignable === true || data.userAssignable === 'true',
+ canAssign: data.canAssign === true || data.canAssign === 'true',
+ }
+ },
+ })
+
+ OC.SystemTags = OC.SystemTags || {}
+ OC.SystemTags.SystemTagModel = SystemTagModel
+})(OC)
diff --git a/core/src/systemtags/systemtags.js b/core/src/systemtags/systemtags.js
new file mode 100644
index 00000000000..6254941296c
--- /dev/null
+++ b/core/src/systemtags/systemtags.js
@@ -0,0 +1,70 @@
+/**
+ * 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 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/>.
+ *
+ */
+
+/* eslint-disable */
+import escapeHTML from 'escape-html'
+
+(function(OC) {
+ /**
+ * @namespace
+ */
+ OC.SystemTags = {
+ /**
+ *
+ * @param {OC.SystemTags.SystemTagModel|Object|String} tag
+ * @returns {jQuery}
+ */
+ getDescriptiveTag: function(tag) {
+ if (_.isUndefined(tag.name) && !_.isUndefined(tag.toJSON)) {
+ tag = tag.toJSON()
+ }
+
+ if (_.isUndefined(tag.name)) {
+ return $('<span>').addClass('non-existing-tag').text(
+ t('core', 'Non-existing tag #{tag}', {
+ tag: tag
+ })
+ )
+ }
+
+ var $span = $('<span>')
+ $span.append(escapeHTML(tag.name))
+
+ var scope
+ if (!tag.userAssignable) {
+ scope = t('core', 'restricted')
+ }
+ if (!tag.userVisible) {
+ // invisible also implicitly means not assignable
+ scope = t('core', 'invisible')
+ }
+ if (scope) {
+ $span.append($('<em>').text(' (' + scope + ')'))
+ }
+ return $span
+ }
+ }
+})(OC)
diff --git a/core/src/systemtags/systemtagscollection.js b/core/src/systemtags/systemtagscollection.js
new file mode 100644
index 00000000000..5e6c43314e1
--- /dev/null
+++ b/core/src/systemtags/systemtagscollection.js
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2015
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ * @author Vincent Petry <vincent@nextcloud.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/>.
+ *
+ */
+
+/* eslint-disable */
+(function(OC) {
+
+ function filterFunction(model, term) {
+ return model.get('name').substr(0, term.length).toLowerCase() === term.toLowerCase()
+ }
+
+ /**
+ * @class OCA.SystemTags.SystemTagsCollection
+ * @classdesc
+ *
+ * Collection of tags assigned to a file
+ *
+ */
+ var SystemTagsCollection = OC.Backbone.Collection.extend(
+ /** @lends OC.SystemTags.SystemTagsCollection.prototype */ {
+
+ sync: OC.Backbone.davSync,
+
+ model: OC.SystemTags.SystemTagModel,
+
+ url: function() {
+ return OC.linkToRemote('dav') + '/systemtags/'
+ },
+
+ filterByName: function(name) {
+ return this.filter(function(model) {
+ return filterFunction(model, name)
+ })
+ },
+
+ reset: function() {
+ this.fetched = false
+ return OC.Backbone.Collection.prototype.reset.apply(this, arguments)
+ },
+
+ /**
+ * Lazy fetch.
+ * Only fetches once, subsequent calls will directly call the success handler.
+ *
+ * @param options
+ * @param [options.force] true to force fetch even if cached entries exist
+ *
+ * @see Backbone.Collection#fetch
+ */
+ fetch: function(options) {
+ var self = this
+ options = options || {}
+ if (this.fetched || options.force) {
+ // directly call handler
+ if (options.success) {
+ options.success(this, null, options)
+ }
+ // trigger sync event
+ this.trigger('sync', this, null, options)
+ return Promise.resolve()
+ }
+
+ var success = options.success
+ options = _.extend({}, options)
+ options.success = function() {
+ self.fetched = true
+ if (success) {
+ return success.apply(this, arguments)
+ }
+ }
+
+ return OC.Backbone.Collection.prototype.fetch.call(this, options)
+ }
+ })
+
+ OC.SystemTags = OC.SystemTags || {}
+ OC.SystemTags.SystemTagsCollection = SystemTagsCollection
+
+ /**
+ * @type OC.SystemTags.SystemTagsCollection
+ */
+ OC.SystemTags.collection = new OC.SystemTags.SystemTagsCollection()
+})(OC)
diff --git a/core/src/systemtags/systemtagsinputfield.js b/core/src/systemtags/systemtagsinputfield.js
new file mode 100644
index 00000000000..f0aac9381d6
--- /dev/null
+++ b/core/src/systemtags/systemtagsinputfield.js
@@ -0,0 +1,463 @@
+/**
+ * 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 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/>.
+ *
+ */
+
+/* eslint-disable */
+import templateResult from './templates/result.handlebars'
+import templateResultForm from './templates/result_form.handlebars'
+import templateSelection from './templates/selection.handlebars'
+
+(function(OC) {
+
+ /**
+ * @class OC.SystemTags.SystemTagsInputField
+ * @classdesc
+ *
+ * Displays a file's system tags
+ *
+ */
+ var SystemTagsInputField = OC.Backbone.View.extend(
+ /** @lends OC.SystemTags.SystemTagsInputField.prototype */ {
+
+ _rendered: false,
+
+ _newTag: null,
+
+ _lastUsedTags: [],
+
+ className: 'systemTagsInputFieldContainer',
+
+ template: function(data) {
+ return '<input class="systemTagsInputField" type="hidden" name="tags" value=""/>'
+ },
+
+ /**
+ * Creates a new SystemTagsInputField
+ *
+ * @param {Object} [options]
+ * @param {string} [options.objectType=files] object type for which tags are assigned to
+ * @param {bool} [options.multiple=false] whether to allow selecting multiple tags
+ * @param {bool} [options.allowActions=true] whether tags can be renamed/delete within the dropdown
+ * @param {bool} [options.allowCreate=true] whether new tags can be created
+ * @param {bool} [options.isAdmin=true] whether the user is an administrator
+ * @param {Function} options.initSelection function to convert selection to data
+ */
+ initialize: function(options) {
+ options = options || {}
+
+ this._multiple = !!options.multiple
+ this._allowActions = _.isUndefined(options.allowActions) || !!options.allowActions
+ this._allowCreate = _.isUndefined(options.allowCreate) || !!options.allowCreate
+ this._isAdmin = !!options.isAdmin
+
+ if (_.isFunction(options.initSelection)) {
+ this._initSelection = options.initSelection
+ }
+
+ this.collection = options.collection || OC.SystemTags.collection
+
+ var self = this
+ this.collection.on('change:name remove', function() {
+ // refresh selection
+ _.defer(self._refreshSelection)
+ })
+
+ _.defer(_.bind(this._getLastUsedTags, this))
+
+ _.bindAll(
+ this,
+ '_refreshSelection',
+ '_onClickRenameTag',
+ '_onClickDeleteTag',
+ '_onSelectTag',
+ '_onDeselectTag',
+ '_onSubmitRenameTag'
+ )
+ },
+
+ _getLastUsedTags: function() {
+ var self = this
+ $.ajax({
+ type: 'GET',
+ url: OC.generateUrl('/apps/systemtags/lastused'),
+ success: function(response) {
+ self._lastUsedTags = response
+ }
+ })
+ },
+
+ /**
+ * Refreshes the selection, triggering a call to
+ * select2's initSelection
+ */
+ _refreshSelection: function() {
+ this.$tagsField.select2('val', this.$tagsField.val())
+ },
+
+ /**
+ * Event handler whenever the user clicked the "rename" action.
+ * This will display the rename field.
+ */
+ _onClickRenameTag: function(ev) {
+ var $item = $(ev.target).closest('.systemtags-item')
+ var tagId = $item.attr('data-id')
+ var tagModel = this.collection.get(tagId)
+
+ var oldName = tagModel.get('name')
+ var $renameForm = $(templateResultForm({
+ cid: this.cid,
+ name: oldName,
+ deleteTooltip: t('core', 'Delete'),
+ renameLabel: t('core', 'Rename'),
+ isAdmin: this._isAdmin
+ }))
+ $item.find('.label').after($renameForm)
+ $item.find('.label, .systemtags-actions').addClass('hidden')
+ $item.closest('.select2-result').addClass('has-form')
+
+ $renameForm.find('[title]').tooltip({
+ placement: 'bottom',
+ container: 'body'
+ })
+ $renameForm.find('input').focus().selectRange(0, oldName.length)
+ return false
+ },
+
+ /**
+ * Event handler whenever the rename form has been submitted after
+ * the user entered a new tag name.
+ * This will submit the change to the server.
+ *
+ * @param {Object} ev event
+ */
+ _onSubmitRenameTag: function(ev) {
+ ev.preventDefault()
+ var $form = $(ev.target)
+ var $item = $form.closest('.systemtags-item')
+ var tagId = $item.attr('data-id')
+ var tagModel = this.collection.get(tagId)
+ var newName = $(ev.target).find('input').val().trim()
+ if (newName && newName !== tagModel.get('name')) {
+ tagModel.save({ 'name': newName })
+ // TODO: spinner, and only change text after finished saving
+ $item.find('.label').text(newName)
+ }
+ $item.find('.label, .systemtags-actions').removeClass('hidden')
+ $form.remove()
+ $item.closest('.select2-result').removeClass('has-form')
+ },
+
+ /**
+ * Event handler whenever a tag must be deleted
+ *
+ * @param {Object} ev event
+ */
+ _onClickDeleteTag: function(ev) {
+ var $item = $(ev.target).closest('.systemtags-item')
+ var tagId = $item.attr('data-id')
+ this.collection.get(tagId).destroy()
+ $(ev.target).tooltip('hide')
+ $item.closest('.select2-result').remove()
+ // TODO: spinner
+ return false
+ },
+
+ _addToSelect2Selection: function(selection) {
+ var data = this.$tagsField.select2('data')
+ data.push(selection)
+ this.$tagsField.select2('data', data)
+ },
+
+ /**
+ * Event handler whenever a tag is selected.
+ * Also called whenever tag creation is requested through the dummy tag object.
+ *
+ * @param {Object} e event
+ */
+ _onSelectTag: function(e) {
+ var self = this
+ var tag
+ if (e.object && e.object.isNew) {
+ // newly created tag, check if existing
+ // create a new tag
+ tag = this.collection.create({
+ name: e.object.name.trim(),
+ userVisible: true,
+ userAssignable: true,
+ canAssign: true
+ }, {
+ success: function(model) {
+ self._addToSelect2Selection(model.toJSON())
+ self._lastUsedTags.unshift(model.id)
+ self.trigger('select', model)
+ },
+ error: function(model, xhr) {
+ if (xhr.status === 409) {
+ // re-fetch collection to get the missing tag
+ self.collection.reset()
+ self.collection.fetch({
+ success: function(collection) {
+ // find the tag in the collection
+ var model = collection.where({
+ name: e.object.name.trim(),
+ userVisible: true,
+ userAssignable: true
+ })
+ if (model.length) {
+ model = model[0]
+ // the tag already exists or was already assigned,
+ // add it to the list anyway
+ self._addToSelect2Selection(model.toJSON())
+ self.trigger('select', model)
+ }
+ }
+ })
+ }
+ }
+ })
+ this.$tagsField.select2('close')
+ e.preventDefault()
+ return false
+ } else {
+ tag = this.collection.get(e.object.id)
+ this._lastUsedTags.unshift(tag.id)
+ }
+ this._newTag = null
+ this.trigger('select', tag)
+ },
+
+ /**
+ * Event handler whenever a tag gets deselected.
+ *
+ * @param {Object} e event
+ */
+ _onDeselectTag: function(e) {
+ this.trigger('deselect', e.choice.id)
+ },
+
+ /**
+ * Autocomplete function for dropdown results
+ *
+ * @param {Object} query select2 query object
+ */
+ _queryTagsAutocomplete: function(query) {
+ var self = this
+ this.collection.fetch({
+ success: function(collection) {
+ var tagModels = collection.filterByName(query.term.trim())
+ if (!self._isAdmin) {
+ tagModels = _.filter(tagModels, function(tagModel) {
+ return tagModel.get('canAssign')
+ })
+ }
+ query.callback({
+ results: _.invoke(tagModels, 'toJSON')
+ })
+ }
+ })
+ },
+
+ _preventDefault: function(e) {
+ e.stopPropagation()
+ },
+
+ /**
+ * Formats a single dropdown result
+ *
+ * @param {Object} data data to format
+ * @returns {string} HTML markup
+ */
+ _formatDropDownResult: function(data) {
+ return templateResult(_.extend({
+ renameTooltip: t('core', 'Rename'),
+ allowActions: this._allowActions,
+ tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data)[0].innerHTML : null,
+ isAdmin: this._isAdmin
+ }, data))
+ },
+
+ /**
+ * Formats a single selection item
+ *
+ * @param {Object} data data to format
+ * @returns {string} HTML markup
+ */
+ _formatSelection: function(data) {
+ return templateSelection(_.extend({
+ tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data)[0].innerHTML : null,
+ isAdmin: this._isAdmin
+ }, data))
+ },
+
+ /**
+ * Create new dummy choice for select2 when the user
+ * types an arbitrary string
+ *
+ * @param {string} term entered term
+ * @returns {Object} dummy tag
+ */
+ _createSearchChoice: function(term) {
+ term = term.trim()
+ if (this.collection.filter(function(entry) {
+ return entry.get('name') === term
+ }).length) {
+ return
+ }
+ if (!this._newTag) {
+ this._newTag = {
+ id: -1,
+ name: term,
+ userAssignable: true,
+ userVisible: true,
+ canAssign: true,
+ isNew: true
+ }
+ } else {
+ this._newTag.name = term
+ }
+
+ return this._newTag
+ },
+
+ _initSelection: function(element, callback) {
+ var self = this
+ var ids = $(element).val().split(',')
+
+ function modelToSelection(model) {
+ var data = model.toJSON()
+ if (!self._isAdmin && !data.canAssign) {
+ // lock static tags for non-admins
+ data.locked = true
+ }
+ return data
+ }
+
+ function findSelectedObjects(ids) {
+ var selectedModels = self.collection.filter(function(model) {
+ return ids.indexOf(model.id) >= 0 && (self._isAdmin || model.get('userVisible'))
+ })
+ return _.map(selectedModels, modelToSelection)
+ }
+
+ this.collection.fetch({
+ success: function() {
+ callback(findSelectedObjects(ids))
+ }
+ })
+ },
+
+ /**
+ * Renders this details view
+ */
+ render: function() {
+ var self = this
+ this.$el.html(this.template())
+
+ this.$el.find('[title]').tooltip({ placement: 'bottom' })
+ this.$tagsField = this.$el.find('[name=tags]')
+ this.$tagsField.select2({
+ placeholder: t('core', 'Collaborative tags'),
+ containerCssClass: 'systemtags-select2-container',
+ dropdownCssClass: 'systemtags-select2-dropdown',
+ closeOnSelect: false,
+ allowClear: false,
+ multiple: this._multiple,
+ toggleSelect: this._multiple,
+ query: _.bind(this._queryTagsAutocomplete, this),
+ id: function(tag) {
+ return tag.id
+ },
+ initSelection: _.bind(this._initSelection, this),
+ formatResult: _.bind(this._formatDropDownResult, this),
+ formatSelection: _.bind(this._formatSelection, this),
+ createSearchChoice: this._allowCreate ? _.bind(this._createSearchChoice, this) : undefined,
+ sortResults: function(results) {
+ var selectedItems = _.pluck(self.$tagsField.select2('data'), 'id')
+ results.sort(function(a, b) {
+ var aSelected = selectedItems.indexOf(a.id) >= 0
+ var bSelected = selectedItems.indexOf(b.id) >= 0
+ if (aSelected === bSelected) {
+ var aLastUsed = self._lastUsedTags.indexOf(a.id)
+ var bLastUsed = self._lastUsedTags.indexOf(b.id)
+
+ if (aLastUsed !== bLastUsed) {
+ if (bLastUsed === -1) {
+ return -1
+ }
+ if (aLastUsed === -1) {
+ return 1
+ }
+ return aLastUsed < bLastUsed ? -1 : 1
+ }
+
+ // Both not found
+ return OC.Util.naturalSortCompare(a.name, b.name)
+ }
+ if (aSelected && !bSelected) {
+ return -1
+ }
+ return 1
+ })
+ return results
+ },
+ formatNoMatches: function() {
+ return t('core', 'No tags found')
+ }
+ })
+ .on('select2-selecting', this._onSelectTag)
+ .on('select2-removing', this._onDeselectTag)
+
+ var $dropDown = this.$tagsField.select2('dropdown')
+ // register events for inside the dropdown
+ $dropDown.on('mouseup', '.rename', this._onClickRenameTag)
+ $dropDown.on('mouseup', '.delete', this._onClickDeleteTag)
+ $dropDown.on('mouseup', '.select2-result-selectable.has-form', this._preventDefault)
+ $dropDown.on('submit', '.systemtags-rename-form', this._onSubmitRenameTag)
+
+ this.delegateEvents()
+ },
+
+ remove: function() {
+ if (this.$tagsField) {
+ this.$tagsField.select2('destroy')
+ }
+ },
+
+ getValues: function() {
+ this.$tagsField.select2('val')
+ },
+
+ setValues: function(values) {
+ this.$tagsField.select2('val', values)
+ },
+
+ setData: function(data) {
+ this.$tagsField.select2('data', data)
+ }
+ })
+
+ OC.SystemTags = OC.SystemTags || {}
+ OC.SystemTags.SystemTagsInputField = SystemTagsInputField
+
+})(OC)
diff --git a/core/src/systemtags/systemtagsmappingcollection.js b/core/src/systemtags/systemtagsmappingcollection.js
new file mode 100644
index 00000000000..6c6b6578608
--- /dev/null
+++ b/core/src/systemtags/systemtagsmappingcollection.js
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2015
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ * @author Vincent Petry <vincent@nextcloud.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/>.
+ *
+ */
+
+import { generateRemoteUrl } from '@nextcloud/router'
+
+(function(OC) {
+ /**
+ * @class OC.SystemTags.SystemTagsMappingCollection
+ * @classdesc
+ *
+ * Collection of tags assigned to a an object
+ *
+ */
+ const SystemTagsMappingCollection = OC.Backbone.Collection.extend(
+ /** @lends OC.SystemTags.SystemTagsMappingCollection.prototype */ {
+
+ sync: OC.Backbone.davSync,
+
+ /**
+ * Use PUT instead of PROPPATCH
+ */
+ usePUT: true,
+
+ /**
+ * Id of the file for which to filter activities by
+ *
+ * @var int
+ */
+ _objectId: null,
+
+ /**
+ * Type of the object to filter by
+ *
+ * @var string
+ */
+ _objectType: 'files',
+
+ model: OC.SystemTags.SystemTagModel,
+
+ url() {
+ return generateRemoteUrl('dav') + '/systemtags-relations/' + this._objectType + '/' + this._objectId
+ },
+
+ /**
+ * Sets the object id to filter by or null for all.
+ *
+ * @param {int} objectId file id or null
+ */
+ setObjectId(objectId) {
+ this._objectId = objectId
+ },
+
+ /**
+ * Sets the object type to filter by or null for all.
+ *
+ * @param {int} objectType file id or null
+ */
+ setObjectType(objectType) {
+ this._objectType = objectType
+ },
+
+ initialize(models, options) {
+ options = options || {}
+ if (!_.isUndefined(options.objectId)) {
+ this._objectId = options.objectId
+ }
+ if (!_.isUndefined(options.objectType)) {
+ this._objectType = options.objectType
+ }
+ },
+
+ getTagIds() {
+ return this.map(function(model) {
+ return model.id
+ })
+ },
+ })
+
+ OC.SystemTags = OC.SystemTags || {}
+ OC.SystemTags.SystemTagsMappingCollection = SystemTagsMappingCollection
+})(OC)
diff --git a/core/src/systemtags/templates/result.handlebars b/core/src/systemtags/templates/result.handlebars
new file mode 100644
index 00000000000..faa7cc4d348
--- /dev/null
+++ b/core/src/systemtags/templates/result.handlebars
@@ -0,0 +1,13 @@
+<span class="systemtags-item{{#if isNew}} new-item{{/if}}" data-id="{{id}}">
+<span class="checkmark icon icon-checkmark"></span>
+ {{#if isAdmin}}
+ <span class="label">{{{tagMarkup}}}</span>
+ {{else}}
+ <span class="label">{{name}}</span>
+ {{/if}}
+ {{#allowActions}}
+ <span class="systemtags-actions">
+ <a href="#" class="rename icon icon-rename" title="{{renameTooltip}}"></a>
+ </span>
+ {{/allowActions}}
+</span>
diff --git a/core/src/systemtags/templates/result_form.handlebars b/core/src/systemtags/templates/result_form.handlebars
new file mode 100644
index 00000000000..28fe8c56fe2
--- /dev/null
+++ b/core/src/systemtags/templates/result_form.handlebars
@@ -0,0 +1,7 @@
+<form class="systemtags-rename-form">
+ <label class="hidden-visually" for="{{cid}}-rename-input">{{renameLabel}}</label>
+ <input id="{{cid}}-rename-input" type="text" value="{{name}}">
+ {{#if isAdmin}}
+ <a href="#" class="delete icon icon-delete" title="{{deleteTooltip}}"></a>
+ {{/if}}
+</form>
diff --git a/core/src/systemtags/templates/selection.handlebars b/core/src/systemtags/templates/selection.handlebars
new file mode 100644
index 00000000000..b006b129748
--- /dev/null
+++ b/core/src/systemtags/templates/selection.handlebars
@@ -0,0 +1,5 @@
+{{#if isAdmin}}
+ <span class="label">{{{tagMarkup}}}</span>
+{{else}}
+ <span class="label">{{name}}</span>
+{{/if}}
diff --git a/core/src/tests/.eslintrc.js b/core/src/tests/.eslintrc.js
new file mode 100644
index 00000000000..449d15fec67
--- /dev/null
+++ b/core/src/tests/.eslintrc.js
@@ -0,0 +1,31 @@
+/**
+ * @copyright Copyright (c) 2016 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/>.
+ *
+ */
+
+module.exports = {
+ globals: {
+ jsdom: true,
+ sinon: true,
+ },
+ rules: {
+ "node/no-unpublished-import": 'off'
+ }
+}
diff --git a/core/src/tests/OC/requesttoken.spec.js b/core/src/tests/OC/requesttoken.spec.js
index d19a4b8e9c8..fb550a93ebe 100644
--- a/core/src/tests/OC/requesttoken.spec.js
+++ b/core/src/tests/OC/requesttoken.spec.js
@@ -1,7 +1,9 @@
-/*
+/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author 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 GNU AGPL version 3 or any later version
*
@@ -16,45 +18,43 @@
* 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
*/
-import {JSDOM} from 'jsdom'
-import {subscribe, unsubscribe} from '@nextcloud/event-bus'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import {manageToken, setToken} from '../../OC/requesttoken'
+import { manageToken, setToken } from '../../OC/requesttoken'
describe('request token', () => {
- let dom
let emit
let manager
const token = 'abc123'
beforeEach(() => {
- dom = new JSDOM()
- emit = sinon.spy()
- const head = dom.window.document.getElementsByTagName('head')[0]
+ emit = jest.fn()
+ const head = window.document.getElementsByTagName('head')[0]
head.setAttribute('data-requesttoken', token)
- manager = manageToken(dom.window.document, emit)
+ manager = manageToken(window.document, emit)
})
- it('reads the token from the document', () => {
- expect(manager.getToken()).to.equal('abc123')
+ test('reads the token from the document', () => {
+ expect(manager.getToken()).toBe('abc123')
})
- it('remembers the updated token', () => {
+ test('remembers the updated token', () => {
manager.setToken('bca321')
- expect(manager.getToken()).to.equal('bca321')
+ expect(manager.getToken()).toBe('bca321')
})
describe('@nextcloud/auth integration', () => {
let listener
beforeEach(() => {
- listener = sinon.spy()
+ listener = jest.fn()
subscribe('csrf-token-update', listener)
})
@@ -63,10 +63,10 @@ describe('request token', () => {
unsubscribe('csrf-token-update', listener)
})
- it('fires off an event for @nextcloud/auth', () => {
+ test('fires off an event for @nextcloud/auth', () => {
setToken('123')
- expect(listener).to.have.been.calledWith({token: '123'})
+ expect(listener).toHaveBeenCalledWith({ token: '123' })
})
})
diff --git a/core/src/tests/setup.js b/core/src/tests/setup.js
deleted file mode 100644
index bec4e11eaea..00000000000
--- a/core/src/tests/setup.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2018 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/>.
- */
-
-require('jsdom-global')()
-const chai = require('chai')
-var sinon = require('sinon')
-var sinonChai = require('sinon-chai');
-
-chai.use(sinonChai)
-global.expect = chai.expect
-global.sinon = sinon
-
-// https://github.com/vuejs/vue-test-utils/issues/936
-// better fix for "TypeError: Super expression must either be null or
-// a function" than pinning an old version of prettier.
-//
-// https://github.com/vuejs/vue-cli/issues/2128#issuecomment-453109575
-window.Date = Date
diff --git a/core/src/unified-search.js b/core/src/unified-search.js
new file mode 100644
index 00000000000..a799586017b
--- /dev/null
+++ b/core/src/unified-search.js
@@ -0,0 +1,59 @@
+/**
+ * @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/>.
+ *
+ */
+
+import { generateFilePath } from '@nextcloud/router'
+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())
+
+// eslint-disable-next-line camelcase
+__webpack_public_path__ = generateFilePath('core', '', 'js/')
+
+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/views/Login.vue b/core/src/views/Login.vue
index baea18cbe3c..1372f40b112 100644
--- a/core/src/views/Login.vue
+++ b/core/src/views/Login.vue
@@ -20,9 +20,9 @@
-->
<template>
- <div>
+ <div v-if="!hideLoginForm || directLogin">
<transition name="fade" mode="out-in">
- <div v-if="!resetPassword && resetPasswordTarget === ''"
+ <div v-if="!passwordlessLogin && !resetPassword && resetPasswordTarget === ''"
key="login">
<LoginForm
:username.sync="user"
@@ -45,6 +45,40 @@
@click.prevent="resetPassword = true">
{{ t('core', 'Forgot password?') }}
</a>
+ <br>
+ <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>
+ </div>
+ <div v-else-if="!loading && passwordlessLogin"
+ key="reset"
+ class="login-additional">
+ <PasswordLessLoginForm
+ :username.sync="user"
+ :redirect-url="redirectUrl"
+ :inverted-colors="invertedColors"
+ :auto-complete-allowed="autoCompleteAllowed"
+ :is-https="isHttps"
+ :is-localhost="isLocalhost"
+ :has-public-key-credential="hasPublicKeyCredential"
+ @submit="loading = true" />
+ <a href="#" @click.prevent="passwordlessLogin = false">
+ {{ t('core', 'Back') }}
+ </a>
</div>
<div v-else-if="!loading && canResetPassword"
key="reset"
@@ -65,10 +99,20 @@
</div>
</transition>
</div>
+ <div v-else>
+ <transition name="fade" mode="out-in">
+ <div class="warning">
+ {{ t('core', 'Login form is disabled.') }}<br>
+ <small>{{ t('core', 'Please contact your administrator.') }}
+ </small>
+ </div>
+ </transition>
+ </div>
</template>
<script>
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'
@@ -76,6 +120,7 @@ export default {
name: 'Login',
components: {
LoginForm,
+ PasswordLessLoginForm,
ResetPassword,
UpdatePassword,
},
@@ -120,11 +165,36 @@ export default {
type: Boolean,
default: false,
},
+ hasPasswordless: {
+ type: Boolean,
+ default: false,
+ },
+ countAlternativeLogins: {
+ type: Number,
+ default: 0,
+ },
+ isHttps: {
+ type: Boolean,
+ default: false,
+ },
+ isLocalhost: {
+ type: Boolean,
+ default: false,
+ },
+ hasPublicKeyCredential: {
+ type: Boolean,
+ default: false,
+ },
+ hideLoginForm: {
+ type: Boolean,
+ default: false,
+ },
},
data() {
return {
loading: false,
user: this.username,
+ passwordlessLogin: false,
resetPassword: false,
}
},
diff --git a/core/src/views/UnifiedSearch.vue b/core/src/views/UnifiedSearch.vue
new file mode 100644
index 00000000000..80ba7c7c7b9
--- /dev/null
+++ b/core/src/views/UnifiedSearch.vue
@@ -0,0 +1,761 @@
+ <!--
+ - @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/>.
+ -
+ -->
+<template>
+ <HeaderMenu id="unified-search"
+ class="unified-search"
+ exclude-click-outside-classes="popover"
+ :open.sync="open"
+ @open="onOpen"
+ @close="onClose">
+ <!-- Header icon -->
+ <template #trigger>
+ <Magnify class="unified-search__trigger"
+ :size="20"
+ :title="ariaLabel"
+ fill-color="var(--color-primary-text)" />
+ </template>
+
+ <!-- Search form & filters wrapper -->
+ <div class="unified-search__input-wrapper">
+ <form class="unified-search__form"
+ role="search"
+ :class="{'icon-loading-small': isLoading}"
+ @submit.prevent.stop="onInputEnter"
+ @reset.prevent.stop="onReset">
+ <!-- Search input -->
+ <input ref="input"
+ v-model="query"
+ class="unified-search__form-input"
+ type="search"
+ :class="{'unified-search__form-input--with-reset': !!query}"
+ :placeholder="t('core', 'Search {types} …', { types: typesNames.join(', ') })"
+ @input="onInputDebounced"
+ @keypress.enter.prevent.stop="onInputEnter">
+
+ <!-- Reset search button -->
+ <input v-if="!!query && !isLoading"
+ type="reset"
+ class="unified-search__form-reset icon-close"
+ :aria-label="t('core','Reset search')"
+ value="">
+ </form>
+
+ <!-- Search filters -->
+ <Actions v-if="availableFilters.length > 1" class="unified-search__filters" placement="bottom">
+ <ActionButton v-for="type in availableFilters"
+ :key="type"
+ icon="icon-filter"
+ :title="t('core', 'Search for {name} only', { name: typesMap[type] })"
+ @click="onClickFilter(`in:${type}`)">
+ {{ `in:${type}` }}
+ </ActionButton>
+ </Actions>
+ </div>
+
+ <template v-if="!hasResults">
+ <!-- Loading placeholders -->
+ <SearchResultPlaceholders v-if="isLoading" />
+
+ <EmptyContent v-else-if="isValidQuery" icon="icon-search">
+ {{ t('core', 'No results for {query}', {query}) }}
+ </EmptyContent>
+
+ <EmptyContent v-else-if="!isLoading || isShortQuery" icon="icon-search">
+ {{ t('core', 'Start typing to search') }}
+ <template v-if="isShortQuery" #desc>
+ {{ n('core',
+ 'Please enter {minSearchLength} character or more to search',
+ 'Please enter {minSearchLength} characters or more to search',
+ minSearchLength,
+ {minSearchLength}) }}
+ </template>
+ </EmptyContent>
+ </template>
+
+ <!-- Grouped search results -->
+ <template v-else>
+ <ul v-for="({list, type}, typesIndex) in orderedResults"
+ :key="type"
+ class="unified-search__results"
+ :class="`unified-search__results-${type}`"
+ :aria-label="typesMap[type]">
+ <!-- Search results -->
+ <li v-for="(result, index) in limitIfAny(list, type)" :key="result.resourceUrl">
+ <SearchResult v-bind="result"
+ :query="query"
+ :focused="focused === 0 && typesIndex === 0 && index === 0"
+ @focus="setFocusedIndex" />
+ </li>
+
+ <!-- Load more button -->
+ <li>
+ <SearchResult v-if="!reached[type]"
+ class="unified-search__result-more"
+ :title="loading[type]
+ ? t('core', 'Loading more results …')
+ : t('core', 'Load more results')"
+ :icon-class="loading[type] ? 'icon-loading-small' : ''"
+ @click.prevent="loadMore(type)"
+ @focus="setFocusedIndex" />
+ </li>
+ </ul>
+ </template>
+ </HeaderMenu>
+</template>
+
+<script>
+import { emit } from '@nextcloud/event-bus'
+import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot } from '../services/UnifiedSearchService'
+import { showError } from '@nextcloud/dialogs'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import Actions from '@nextcloud/vue/dist/Components/Actions'
+import debounce from 'debounce'
+import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
+import Magnify from 'vue-material-design-icons/Magnify'
+
+import HeaderMenu from '../components/HeaderMenu'
+import SearchResult from '../components/UnifiedSearch/SearchResult'
+import SearchResultPlaceholders from '../components/UnifiedSearch/SearchResultPlaceholders'
+
+const REQUEST_FAILED = 0
+const REQUEST_OK = 1
+const REQUEST_CANCELED = 2
+
+export default {
+ name: 'UnifiedSearch',
+
+ components: {
+ ActionButton,
+ Actions,
+ EmptyContent,
+ HeaderMenu,
+ Magnify,
+ SearchResult,
+ SearchResultPlaceholders,
+ },
+
+ data() {
+ return {
+ types: [],
+
+ // Cursors per types
+ cursors: {},
+ // Various search limits per types
+ limits: {},
+ // Loading types
+ loading: {},
+ // Reached search types
+ reached: {},
+ // Pending cancellable requests
+ requests: [],
+ // List of all results
+ results: {},
+
+ query: '',
+ focused: null,
+
+ defaultLimit,
+ minSearchLength,
+
+ open: false,
+ }
+ },
+
+ computed: {
+ typesIDs() {
+ return this.types.map(type => type.id)
+ },
+ typesNames() {
+ return this.types.map(type => type.name)
+ },
+ typesMap() {
+ return this.types.reduce((prev, curr) => {
+ prev[curr.id] = curr.name
+ return prev
+ }, {})
+ },
+
+ ariaLabel() {
+ return t('core', 'Search')
+ },
+
+ /**
+ * Is there any result to display
+ * @returns {boolean}
+ */
+ hasResults() {
+ return Object.keys(this.results).length !== 0
+ },
+
+ /**
+ * Return ordered results
+ * @returns {Array}
+ */
+ orderedResults() {
+ return this.typesIDs
+ .filter(type => type in this.results)
+ .map(type => ({
+ type,
+ list: this.results[type],
+ }))
+ },
+
+ /**
+ * Available filters
+ * We only show filters that are available on the results
+ * @returns {string[]}
+ */
+ availableFilters() {
+ return Object.keys(this.results)
+ },
+
+ /**
+ * Applied filters
+ * @returns {string[]}
+ */
+ usedFiltersIn() {
+ let match
+ const filters = []
+ while ((match = regexFilterIn.exec(this.query)) !== null) {
+ filters.push(match[1])
+ }
+ return filters
+ },
+
+ /**
+ * Applied anti filters
+ * @returns {string[]}
+ */
+ usedFiltersNot() {
+ let match
+ const filters = []
+ while ((match = regexFilterNot.exec(this.query)) !== null) {
+ filters.push(match[1])
+ }
+ return filters
+ },
+
+ /**
+ * Is the current search too short
+ * @returns {boolean}
+ */
+ isShortQuery() {
+ return this.query && this.query.trim().length < minSearchLength
+ },
+
+ /**
+ * Is the current search valid
+ * @returns {boolean}
+ */
+ isValidQuery() {
+ return this.query && this.query.trim() !== '' && !this.isShortQuery
+ },
+
+ /**
+ * Have we reached the end of all types searches
+ * @returns {boolean}
+ */
+ isDoneSearching() {
+ return Object.values(this.reached).every(state => state === false)
+ },
+
+ /**
+ * Is there any search in progress
+ * @returns {boolean}
+ */
+ isLoading() {
+ return Object.values(this.loading).some(state => state === true)
+ },
+ },
+
+ async created() {
+ this.types = await getTypes()
+ this.logger.debug('Unified Search initialized with the following providers', this.types)
+ },
+
+ mounted() {
+ document.addEventListener('keydown', (event) => {
+ // if not already opened, allows us to trigger default browser on second keydown
+ if (event.ctrlKey && event.key === 'f' && !this.open) {
+ event.preventDefault()
+ this.open = true
+ this.focusInput()
+ }
+
+ // https://www.w3.org/WAI/GL/wiki/Using_ARIA_menus
+ if (this.open) {
+ // If arrow down, focus next result
+ if (event.key === 'ArrowDown') {
+ this.focusNext(event)
+ }
+
+ // If arrow up, focus prev result
+ if (event.key === 'ArrowUp') {
+ this.focusPrev(event)
+ }
+ }
+ })
+ },
+
+ methods: {
+ async onOpen() {
+ this.focusInput()
+ // Update types list in the background
+ this.types = await getTypes()
+ },
+ onClose() {
+ emit('nextcloud:unified-search.close')
+ },
+
+ /**
+ * Reset the search state
+ */
+ onReset() {
+ emit('nextcloud:unified-search.reset')
+ this.logger.debug('Search reset')
+ this.query = ''
+ this.resetState()
+ this.focusInput()
+ },
+ async resetState() {
+ this.cursors = {}
+ this.limits = {}
+ this.reached = {}
+ this.results = {}
+ this.focused = null
+ await this.cancelPendingRequests()
+ },
+
+ /**
+ * Cancel any ongoing searches
+ */
+ async cancelPendingRequests() {
+ // Cloning so we can keep processing other requests
+ const requests = this.requests.slice(0)
+ this.requests = []
+
+ // Cancel all pending requests
+ await Promise.all(requests.map(cancel => cancel()))
+ },
+
+ /**
+ * Focus the search input on next tick
+ */
+ focusInput() {
+ this.$nextTick(() => {
+ this.$refs.input.focus()
+ this.$refs.input.select()
+ })
+ },
+
+ /**
+ * If we have results already, open first one
+ * If not, trigger the search again
+ */
+ onInputEnter() {
+ if (this.hasResults) {
+ const results = this.getResultsList()
+ results[0].click()
+ return
+ }
+ this.onInput()
+ },
+
+ /**
+ * Start searching on input
+ */
+ async onInput() {
+ // emit the search query
+ emit('nextcloud:unified-search.search', { query: this.query })
+
+ // Do not search if not long enough
+ if (this.query.trim() === '' || this.isShortQuery) {
+ return
+ }
+
+ let types = this.typesIDs
+ let query = this.query
+
+ // Filter out types
+ if (this.usedFiltersNot.length > 0) {
+ types = this.typesIDs.filter(type => this.usedFiltersNot.indexOf(type) === -1)
+ }
+
+ // Only use those filters if any and check if they are valid
+ if (this.usedFiltersIn.length > 0) {
+ types = this.typesIDs.filter(type => this.usedFiltersIn.indexOf(type) > -1)
+ }
+
+ // Remove any filters from the query
+ query = query.replace(regexFilterIn, '').replace(regexFilterNot, '')
+
+ // Reset search if the query changed
+ await this.resetState()
+ this.$set(this.loading, 'all', true)
+ this.logger.debug(`Searching ${query} in`, types)
+
+ Promise.all(types.map(async type => {
+ try {
+ // Init cancellable request
+ const { request, cancel } = search({ type, query })
+ this.requests.push(cancel)
+
+ // Fetch results
+ const { data } = await request()
+
+ // Process results
+ if (data.ocs.data.entries.length > 0) {
+ this.$set(this.results, type, data.ocs.data.entries)
+ } else {
+ this.$delete(this.results, type)
+ }
+
+ // Save cursor if any
+ if (data.ocs.data.cursor) {
+ this.$set(this.cursors, type, data.ocs.data.cursor)
+ } else if (!data.ocs.data.isPaginated) {
+ // If no cursor and no pagination, we save the default amount
+ // provided by server's initial state `defaultLimit`
+ this.$set(this.limits, type, this.defaultLimit)
+ }
+
+ // Check if we reached end of pagination
+ if (data.ocs.data.entries.length < this.defaultLimit) {
+ this.$set(this.reached, type, true)
+ }
+
+ // If none already focused, focus the first rendered result
+ if (this.focused === null) {
+ this.focused = 0
+ }
+ return REQUEST_OK
+ } catch (error) {
+ this.$delete(this.results, type)
+
+ // If this is not a cancelled throw
+ if (error.response && error.response.status) {
+ this.logger.error(`Error searching for ${this.typesMap[type]}`, error)
+ showError(this.t('core', 'An error occurred while searching for {type}', { type: this.typesMap[type] }))
+ return REQUEST_FAILED
+ }
+ return REQUEST_CANCELED
+ }
+ })).then(results => {
+ // Do not declare loading finished if the request have been cancelled
+ // This means another search was triggered and we're therefore still loading
+ if (results.some(result => result === REQUEST_CANCELED)) {
+ return
+ }
+ // We finished all searches
+ this.loading = {}
+ })
+ },
+ onInputDebounced: debounce(function(e) {
+ this.onInput(e)
+ }, 200),
+
+ /**
+ * Load more results for the provided type
+ * @param {String} type type
+ */
+ async loadMore(type) {
+ // If already loading, ignore
+ if (this.loading[type]) {
+ return
+ }
+
+ if (this.cursors[type]) {
+ // Init cancellable request
+ const { request, cancel } = search({ type, query: this.query, cursor: this.cursors[type] })
+ this.requests.push(cancel)
+
+ // Fetch results
+ const { data } = await request()
+
+ // Save cursor if any
+ if (data.ocs.data.cursor) {
+ this.$set(this.cursors, type, data.ocs.data.cursor)
+ }
+
+ // Process results
+ if (data.ocs.data.entries.length > 0) {
+ this.results[type].push(...data.ocs.data.entries)
+ }
+
+ // Check if we reached end of pagination
+ if (data.ocs.data.entries.length < this.defaultLimit) {
+ this.$set(this.reached, type, true)
+ }
+ } else
+
+ // If no cursor, we might have all the results already,
+ // let's fake pagination and show the next xxx entries
+ if (this.limits[type] && this.limits[type] >= 0) {
+ this.limits[type] += this.defaultLimit
+
+ // Check if we reached end of pagination
+ if (this.limits[type] >= this.results[type].length) {
+ this.$set(this.reached, type, true)
+ }
+ }
+
+ // Focus result after render
+ if (this.focused !== null) {
+ this.$nextTick(() => {
+ this.focusIndex(this.focused)
+ })
+ }
+ },
+
+ /**
+ * Return a subset of the array if the search provider
+ * doesn't supports pagination
+ *
+ * @param {Array} list the results
+ * @param {string} type the type
+ * @returns {Array}
+ */
+ limitIfAny(list, type) {
+ if (type in this.limits) {
+ return list.slice(0, this.limits[type])
+ }
+ return list
+ },
+
+ getResultsList() {
+ return this.$el.querySelectorAll('.unified-search__results .unified-search__result')
+ },
+
+ /**
+ * Focus the first result if any
+ * @param {Event} event the keydown event
+ */
+ focusFirst(event) {
+ const results = this.getResultsList()
+ if (results && results.length > 0) {
+ if (event) {
+ event.preventDefault()
+ }
+ this.focused = 0
+ this.focusIndex(this.focused)
+ }
+ },
+
+ /**
+ * Focus the next result if any
+ * @param {Event} event the keydown event
+ */
+ focusNext(event) {
+ if (this.focused === null) {
+ this.focusFirst(event)
+ return
+ }
+
+ const results = this.getResultsList()
+ // If we're not focusing the last, focus the next one
+ if (results && results.length > 0 && this.focused + 1 < results.length) {
+ event.preventDefault()
+ this.focused++
+ this.focusIndex(this.focused)
+ }
+ },
+
+ /**
+ * Focus the previous result if any
+ * @param {Event} event the keydown event
+ */
+ focusPrev(event) {
+ if (this.focused === null) {
+ this.focusFirst(event)
+ return
+ }
+
+ const results = this.getResultsList()
+ // If we're not focusing the first, focus the previous one
+ if (results && results.length > 0 && this.focused > 0) {
+ event.preventDefault()
+ this.focused--
+ this.focusIndex(this.focused)
+ }
+
+ },
+
+ /**
+ * Focus the specified result index if it exists
+ * @param {number} index the result index
+ */
+ focusIndex(index) {
+ const results = this.getResultsList()
+ if (results && results[index]) {
+ results[index].focus()
+ }
+ },
+
+ /**
+ * Set the current focused element based on the target
+ * @param {Event} event the focus event
+ */
+ setFocusedIndex(event) {
+ const entry = event.target
+ const results = this.getResultsList()
+ const index = [...results].findIndex(search => search === entry)
+ if (index > -1) {
+ // let's not use focusIndex as the entry is already focused
+ this.focused = index
+ }
+ },
+
+ onClickFilter(filter) {
+ this.query = `${this.query} ${filter}`
+ .replace(/ {2}/g, ' ')
+ .trim()
+ this.onInput()
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+$margin: 10px;
+$input-height: 34px;
+$input-padding: 6px;
+
+.unified-search {
+ &__trigger {
+ width: 20px;
+ height: 20px;
+ }
+
+ &__input-wrapper {
+ position: sticky;
+ // above search results
+ z-index: 2;
+ top: 0;
+ display: inline-flex;
+ align-items: center;
+ width: 100%;
+ background-color: var(--color-main-background);
+ }
+
+ &__filters {
+ margin: $margin / 2 $margin;
+ ul {
+ display: inline-flex;
+ justify-content: space-between;
+ }
+ }
+
+ &__form {
+ position: relative;
+ width: 100%;
+ margin: $margin;
+
+ // Loading spinner
+ &::after {
+ right: $input-padding;
+ left: auto;
+ }
+
+ &-input,
+ &-reset {
+ margin: $input-padding / 2;
+ }
+
+ &-input {
+ width: 100%;
+ height: $input-height;
+ padding: $input-padding;
+
+ &,
+ &[placeholder],
+ &::placeholder {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ // Hide webkit clear search
+ &::-webkit-search-decoration,
+ &::-webkit-search-cancel-button,
+ &::-webkit-search-results-button,
+ &::-webkit-search-results-decoration {
+ -webkit-appearance: none;
+ }
+
+ // Ellipsis earlier if reset button is here
+ .icon-loading-small &,
+ &--with-reset {
+ padding-right: $input-height;
+ }
+ }
+
+ &-reset {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: $input-height - $input-padding;
+ height: $input-height - $input-padding;
+ padding: 0;
+ opacity: .5;
+ border: none;
+ background-color: transparent;
+ margin-right: 0;
+
+ &:hover,
+ &:focus,
+ &:active {
+ opacity: 1;
+ }
+ }
+ }
+
+ &__filters {
+ margin-right: $margin / 2;
+ }
+
+ &__results {
+ &::before {
+ display: block;
+ margin: $margin;
+ margin-left: $margin + $input-padding;
+ content: attr(aria-label);
+ color: var(--color-primary-element);
+ }
+ }
+
+ .unified-search__result-more::v-deep {
+ color: var(--color-text-maxcontrast);
+ }
+
+ .empty-content {
+ margin: 10vh 0;
+
+ ::v-deep .empty-content__title {
+ font-weight: normal;
+ font-size: var(--default-font-size);
+ padding: 0 15px;
+ text-align: center;
+ }
+ }
+}
+
+</style>