aboutsummaryrefslogtreecommitdiffstats
path: root/core/src/OC
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/OC')
-rw-r--r--core/src/OC/admin.js14
-rw-r--r--core/src/OC/appconfig.js58
-rw-r--r--core/src/OC/apps.js120
-rw-r--r--core/src/OC/appswebroots.js8
-rw-r--r--core/src/OC/backbone-webdav.js308
-rw-r--r--core/src/OC/backbone.js17
-rw-r--r--core/src/OC/capabilities.js18
-rw-r--r--core/src/OC/config.js8
-rw-r--r--core/src/OC/constants.js15
-rw-r--r--core/src/OC/currentuser.js20
-rw-r--r--core/src/OC/debug.js8
-rw-r--r--core/src/OC/dialogs.js789
-rw-r--r--core/src/OC/eventsource.js145
-rw-r--r--core/src/OC/get_set.js38
-rw-r--r--core/src/OC/host.js42
-rw-r--r--core/src/OC/index.js294
-rw-r--r--core/src/OC/l10n.js90
-rw-r--r--core/src/OC/menu.js125
-rw-r--r--core/src/OC/msg.js99
-rw-r--r--core/src/OC/navigation.js13
-rw-r--r--core/src/OC/notification.js164
-rw-r--r--core/src/OC/password-confirmation.js26
-rw-r--r--core/src/OC/plugins.js70
-rw-r--r--core/src/OC/query-string.js75
-rw-r--r--core/src/OC/requesttoken.ts49
-rw-r--r--core/src/OC/routing.js18
-rw-r--r--core/src/OC/theme.js6
-rw-r--r--core/src/OC/util-history.js168
-rw-r--r--core/src/OC/util.js244
-rw-r--r--core/src/OC/webroot.js18
-rw-r--r--core/src/OC/xhr-error.js102
31 files changed, 3169 insertions, 0 deletions
diff --git a/core/src/OC/admin.js b/core/src/OC/admin.js
new file mode 100644
index 00000000000..d29e4cf676b
--- /dev/null
+++ b/core/src/OC/admin.js
@@ -0,0 +1,14 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+const isAdmin = !!window._oc_isadmin
+
+/**
+ * Returns whether the current user is an administrator
+ *
+ * @return {boolean} true if the user is an admin, false otherwise
+ * @since 9.0.0
+ */
+export const isUserAdmin = () => isAdmin
diff --git a/core/src/OC/appconfig.js b/core/src/OC/appconfig.js
new file mode 100644
index 00000000000..350ffc3f21c
--- /dev/null
+++ b/core/src/OC/appconfig.js
@@ -0,0 +1,58 @@
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2014 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* eslint-disable */
+ import { getValue, setValue, getApps, getKeys, deleteKey } from '../OCP/appconfig.js'
+
+export const appConfig = window.oc_appconfig || {}
+
+/**
+ * @namespace
+ * @deprecated 16.0.0 Use OCP.AppConfig instead
+ */
+export const AppConfig = {
+ /**
+ * @deprecated Use OCP.AppConfig.getValue() instead
+ */
+ getValue: function(app, key, defaultValue, callback) {
+ getValue(app, key, defaultValue, {
+ success: callback
+ })
+ },
+
+ /**
+ * @deprecated Use OCP.AppConfig.setValue() instead
+ */
+ setValue: function(app, key, value) {
+ setValue(app, key, value)
+ },
+
+ /**
+ * @deprecated Use OCP.AppConfig.getApps() instead
+ */
+ getApps: function(callback) {
+ getApps({
+ success: callback
+ })
+ },
+
+ /**
+ * @deprecated Use OCP.AppConfig.getKeys() instead
+ */
+ getKeys: function(app, callback) {
+ getKeys(app, {
+ success: callback
+ })
+ },
+
+ /**
+ * @deprecated Use OCP.AppConfig.deleteKey() instead
+ */
+ deleteKey: function(app, key) {
+ deleteKey(app, key)
+ }
+
+}
diff --git a/core/src/OC/apps.js b/core/src/OC/apps.js
new file mode 100644
index 00000000000..dec2b94bfbb
--- /dev/null
+++ b/core/src/OC/apps.js
@@ -0,0 +1,120 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2014 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import $ from 'jquery'
+
+let dynamicSlideToggleEnabled = false
+
+const Apps = {
+ enableDynamicSlideToggle() {
+ dynamicSlideToggleEnabled = true
+ },
+}
+
+/**
+ * Shows the #app-sidebar and add .with-app-sidebar to subsequent siblings
+ *
+ * @param {object} [$el] sidebar element to show, defaults to $('#app-sidebar')
+ */
+Apps.showAppSidebar = function($el) {
+ const $appSidebar = $el || $('#app-sidebar')
+ $appSidebar.removeClass('disappear').show()
+ $('#app-content').trigger(new $.Event('appresized'))
+}
+
+/**
+ * Shows the #app-sidebar and removes .with-app-sidebar from subsequent
+ * siblings
+ *
+ * @param {object} [$el] sidebar element to hide, defaults to $('#app-sidebar')
+ */
+Apps.hideAppSidebar = function($el) {
+ const $appSidebar = $el || $('#app-sidebar')
+ $appSidebar.hide().addClass('disappear')
+ $('#app-content').trigger(new $.Event('appresized'))
+}
+
+/**
+ * Provides a way to slide down a target area through a button and slide it
+ * up if the user clicks somewhere else. Used for the news app settings and
+ * add new field.
+ *
+ * Usage:
+ * <button data-apps-slide-toggle=".slide-area">slide</button>
+ * <div class=".slide-area" class="hidden">I'm sliding up</div>
+ */
+export const registerAppsSlideToggle = () => {
+ let buttons = $('[data-apps-slide-toggle]')
+
+ if (buttons.length === 0) {
+ $('#app-navigation').addClass('without-app-settings')
+ }
+
+ $(document).click(function(event) {
+
+ if (dynamicSlideToggleEnabled) {
+ buttons = $('[data-apps-slide-toggle]')
+ }
+
+ buttons.each(function(index, button) {
+
+ const areaSelector = $(button).data('apps-slide-toggle')
+ const area = $(areaSelector)
+
+ /**
+ *
+ */
+ function hideArea() {
+ area.slideUp(OC.menuSpeed * 4, function() {
+ area.trigger(new $.Event('hide'))
+ })
+ area.removeClass('opened')
+ $(button).removeClass('opened')
+ $(button).attr('aria-expanded', 'false')
+ }
+
+ /**
+ *
+ */
+ function showArea() {
+ area.slideDown(OC.menuSpeed * 4, function() {
+ area.trigger(new $.Event('show'))
+ })
+ area.addClass('opened')
+ $(button).addClass('opened')
+ $(button).attr('aria-expanded', 'true')
+ const input = $(areaSelector + ' [autofocus]')
+ if (input.length === 1) {
+ input.focus()
+ }
+ }
+
+ // do nothing if the area is animated
+ if (!area.is(':animated')) {
+
+ // button toggles the area
+ if ($(button).is($(event.target).closest('[data-apps-slide-toggle]'))) {
+ if (area.is(':visible')) {
+ hideArea()
+ } else {
+ showArea()
+ }
+
+ // all other areas that have not been clicked but are open
+ // should be slid up
+ } else {
+ const closest = $(event.target).closest(areaSelector)
+ if (area.is(':visible') && closest[0] !== area[0]) {
+ hideArea()
+ }
+ }
+ }
+ })
+
+ })
+}
+
+export default Apps
diff --git a/core/src/OC/appswebroots.js b/core/src/OC/appswebroots.js
new file mode 100644
index 00000000000..debbd2084bf
--- /dev/null
+++ b/core/src/OC/appswebroots.js
@@ -0,0 +1,8 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+const appswebroots = (window._oc_appswebroots !== undefined) ? window._oc_appswebroots : false
+
+export default appswebroots
diff --git a/core/src/OC/backbone-webdav.js b/core/src/OC/backbone-webdav.js
new file mode 100644
index 00000000000..318c50e8ee5
--- /dev/null
+++ b/core/src/OC/backbone-webdav.js
@@ -0,0 +1,308 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* eslint-disable */
+import _ from 'underscore'
+import { dav } from 'davclient.js'
+
+const methodMap = {
+ create: 'POST',
+ update: 'PROPPATCH',
+ patch: 'PROPPATCH',
+ delete: 'DELETE',
+ read: 'PROPFIND'
+}
+
+// Throw an error when a URL is needed, and none is supplied.
+function urlError() {
+ throw new Error('A "url" property or function must be specified')
+}
+
+/**
+ * Convert a single propfind result to JSON
+ *
+ * @param {Object} result
+ * @param {Object} davProperties properties mapping
+ */
+function parsePropFindResult(result, davProperties) {
+ if (_.isArray(result)) {
+ return _.map(result, function(subResult) {
+ return parsePropFindResult(subResult, davProperties)
+ })
+ }
+ var props = {
+ href: result.href
+ }
+
+ _.each(result.propStat, function(propStat) {
+ if (propStat.status !== 'HTTP/1.1 200 OK') {
+ return
+ }
+
+ for (var key in propStat.properties) {
+ var propKey = key
+ if (key in davProperties) {
+ propKey = davProperties[key]
+ }
+ props[propKey] = propStat.properties[key]
+ }
+ })
+
+ if (!props.id) {
+ // parse id from href
+ props.id = parseIdFromLocation(props.href)
+ }
+
+ return props
+}
+
+/**
+ * Parse ID from location
+ *
+ * @param {string} url url
+ * @returns {string} id
+ */
+function parseIdFromLocation(url) {
+ var queryPos = url.indexOf('?')
+ if (queryPos > 0) {
+ url = url.substr(0, queryPos)
+ }
+
+ var parts = url.split('/')
+ var result
+ do {
+ result = parts[parts.length - 1]
+ parts.pop()
+ // note: first result can be empty when there is a trailing slash,
+ // so we take the part before that
+ } while (!result && parts.length > 0)
+
+ return result
+}
+
+function isSuccessStatus(status) {
+ return status >= 200 && status <= 299
+}
+
+function convertModelAttributesToDavProperties(attrs, davProperties) {
+ var props = {}
+ var key
+ for (key in attrs) {
+ var changedProp = davProperties[key]
+ var value = attrs[key]
+ if (!changedProp) {
+ console.warn('No matching DAV property for property "' + key)
+ changedProp = key
+ }
+ if (_.isBoolean(value) || _.isNumber(value)) {
+ // convert to string
+ value = '' + value
+ }
+ props[changedProp] = value
+ }
+ return props
+}
+
+function callPropFind(client, options, model, headers) {
+ return client.propFind(
+ options.url,
+ _.values(options.davProperties) || [],
+ options.depth,
+ headers
+ ).then(function(response) {
+ if (isSuccessStatus(response.status)) {
+ if (_.isFunction(options.success)) {
+ var propsMapping = _.invert(options.davProperties)
+ var results = parsePropFindResult(response.body, propsMapping)
+ if (options.depth > 0) {
+ // discard root entry
+ results.shift()
+ }
+
+ options.success(results)
+
+ }
+ } else if (_.isFunction(options.error)) {
+ options.error(response)
+ }
+ })
+}
+
+function callPropPatch(client, options, model, headers) {
+ return client.propPatch(
+ options.url,
+ convertModelAttributesToDavProperties(model.changed, options.davProperties),
+ headers
+ ).then(function(result) {
+ if (isSuccessStatus(result.status)) {
+ if (_.isFunction(options.success)) {
+ // pass the object's own values because the server
+ // does not return the updated model
+ options.success(model.toJSON())
+ }
+ } else if (_.isFunction(options.error)) {
+ options.error(result)
+ }
+ })
+
+}
+
+function callMkCol(client, options, model, headers) {
+ // call MKCOL without data, followed by PROPPATCH
+ return client.request(
+ options.type,
+ options.url,
+ headers,
+ null
+ ).then(function(result) {
+ if (!isSuccessStatus(result.status)) {
+ if (_.isFunction(options.error)) {
+ options.error(result)
+ }
+ return
+ }
+
+ callPropPatch(client, options, model, headers)
+ })
+}
+
+function callMethod(client, options, model, headers) {
+ headers['Content-Type'] = 'application/json'
+ return client.request(
+ options.type,
+ options.url,
+ headers,
+ options.data
+ ).then(function(result) {
+ if (!isSuccessStatus(result.status)) {
+ if (_.isFunction(options.error)) {
+ options.error(result)
+ }
+ return
+ }
+
+ if (_.isFunction(options.success)) {
+ if (options.type === 'PUT' || options.type === 'POST' || options.type === 'MKCOL') {
+ // pass the object's own values because the server
+ // does not return anything
+ var responseJson = result.body || model.toJSON()
+ var locationHeader = result.xhr.getResponseHeader('Content-Location')
+ if (options.type === 'POST' && locationHeader) {
+ responseJson.id = parseIdFromLocation(locationHeader)
+ }
+ options.success(responseJson)
+ return
+ }
+ // if multi-status, parse
+ if (result.status === 207) {
+ var propsMapping = _.invert(options.davProperties)
+ options.success(parsePropFindResult(result.body, propsMapping))
+ } else {
+ options.success(result.body)
+ }
+ }
+ })
+}
+
+export const davCall = (options, model) => {
+ var client = new dav.Client({
+ baseUrl: options.url,
+ xmlNamespaces: _.extend({
+ 'DAV:': 'd',
+ 'http://owncloud.org/ns': 'oc'
+ }, options.xmlNamespaces || {})
+ })
+ client.resolveUrl = function() {
+ return options.url
+ }
+ var headers = _.extend({
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'requesttoken': OC.requestToken
+ }, options.headers)
+ if (options.type === 'PROPFIND') {
+ return callPropFind(client, options, model, headers)
+ } else if (options.type === 'PROPPATCH') {
+ return callPropPatch(client, options, model, headers)
+ } else if (options.type === 'MKCOL') {
+ return callMkCol(client, options, model, headers)
+ } else {
+ return callMethod(client, options, model, headers)
+ }
+}
+
+/**
+ * DAV transport
+ */
+export const davSync = Backbone => (method, model, options) => {
+ var params = { type: methodMap[method] || method }
+ var isCollection = (model instanceof Backbone.Collection)
+
+ if (method === 'update') {
+ // if a model has an inner collection, it must define an
+ // attribute "hasInnerCollection" that evaluates to true
+ if (model.hasInnerCollection) {
+ // if the model itself is a Webdav collection, use MKCOL
+ params.type = 'MKCOL'
+ } else if (model.usePUT || (model.collection && model.collection.usePUT)) {
+ // use PUT instead of PROPPATCH
+ params.type = 'PUT'
+ }
+ }
+
+ // Ensure that we have a URL.
+ if (!options.url) {
+ params.url = _.result(model, 'url') || urlError()
+ }
+
+ // Ensure that we have the appropriate request data.
+ if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
+ params.data = JSON.stringify(options.attrs || model.toJSON(options))
+ }
+
+ // Don't process data on a non-GET request.
+ if (params.type !== 'PROPFIND') {
+ params.processData = false
+ }
+
+ if (params.type === 'PROPFIND' || params.type === 'PROPPATCH') {
+ var davProperties = model.davProperties
+ if (!davProperties && model.model) {
+ // use dav properties from model in case of collection
+ davProperties = model.model.prototype.davProperties
+ }
+ if (davProperties) {
+ if (_.isFunction(davProperties)) {
+ params.davProperties = davProperties.call(model)
+ } else {
+ params.davProperties = davProperties
+ }
+ }
+
+ params.davProperties = _.extend(params.davProperties || {}, options.davProperties)
+
+ if (_.isUndefined(options.depth)) {
+ if (isCollection) {
+ options.depth = 1
+ } else {
+ options.depth = 0
+ }
+ }
+ }
+
+ // Pass along `textStatus` and `errorThrown` from jQuery.
+ var error = options.error
+ options.error = function(xhr, textStatus, errorThrown) {
+ options.textStatus = textStatus
+ options.errorThrown = errorThrown
+ if (error) {
+ error.call(options.context, xhr, textStatus, errorThrown)
+ }
+ }
+
+ // Make the request, allowing the user to override any Ajax options.
+ var xhr = options.xhr = Backbone.davCall(_.extend(params, options), model)
+ model.trigger('request', model, xhr, options)
+ return xhr
+}
diff --git a/core/src/OC/backbone.js b/core/src/OC/backbone.js
new file mode 100644
index 00000000000..08520e278f6
--- /dev/null
+++ b/core/src/OC/backbone.js
@@ -0,0 +1,17 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import VendorBackbone from 'backbone'
+import { davCall, davSync } from './backbone-webdav.js'
+
+const Backbone = VendorBackbone.noConflict()
+
+// Patch Backbone for DAV
+Object.assign(Backbone, {
+ davCall,
+ davSync: davSync(Backbone),
+})
+
+export default Backbone
diff --git a/core/src/OC/capabilities.js b/core/src/OC/capabilities.js
new file mode 100644
index 00000000000..10623229625
--- /dev/null
+++ b/core/src/OC/capabilities.js
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getCapabilities as realGetCapabilities } from '@nextcloud/capabilities'
+
+/**
+ * Returns the capabilities
+ *
+ * @return {Array} capabilities
+ *
+ * @since 14.0.0
+ */
+export const getCapabilities = () => {
+ OC.debug && 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
new file mode 100644
index 00000000000..c47df61f6e6
--- /dev/null
+++ b/core/src/OC/config.js
@@ -0,0 +1,8 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+const config = window._oc_config || {}
+
+export default config
diff --git a/core/src/OC/constants.js b/core/src/OC/constants.js
new file mode 100644
index 00000000000..5298107e94d
--- /dev/null
+++ b/core/src/OC/constants.js
@@ -0,0 +1,15 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export const coreApps = ['', 'admin', 'log', 'core/search', 'core', '3rdparty']
+export const menuSpeed = 50
+export const PERMISSION_NONE = 0
+export const PERMISSION_CREATE = 4
+export const PERMISSION_READ = 1
+export const PERMISSION_UPDATE = 2
+export const PERMISSION_DELETE = 8
+export const PERMISSION_SHARE = 16
+export const PERMISSION_ALL = 31
+export const TAG_FAVORITE = '_$!<Favorite>!$_'
diff --git a/core/src/OC/currentuser.js b/core/src/OC/currentuser.js
new file mode 100644
index 00000000000..a022698eab0
--- /dev/null
+++ b/core/src/OC/currentuser.js
@@ -0,0 +1,20 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+const rawUid = document
+ .getElementsByTagName('head')[0]
+ .getAttribute('data-user')
+const displayName = document
+ .getElementsByTagName('head')[0]
+ .getAttribute('data-user-displayname')
+
+export const currentUser = rawUid !== undefined ? rawUid : false
+
+export const getCurrentUser = () => {
+ return {
+ uid: currentUser,
+ displayName,
+ }
+}
diff --git a/core/src/OC/debug.js b/core/src/OC/debug.js
new file mode 100644
index 00000000000..52a9ef28145
--- /dev/null
+++ b/core/src/OC/debug.js
@@ -0,0 +1,8 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+const base = window._oc_debug
+
+export const debug = base
diff --git a/core/src/OC/dialogs.js b/core/src/OC/dialogs.js
new file mode 100644
index 00000000000..5c6934e67a2
--- /dev/null
+++ b/core/src/OC/dialogs.js
@@ -0,0 +1,789 @@
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* eslint-disable */
+import _ from 'underscore'
+import $ from 'jquery'
+
+import IconMove from '@mdi/svg/svg/folder-move.svg?raw'
+import IconCopy from '@mdi/svg/svg/folder-multiple-outline.svg?raw'
+
+import OC from './index.js'
+import { DialogBuilder, FilePickerType, getFilePickerBuilder, spawnDialog } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
+import { basename } from 'path'
+import { defineAsyncComponent } from 'vue'
+
+/**
+ * this class to ease the usage of jquery dialogs
+ */
+const Dialogs = {
+ // dialog button types
+ /** @deprecated use `@nextcloud/dialogs` */
+ YES_NO_BUTTONS: 70,
+ /** @deprecated use `@nextcloud/dialogs` */
+ OK_BUTTONS: 71,
+
+ /** @deprecated use FilePickerType from `@nextcloud/dialogs` */
+ FILEPICKER_TYPE_CHOOSE: 1,
+ /** @deprecated use FilePickerType from `@nextcloud/dialogs` */
+ FILEPICKER_TYPE_MOVE: 2,
+ /** @deprecated use FilePickerType from `@nextcloud/dialogs` */
+ FILEPICKER_TYPE_COPY: 3,
+ /** @deprecated use FilePickerType from `@nextcloud/dialogs` */
+ FILEPICKER_TYPE_COPY_MOVE: 4,
+ /** @deprecated use FilePickerType from `@nextcloud/dialogs` */
+ FILEPICKER_TYPE_CUSTOM: 5,
+
+ /**
+ * displays alert dialog
+ * @param {string} text content of dialog
+ * @param {string} title dialog title
+ * @param {function} callback which will be triggered when user presses OK
+ * @param {boolean} [modal] make the dialog modal
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
+ */
+ alert: function(text, title, callback, modal) {
+ this.message(
+ text,
+ title,
+ 'alert',
+ Dialogs.OK_BUTTON,
+ callback,
+ modal
+ )
+ },
+
+ /**
+ * displays info dialog
+ * @param {string} text content of dialog
+ * @param {string} title dialog title
+ * @param {function} callback which will be triggered when user presses OK
+ * @param {boolean} [modal] make the dialog modal
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
+ */
+ info: function(text, title, callback, modal) {
+ this.message(text, title, 'info', Dialogs.OK_BUTTON, callback, modal)
+ },
+
+ /**
+ * displays confirmation dialog
+ * @param {string} text content of dialog
+ * @param {string} title dialog title
+ * @param {function} callback which will be triggered when user presses OK (true or false would be passed to callback respectively)
+ * @param {boolean} [modal] make the dialog modal
+ * @returns {Promise}
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
+ */
+ confirm: function(text, title, callback, modal) {
+ return this.message(
+ text,
+ title,
+ 'notice',
+ Dialogs.YES_NO_BUTTONS,
+ callback,
+ modal
+ )
+ },
+ /**
+ * displays confirmation dialog
+ * @param {string} text content of dialog
+ * @param {string} title dialog title
+ * @param {(number|{type: number, confirm: string, cancel: string, confirmClasses: string})} buttons text content of buttons
+ * @param {function} callback which will be triggered when user presses OK (true or false would be passed to callback respectively)
+ * @param {boolean} [modal] make the dialog modal
+ * @returns {Promise}
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
+ */
+ confirmDestructive: function(text, title, buttons = Dialogs.OK_BUTTONS, callback = () => {}, modal) {
+ return (new DialogBuilder())
+ .setName(title)
+ .setText(text)
+ .setButtons(
+ buttons === Dialogs.OK_BUTTONS
+ ? [
+ {
+ label: t('core', 'Yes'),
+ type: 'error',
+ callback: () => {
+ callback.clicked = true
+ callback(true)
+ },
+ }
+ ]
+ : Dialogs._getLegacyButtons(buttons, callback)
+ )
+ .build()
+ .show()
+ .then(() => {
+ if (!callback.clicked) {
+ callback(false)
+ }
+ })
+ },
+ /**
+ * displays confirmation dialog
+ * @param {string} text content of dialog
+ * @param {string} title dialog title
+ * @param {function} callback which will be triggered when user presses OK (true or false would be passed to callback respectively)
+ * @param {boolean} [modal] make the dialog modal
+ * @returns {Promise}
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
+ */
+ confirmHtml: function(text, title, callback, modal) {
+ return (new DialogBuilder())
+ .setName(title)
+ .setText('')
+ .setButtons([
+ {
+ label: t('core', 'No'),
+ callback: () => {},
+ },
+ {
+ label: t('core', 'Yes'),
+ type: 'primary',
+ callback: () => {
+ callback.clicked = true
+ callback(true)
+ },
+ },
+ ])
+ .build()
+ .setHTML(text)
+ .show()
+ .then(() => {
+ if (!callback.clicked) {
+ callback(false)
+ }
+ })
+ },
+ /**
+ * displays prompt dialog
+ * @param {string} text content of dialog
+ * @param {string} title dialog title
+ * @param {function} callback which will be triggered when user presses OK (true or false would be passed to callback respectively)
+ * @param {boolean} [modal] make the dialog modal
+ * @param {string} name name of the input field
+ * @param {boolean} password whether the input should be a password input
+ * @returns {Promise}
+ *
+ * @deprecated Use NcDialog from `@nextcloud/vue` instead
+ */
+ prompt: function(text, title, callback, modal, name, password) {
+ return new Promise((resolve) => {
+ spawnDialog(
+ defineAsyncComponent(() => import('../components/LegacyDialogPrompt.vue')),
+ {
+ text,
+ name: title,
+ callback,
+ inputName: name,
+ isPassword: !!password
+ },
+ (...args) => {
+ callback(...args)
+ resolve()
+ },
+ )
+ })
+ },
+
+ /**
+ * Legacy wrapper to the new Vue based filepicker from `@nextcloud/dialogs`
+ *
+ * Prefer to use the Vue filepicker directly instead.
+ *
+ * In order to pick several types of mime types they need to be passed as an
+ * array of strings.
+ *
+ * When no mime type filter is given only files can be selected. In order to
+ * be able to select both files and folders "['*', 'httpd/unix-directory']"
+ * should be used instead.
+ *
+ * @param {string} title dialog title
+ * @param {Function} callback which will be triggered when user presses Choose
+ * @param {boolean} [multiselect] whether it should be possible to select multiple files
+ * @param {string[]} [mimetype] mimetype to filter by - directories will always be included
+ * @param {boolean} [_modal] do not use
+ * @param {string} [type] Type of file picker : Choose, copy, move, copy and move
+ * @param {string} [path] path to the folder that the the file can be picket from
+ * @param {object} [options] additonal options that need to be set
+ * @param {Function} [options.filter] filter function for advanced filtering
+ * @param {boolean} [options.allowDirectoryChooser] Allow to select directories
+ * @deprecated since 27.1.0 use the filepicker from `@nextcloud/dialogs` instead
+ */
+ filepicker(title, callback, multiselect = false, mimetype = undefined, _modal = undefined, type = FilePickerType.Choose, path = undefined, options = undefined) {
+
+ /**
+ * Create legacy callback wrapper to support old filepicker syntax
+ * @param fn The original callback
+ * @param type The file picker type which was used to pick the file(s)
+ */
+ const legacyCallback = (fn, type) => {
+ const getPath = (node) => {
+ const root = node?.root || ''
+ let path = node?.path || ''
+ // TODO: Fix this in @nextcloud/files
+ if (path.startsWith(root)) {
+ path = path.slice(root.length) || '/'
+ }
+ return path
+ }
+
+ if (multiselect) {
+ return (nodes) => fn(nodes.map(getPath), type)
+ } else {
+ return (nodes) => fn(getPath(nodes[0]), type)
+ }
+ }
+
+ /**
+ * Coverting a Node into a legacy file info to support the OC.dialogs.filepicker filter function
+ * @param node The node to convert
+ */
+ const nodeToLegacyFile = (node) => ({
+ id: node.fileid || null,
+ path: node.path,
+ mimetype: node.mime || null,
+ mtime: node.mtime?.getTime() || null,
+ permissions: node.permissions,
+ name: node.attributes?.displayName || node.basename,
+ etag: node.attributes?.etag || null,
+ hasPreview: node.attributes?.hasPreview || null,
+ mountType: node.attributes?.mountType || null,
+ quotaAvailableBytes: node.attributes?.quotaAvailableBytes || null,
+ icon: null,
+ sharePermissions: null,
+ })
+
+ const builder = getFilePickerBuilder(title)
+
+ // Setup buttons
+ if (type === this.FILEPICKER_TYPE_CUSTOM) {
+ (options.buttons || []).forEach((button) => {
+ builder.addButton({
+ callback: legacyCallback(callback, button.type),
+ label: button.text,
+ type: button.defaultButton ? 'primary' : 'secondary',
+ })
+ })
+ } else {
+ builder.setButtonFactory((nodes, path) => {
+ const buttons = []
+ const [node] = nodes
+ const target = node?.displayname || node?.basename || basename(path)
+
+ if (type === FilePickerType.Choose) {
+ buttons.push({
+ callback: legacyCallback(callback, FilePickerType.Choose),
+ label: node && !this.multiSelect ? t('core', 'Choose {file}', { file: target }) : t('core', 'Choose'),
+ type: 'primary',
+ })
+ }
+ if (type === FilePickerType.CopyMove || type === FilePickerType.Copy) {
+ buttons.push({
+ callback: legacyCallback(callback, FilePickerType.Copy),
+ label: target ? t('core', 'Copy to {target}', { target }) : t('core', 'Copy'),
+ type: 'primary',
+ icon: IconCopy,
+ })
+ }
+ if (type === FilePickerType.Move || type === FilePickerType.CopyMove) {
+ buttons.push({
+ callback: legacyCallback(callback, FilePickerType.Move),
+ label: target ? t('core', 'Move to {target}', { target }) : t('core', 'Move'),
+ type: type === FilePickerType.Move ? 'primary' : 'secondary',
+ icon: IconMove,
+ })
+ }
+ return buttons
+ })
+ }
+
+ if (mimetype) {
+ builder.setMimeTypeFilter(typeof mimetype === 'string' ? [mimetype] : (mimetype || []))
+ }
+ if (typeof options?.filter === 'function') {
+ builder.setFilter((node) => options.filter(nodeToLegacyFile(node)))
+ }
+ builder.allowDirectories(options?.allowDirectoryChooser === true || mimetype?.includes('httpd/unix-directory') || false)
+ .setMultiSelect(multiselect)
+ .startAt(path)
+ .build()
+ .pick()
+ },
+
+ /**
+ * Displays raw dialog
+ * You better use a wrapper instead ...
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
+ */
+ message: function(content, title, dialogType, buttons, callback = () => {}, modal, allowHtml) {
+ const builder = (new DialogBuilder())
+ .setName(title)
+ .setText(allowHtml ? '' : content)
+ .setButtons(Dialogs._getLegacyButtons(buttons, callback))
+
+ switch (dialogType) {
+ case 'alert':
+ builder.setSeverity('warning')
+ break
+ case 'notice':
+ builder.setSeverity('info')
+ break
+ default:
+ break
+ }
+
+ const dialog = builder.build()
+
+ if (allowHtml) {
+ dialog.setHTML(content)
+ }
+
+ return dialog.show().then(() => {
+ if(!callback._clicked) {
+ callback(false)
+ }
+ })
+ },
+
+ /**
+ * Helper for legacy API
+ * @deprecated
+ */
+ _getLegacyButtons(buttons, callback) {
+ const buttonList = []
+
+ switch (typeof buttons === 'object' ? buttons.type : buttons) {
+ case Dialogs.YES_NO_BUTTONS:
+ buttonList.push({
+ label: buttons?.cancel ?? t('core', 'No'),
+ callback: () => {
+ callback._clicked = true
+ callback(false)
+ },
+ })
+ buttonList.push({
+ label: buttons?.confirm ?? t('core', 'Yes'),
+ type: 'primary',
+ callback: () => {
+ callback._clicked = true
+ callback(true)
+ },
+ })
+ break
+ case Dialogs.OK_BUTTONS:
+ buttonList.push({
+ label: buttons?.confirm ?? t('core', 'OK'),
+ type: 'primary',
+ callback: () => {
+ callback._clicked = true
+ callback(true)
+ },
+ })
+ break
+ default:
+ console.error('Invalid call to OC.dialogs')
+ break
+ }
+ return buttonList
+ },
+
+ _fileexistsshown: false,
+ /**
+ * Displays file exists dialog
+ * @param {object} data upload object
+ * @param {object} original file with name, size and mtime
+ * @param {object} replacement file with name, size and mtime
+ * @param {object} controller with onCancel, onSkip, onReplace and onRename methods
+ * @returns {Promise} jquery promise that resolves after the dialog template was loaded
+ *
+ * @deprecated 29.0.0 Use openConflictPicker from the @nextcloud/upload package instead
+ */
+ fileexists: function(data, original, replacement, controller) {
+ var self = this
+ var dialogDeferred = new $.Deferred()
+
+ var getCroppedPreview = function(file) {
+ var deferred = new $.Deferred()
+ // Only process image files.
+ var type = file.type && file.type.split('/').shift()
+ if (window.FileReader && type === 'image') {
+ var reader = new FileReader()
+ reader.onload = function(e) {
+ var blob = new Blob([e.target.result])
+ window.URL = window.URL || window.webkitURL
+ var originalUrl = window.URL.createObjectURL(blob)
+ var image = new Image()
+ image.src = originalUrl
+ image.onload = function() {
+ var url = crop(image)
+ deferred.resolve(url)
+ }
+ }
+ reader.readAsArrayBuffer(file)
+ } else {
+ deferred.reject()
+ }
+ return deferred
+ }
+
+ var crop = function(img) {
+ var canvas = document.createElement('canvas')
+ var targetSize = 96
+ var width = img.width
+ var height = img.height
+ var x; var y; var size
+
+ // Calculate the width and height, constraining the proportions
+ if (width > height) {
+ y = 0
+ x = (width - height) / 2
+ } else {
+ y = (height - width) / 2
+ x = 0
+ }
+ size = Math.min(width, height)
+
+ // Set canvas size to the cropped area
+ canvas.width = size
+ canvas.height = size
+ var ctx = canvas.getContext('2d')
+ ctx.drawImage(img, x, y, size, size, 0, 0, size, size)
+
+ // Resize the canvas to match the destination (right size uses 96px)
+ resampleHermite(canvas, size, size, targetSize, targetSize)
+
+ return canvas.toDataURL('image/png', 0.7)
+ }
+
+ /**
+ * Fast image resize/resample using Hermite filter with JavaScript.
+ *
+ * @author: ViliusL
+ *
+ * @param {*} canvas
+ * @param {number} W
+ * @param {number} H
+ * @param {number} W2
+ * @param {number} H2
+ */
+ var resampleHermite = function(canvas, W, H, W2, H2) {
+ W2 = Math.round(W2)
+ H2 = Math.round(H2)
+ var img = canvas.getContext('2d').getImageData(0, 0, W, H)
+ var img2 = canvas.getContext('2d').getImageData(0, 0, W2, H2)
+ var data = img.data
+ var data2 = img2.data
+ var ratio_w = W / W2
+ var ratio_h = H / H2
+ var ratio_w_half = Math.ceil(ratio_w / 2)
+ var ratio_h_half = Math.ceil(ratio_h / 2)
+
+ for (var j = 0; j < H2; j++) {
+ for (var i = 0; i < W2; i++) {
+ var x2 = (i + j * W2) * 4
+ var weight = 0
+ var weights = 0
+ var weights_alpha = 0
+ var gx_r = 0
+ var gx_g = 0
+ var gx_b = 0
+ var gx_a = 0
+ var center_y = (j + 0.5) * ratio_h
+ for (var yy = Math.floor(j * ratio_h); yy < (j + 1) * ratio_h; yy++) {
+ var dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half
+ var center_x = (i + 0.5) * ratio_w
+ var w0 = dy * dy // pre-calc part of w
+ for (var xx = Math.floor(i * ratio_w); xx < (i + 1) * ratio_w; xx++) {
+ var dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half
+ var w = Math.sqrt(w0 + dx * dx)
+ if (w >= -1 && w <= 1) {
+ // hermite filter
+ weight = 2 * w * w * w - 3 * w * w + 1
+ if (weight > 0) {
+ dx = 4 * (xx + yy * W)
+ // alpha
+ gx_a += weight * data[dx + 3]
+ weights_alpha += weight
+ // colors
+ if (data[dx + 3] < 255) { weight = weight * data[dx + 3] / 250 }
+ gx_r += weight * data[dx]
+ gx_g += weight * data[dx + 1]
+ gx_b += weight * data[dx + 2]
+ weights += weight
+ }
+ }
+ }
+ }
+ data2[x2] = gx_r / weights
+ data2[x2 + 1] = gx_g / weights
+ data2[x2 + 2] = gx_b / weights
+ data2[x2 + 3] = gx_a / weights_alpha
+ }
+ }
+ canvas.getContext('2d').clearRect(0, 0, Math.max(W, W2), Math.max(H, H2))
+ canvas.width = W2
+ canvas.height = H2
+ canvas.getContext('2d').putImageData(img2, 0, 0)
+ }
+
+ var addConflict = function($conflicts, original, replacement) {
+
+ var $conflict = $conflicts.find('.template').clone().removeClass('template').addClass('conflict')
+ var $originalDiv = $conflict.find('.original')
+ var $replacementDiv = $conflict.find('.replacement')
+
+ $conflict.data('data', data)
+
+ $conflict.find('.filename').text(original.name)
+ $originalDiv.find('.size').text(OC.Util.humanFileSize(original.size))
+ $originalDiv.find('.mtime').text(OC.Util.formatDate(original.mtime))
+ // ie sucks
+ if (replacement.size && replacement.lastModified) {
+ $replacementDiv.find('.size').text(OC.Util.humanFileSize(replacement.size))
+ $replacementDiv.find('.mtime').text(OC.Util.formatDate(replacement.lastModified))
+ }
+ var path = original.directory + '/' + original.name
+ var urlSpec = {
+ file: path,
+ x: 96,
+ y: 96,
+ c: original.etag,
+ forceIcon: 0
+ }
+ var previewpath = Files.generatePreviewUrl(urlSpec)
+ // Escaping single quotes
+ previewpath = previewpath.replace(/'/g, '%27')
+ $originalDiv.find('.icon').css({ 'background-image': "url('" + previewpath + "')" })
+ getCroppedPreview(replacement).then(
+ function(path) {
+ $replacementDiv.find('.icon').css('background-image', 'url(' + path + ')')
+ }, function() {
+ path = OC.MimeType.getIconUrl(replacement.type)
+ $replacementDiv.find('.icon').css('background-image', 'url(' + path + ')')
+ }
+ )
+ // connect checkboxes with labels
+ var checkboxId = $conflicts.find('.conflict').length
+ $originalDiv.find('input:checkbox').attr('id', 'checkbox_original_' + checkboxId)
+ $replacementDiv.find('input:checkbox').attr('id', 'checkbox_replacement_' + checkboxId)
+
+ $conflicts.append($conflict)
+
+ // set more recent mtime bold
+ // ie sucks
+ if (replacement.lastModified > original.mtime) {
+ $replacementDiv.find('.mtime').css('font-weight', 'bold')
+ } else if (replacement.lastModified < original.mtime) {
+ $originalDiv.find('.mtime').css('font-weight', 'bold')
+ } else {
+ // TODO add to same mtime collection?
+ }
+
+ // set bigger size bold
+ if (replacement.size && replacement.size > original.size) {
+ $replacementDiv.find('.size').css('font-weight', 'bold')
+ } else if (replacement.size && replacement.size < original.size) {
+ $originalDiv.find('.size').css('font-weight', 'bold')
+ } else {
+ // TODO add to same size collection?
+ }
+
+ // TODO show skip action for files with same size and mtime in bottom row
+
+ // always keep readonly files
+
+ if (original.status === 'readonly') {
+ $originalDiv
+ .addClass('readonly')
+ .find('input[type="checkbox"]')
+ .prop('checked', true)
+ .prop('disabled', true)
+ $originalDiv.find('.message')
+ .text(t('core', 'read-only'))
+ }
+ }
+ // var selection = controller.getSelection(data.originalFiles);
+ // if (selection.defaultAction) {
+ // controller[selection.defaultAction](data);
+ // } else {
+ var dialogName = 'oc-dialog-fileexists-content'
+ var dialogId = '#' + dialogName
+ if (this._fileexistsshown) {
+ // add conflict
+
+ var $conflicts = $(dialogId + ' .conflicts')
+ addConflict($conflicts, original, replacement)
+
+ var count = $(dialogId + ' .conflict').length
+ var title = n('core',
+ '{count} file conflict',
+ '{count} file conflicts',
+ count,
+ { count: count }
+ )
+ $(dialogId).parent().children('.oc-dialog-title').text(title)
+
+ // recalculate dimensions
+ $(window).trigger('resize')
+ dialogDeferred.resolve()
+ } else {
+ // create dialog
+ this._fileexistsshown = true
+ $.when(this._getFileExistsTemplate()).then(function($tmpl) {
+ var title = t('core', 'One file conflict')
+ var $dlg = $tmpl.octemplate({
+ dialog_name: dialogName,
+ title: title,
+ type: 'fileexists',
+
+ allnewfiles: t('core', 'New Files'),
+ allexistingfiles: t('core', 'Already existing files'),
+
+ why: t('core', 'Which files do you want to keep?'),
+ what: t('core', 'If you select both versions, the copied file will have a number added to its name.')
+ })
+ $('body').append($dlg)
+
+ if (original && replacement) {
+ var $conflicts = $dlg.find('.conflicts')
+ addConflict($conflicts, original, replacement)
+ }
+
+ var buttonlist = [{
+ text: t('core', 'Cancel'),
+ classes: 'cancel',
+ click: function() {
+ if (typeof controller.onCancel !== 'undefined') {
+ controller.onCancel(data)
+ }
+ $(dialogId).ocdialog('close')
+ }
+ },
+ {
+ text: t('core', 'Continue'),
+ classes: 'continue',
+ click: function() {
+ if (typeof controller.onContinue !== 'undefined') {
+ controller.onContinue($(dialogId + ' .conflict'))
+ }
+ $(dialogId).ocdialog('close')
+ }
+ }]
+
+ $(dialogId).ocdialog({
+ width: 500,
+ closeOnEscape: true,
+ modal: true,
+ buttons: buttonlist,
+ closeButton: null,
+ close: function() {
+ self._fileexistsshown = false
+ try {
+ $(this).ocdialog('destroy').remove()
+ } catch (e) {
+ // ignore
+ }
+ }
+ })
+
+ $(dialogId).css('height', 'auto')
+
+ var $primaryButton = $dlg.closest('.oc-dialog').find('button.continue')
+ $primaryButton.prop('disabled', true)
+
+ function updatePrimaryButton() {
+ var checkedCount = $dlg.find('.conflicts .checkbox:checked').length
+ $primaryButton.prop('disabled', checkedCount === 0)
+ }
+
+ // add checkbox toggling actions
+ $(dialogId).find('.allnewfiles').on('click', function() {
+ var $checkboxes = $(dialogId).find('.conflict .replacement input[type="checkbox"]')
+ $checkboxes.prop('checked', $(this).prop('checked'))
+ })
+ $(dialogId).find('.allexistingfiles').on('click', function() {
+ var $checkboxes = $(dialogId).find('.conflict .original:not(.readonly) input[type="checkbox"]')
+ $checkboxes.prop('checked', $(this).prop('checked'))
+ })
+ $(dialogId).find('.conflicts').on('click', '.replacement,.original:not(.readonly)', function() {
+ var $checkbox = $(this).find('input[type="checkbox"]')
+ $checkbox.prop('checked', !$checkbox.prop('checked'))
+ })
+ $(dialogId).find('.conflicts').on('click', '.replacement input[type="checkbox"],.original:not(.readonly) input[type="checkbox"]', function() {
+ var $checkbox = $(this)
+ $checkbox.prop('checked', !$checkbox.prop('checked'))
+ })
+
+ // update counters
+ $(dialogId).on('click', '.replacement,.allnewfiles', function() {
+ var count = $(dialogId).find('.conflict .replacement input[type="checkbox"]:checked').length
+ if (count === $(dialogId + ' .conflict').length) {
+ $(dialogId).find('.allnewfiles').prop('checked', true)
+ $(dialogId).find('.allnewfiles + .count').text(t('core', '(all selected)'))
+ } else if (count > 0) {
+ $(dialogId).find('.allnewfiles').prop('checked', false)
+ $(dialogId).find('.allnewfiles + .count').text(t('core', '({count} selected)', { count: count }))
+ } else {
+ $(dialogId).find('.allnewfiles').prop('checked', false)
+ $(dialogId).find('.allnewfiles + .count').text('')
+ }
+ updatePrimaryButton()
+ })
+ $(dialogId).on('click', '.original,.allexistingfiles', function() {
+ var count = $(dialogId).find('.conflict .original input[type="checkbox"]:checked').length
+ if (count === $(dialogId + ' .conflict').length) {
+ $(dialogId).find('.allexistingfiles').prop('checked', true)
+ $(dialogId).find('.allexistingfiles + .count').text(t('core', '(all selected)'))
+ } else if (count > 0) {
+ $(dialogId).find('.allexistingfiles').prop('checked', false)
+ $(dialogId).find('.allexistingfiles + .count')
+ .text(t('core', '({count} selected)', { count: count }))
+ } else {
+ $(dialogId).find('.allexistingfiles').prop('checked', false)
+ $(dialogId).find('.allexistingfiles + .count').text('')
+ }
+ updatePrimaryButton()
+ })
+
+ dialogDeferred.resolve()
+ })
+ .fail(function() {
+ dialogDeferred.reject()
+ alert(t('core', 'Error loading file exists template'))
+ })
+ }
+ // }
+ return dialogDeferred.promise()
+ },
+
+ _getFileExistsTemplate: function() {
+ var defer = $.Deferred()
+ if (!this.$fileexistsTemplate) {
+ var self = this
+ $.get(OC.filePath('core', 'templates/legacy', 'fileexists.html'), function(tmpl) {
+ self.$fileexistsTemplate = $(tmpl)
+ defer.resolve(self.$fileexistsTemplate)
+ })
+ .fail(function() {
+ defer.reject()
+ })
+ } else {
+ defer.resolve(this.$fileexistsTemplate)
+ }
+ return defer.promise()
+ },
+}
+
+export default Dialogs
diff --git a/core/src/OC/eventsource.js b/core/src/OC/eventsource.js
new file mode 100644
index 00000000000..090c351c057
--- /dev/null
+++ b/core/src/OC/eventsource.js
@@ -0,0 +1,145 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* eslint-disable */
+import $ from 'jquery'
+
+import { getRequestToken } from './requesttoken.ts'
+
+/**
+ * Create a new event source
+ * @param {string} src
+ * @param {object} [data] to be send as GET
+ *
+ * @constructs OCEventSource
+ */
+const OCEventSource = function(src, data) {
+ var dataStr = ''
+ var name
+ var joinChar
+ this.typelessListeners = []
+ this.closed = false
+ this.listeners = {}
+ if (data) {
+ for (name in data) {
+ dataStr += name + '=' + encodeURIComponent(data[name]) + '&'
+ }
+ }
+ dataStr += 'requesttoken=' + encodeURIComponent(getRequestToken())
+ if (!this.useFallBack && typeof EventSource !== 'undefined') {
+ joinChar = '&'
+ if (src.indexOf('?') === -1) {
+ joinChar = '?'
+ }
+ this.source = new EventSource(src + joinChar + dataStr)
+ this.source.onmessage = function(e) {
+ for (var i = 0; i < this.typelessListeners.length; i++) {
+ this.typelessListeners[i](JSON.parse(e.data))
+ }
+ }.bind(this)
+ } else {
+ var iframeId = 'oc_eventsource_iframe_' + OCEventSource.iframeCount
+ OCEventSource.fallBackSources[OCEventSource.iframeCount] = this
+ this.iframe = $('<iframe></iframe>')
+ this.iframe.attr('id', iframeId)
+ this.iframe.hide()
+
+ joinChar = '&'
+ if (src.indexOf('?') === -1) {
+ joinChar = '?'
+ }
+ this.iframe.attr('src', src + joinChar + 'fallback=true&fallback_id=' + OCEventSource.iframeCount + '&' + dataStr)
+ $('body').append(this.iframe)
+ this.useFallBack = true
+ OCEventSource.iframeCount++
+ }
+ // add close listener
+ this.listen('__internal__', function(data) {
+ if (data === 'close') {
+ this.close()
+ }
+ }.bind(this))
+}
+OCEventSource.fallBackSources = []
+OCEventSource.iframeCount = 0// number of fallback iframes
+OCEventSource.fallBackCallBack = function(id, type, data) {
+ OCEventSource.fallBackSources[id].fallBackCallBack(type, data)
+}
+OCEventSource.prototype = {
+ typelessListeners: [],
+ iframe: null,
+ listeners: {}, // only for fallback
+ useFallBack: false,
+ /**
+ * Fallback callback for browsers that don't have the
+ * native EventSource object.
+ *
+ * Calls the registered listeners.
+ *
+ * @private
+ * @param {String} type event type
+ * @param {Object} data received data
+ */
+ fallBackCallBack: function(type, data) {
+ var i
+ // ignore messages that might appear after closing
+ if (this.closed) {
+ return
+ }
+ if (type) {
+ if (typeof this.listeners.done !== 'undefined') {
+ for (i = 0; i < this.listeners[type].length; i++) {
+ this.listeners[type][i](data)
+ }
+ }
+ } else {
+ for (i = 0; i < this.typelessListeners.length; i++) {
+ this.typelessListeners[i](data)
+ }
+ }
+ },
+ lastLength: 0, // for fallback
+ /**
+ * Listen to a given type of events.
+ *
+ * @param {String} type event type
+ * @param {Function} callback event callback
+ */
+ listen: function(type, callback) {
+ if (callback && callback.call) {
+
+ if (type) {
+ if (this.useFallBack) {
+ if (!this.listeners[type]) {
+ this.listeners[type] = []
+ }
+ this.listeners[type].push(callback)
+ } else {
+ this.source.addEventListener(type, function(e) {
+ if (typeof e.data !== 'undefined') {
+ callback(JSON.parse(e.data))
+ } else {
+ callback('')
+ }
+ }, false)
+ }
+ } else {
+ this.typelessListeners.push(callback)
+ }
+ }
+ },
+ /**
+ * Closes this event source.
+ */
+ close: function() {
+ this.closed = true
+ if (typeof this.source !== 'undefined') {
+ this.source.close()
+ }
+ }
+}
+
+export default OCEventSource
diff --git a/core/src/OC/get_set.js b/core/src/OC/get_set.js
new file mode 100644
index 00000000000..0c909ad04fd
--- /dev/null
+++ b/core/src/OC/get_set.js
@@ -0,0 +1,38 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export const get = context => name => {
+ const namespaces = name.split('.')
+ const tail = namespaces.pop()
+
+ for (let i = 0; i < namespaces.length; i++) {
+ context = context[namespaces[i]]
+ if (!context) {
+ return false
+ }
+ }
+ return context[tail]
+}
+
+/**
+ * Set a variable by name
+ *
+ * @param {string} context context
+ * @return {Function} setter
+ * @deprecated 19.0.0 use https://lodash.com/docs#set
+ */
+export const set = context => (name, value) => {
+ const namespaces = name.split('.')
+ const tail = namespaces.pop()
+
+ for (let i = 0; i < namespaces.length; i++) {
+ if (!context[namespaces[i]]) {
+ context[namespaces[i]] = {}
+ }
+ context = context[namespaces[i]]
+ }
+ context[tail] = value
+ return value
+}
diff --git a/core/src/OC/host.js b/core/src/OC/host.js
new file mode 100644
index 00000000000..75c7d63804b
--- /dev/null
+++ b/core/src/OC/host.js
@@ -0,0 +1,42 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export const getProtocol = () => window.location.protocol.split(':')[0]
+
+/**
+ * Returns the host used to access this Nextcloud instance
+ * Host is sometimes the same as the hostname but now always.
+ *
+ * Examples:
+ * http://example.com => example.com
+ * https://example.com => example.com
+ * http://example.com:8080 => example.com:8080
+ *
+ * @return {string} host
+ *
+ * @since 8.2.0
+ * @deprecated 17.0.0 use window.location.host directly
+ */
+export const getHost = () => window.location.host
+
+/**
+ * Returns the hostname used to access this Nextcloud instance
+ * The hostname is always stripped of the port
+ *
+ * @return {string} hostname
+ * @since 9.0.0
+ * @deprecated 17.0.0 use window.location.hostname directly
+ */
+export const getHostName = () => window.location.hostname
+
+/**
+ * Returns the port number used to access this Nextcloud instance
+ *
+ * @return {number} port number
+ *
+ * @since 8.2.0
+ * @deprecated 17.0.0 use window.location.port directly
+ */
+export const getPort = () => window.location.port
diff --git a/core/src/OC/index.js b/core/src/OC/index.js
new file mode 100644
index 00000000000..5afc941b396
--- /dev/null
+++ b/core/src/OC/index.js
@@ -0,0 +1,294 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { subscribe } from '@nextcloud/event-bus'
+
+import {
+ ajaxConnectionLostHandler,
+ processAjaxError,
+ registerXHRForErrorProcessing,
+} from './xhr-error.js'
+import Apps from './apps.js'
+import { AppConfig, appConfig } from './appconfig.js'
+import appswebroots from './appswebroots.js'
+import Backbone from './backbone.js'
+import {
+ basename,
+ dirname,
+ encodePath,
+ isSamePath,
+ joinPaths,
+} from '@nextcloud/paths'
+import {
+ build as buildQueryString,
+ parse as parseQueryString,
+} from './query-string.js'
+import Config from './config.js'
+import {
+ coreApps,
+ menuSpeed,
+ PERMISSION_ALL,
+ PERMISSION_CREATE,
+ PERMISSION_DELETE,
+ PERMISSION_NONE,
+ PERMISSION_READ,
+ PERMISSION_SHARE,
+ PERMISSION_UPDATE,
+ TAG_FAVORITE,
+} from './constants.js'
+import { currentUser, getCurrentUser } from './currentuser.js'
+import Dialogs from './dialogs.js'
+import EventSource from './eventsource.js'
+import { get, set } from './get_set.js'
+import { getCapabilities } from './capabilities.js'
+import {
+ getHost,
+ getHostName,
+ getPort,
+ getProtocol,
+} from './host.js'
+import { getRequestToken } from './requesttoken.ts'
+import {
+ hideMenus,
+ registerMenu,
+ showMenu,
+ unregisterMenu,
+} from './menu.js'
+import { isUserAdmin } from './admin.js'
+import L10N from './l10n.js'
+import {
+ getCanonicalLocale,
+ getLanguage,
+ getLocale,
+} from '@nextcloud/l10n'
+
+import {
+ generateUrl,
+ generateFilePath,
+ generateOcsUrl,
+ generateRemoteUrl,
+ getRootUrl,
+ imagePath,
+ linkTo,
+} from '@nextcloud/router'
+
+import {
+ linkToRemoteBase,
+} from './routing.js'
+import msg from './msg.js'
+import Notification from './notification.js'
+import PasswordConfirmation from './password-confirmation.js'
+import Plugins from './plugins.js'
+import { theme } from './theme.js'
+import Util from './util.js'
+import { debug } from './debug.js'
+import { redirect, reload } from './navigation.js'
+import webroot from './webroot.js'
+
+/** @namespace OC */
+export default {
+ /*
+ * Constants
+ */
+ coreApps,
+ menuSpeed,
+ PERMISSION_ALL,
+ PERMISSION_CREATE,
+ PERMISSION_DELETE,
+ PERMISSION_NONE,
+ PERMISSION_READ,
+ PERMISSION_SHARE,
+ PERMISSION_UPDATE,
+ TAG_FAVORITE,
+
+ /*
+ * Deprecated helpers to be removed
+ */
+ /**
+ * Check if a user file is allowed to be handled.
+ *
+ * @param {string} file to check
+ * @return {boolean}
+ * @deprecated 17.0.0
+ */
+ fileIsBlacklisted: file => !!(file.match(Config.blacklist_files_regex)),
+ Apps,
+ AppConfig,
+ appConfig,
+ appswebroots,
+ Backbone,
+ config: Config,
+ /**
+ * Currently logged in user or null if none
+ *
+ * @type {string}
+ * @deprecated use `getCurrentUser` from https://www.npmjs.com/package/@nextcloud/auth
+ */
+ currentUser,
+ dialogs: Dialogs,
+ EventSource,
+ /**
+ * Returns the currently logged in user or null if there is no logged in
+ * user (public page mode)
+ *
+ * @since 9.0.0
+ * @deprecated 19.0.0 use `getCurrentUser` from https://www.npmjs.com/package/@nextcloud/auth
+ */
+ getCurrentUser,
+ isUserAdmin,
+ L10N,
+
+ /**
+ * Ajax error handlers
+ *
+ * @todo remove from here and keep internally -> requires new tests
+ */
+ _ajaxConnectionLostHandler: ajaxConnectionLostHandler,
+ _processAjaxError: processAjaxError,
+ registerXHRForErrorProcessing,
+
+ /**
+ * Capabilities
+ *
+ * @type {Array}
+ * @deprecated 20.0.0 use @nextcloud/capabilities instead
+ */
+ getCapabilities,
+
+ /*
+ * Legacy menu helpers
+ */
+ hideMenus,
+ registerMenu,
+ showMenu,
+ unregisterMenu,
+
+ /*
+ * Path helpers
+ */
+ /**
+ * @deprecated 18.0.0 use https://www.npmjs.com/package/@nextcloud/paths
+ */
+ basename,
+ /**
+ * @deprecated 18.0.0 use https://www.npmjs.com/package/@nextcloud/paths
+ */
+ encodePath,
+ /**
+ * @deprecated 18.0.0 use https://www.npmjs.com/package/@nextcloud/paths
+ */
+ dirname,
+ /**
+ * @deprecated 18.0.0 use https://www.npmjs.com/package/@nextcloud/paths
+ */
+ isSamePath,
+ /**
+ * @deprecated 18.0.0 use https://www.npmjs.com/package/@nextcloud/paths
+ */
+ joinPaths,
+
+ /**
+ * Host (url) helpers
+ */
+ getHost,
+ getHostName,
+ getPort,
+ getProtocol,
+
+ /**
+ * @deprecated 20.0.0 use `getCanonicalLocale` from https://www.npmjs.com/package/@nextcloud/l10n
+ */
+ getCanonicalLocale,
+ /**
+ * @deprecated 26.0.0 use `getLocale` from https://www.npmjs.com/package/@nextcloud/l10n
+ */
+ getLocale,
+ /**
+ * @deprecated 26.0.0 use `getLanguage` from https://www.npmjs.com/package/@nextcloud/l10n
+ */
+ getLanguage,
+
+ /**
+ * Query string helpers
+ */
+ buildQueryString,
+ parseQueryString,
+
+ msg,
+ Notification,
+ /**
+ * @deprecated 28.0.0 use methods from '@nextcloud/password-confirmation'
+ */
+ PasswordConfirmation,
+ Plugins,
+ theme,
+ Util,
+ debug,
+ /**
+ * @deprecated 19.0.0 use `generateFilePath` from https://www.npmjs.com/package/@nextcloud/router
+ */
+ filePath: generateFilePath,
+ /**
+ * @deprecated 19.0.0 use `generateUrl` from https://www.npmjs.com/package/@nextcloud/router
+ */
+ generateUrl,
+ /**
+ * @deprecated 19.0.0 use https://lodash.com/docs#get
+ */
+ get: get(window),
+ /**
+ * @deprecated 19.0.0 use https://lodash.com/docs#set
+ */
+ set: set(window),
+ /**
+ * @deprecated 19.0.0 use `getRootUrl` from https://www.npmjs.com/package/@nextcloud/router
+ */
+ getRootPath: getRootUrl,
+ /**
+ * @deprecated 19.0.0 use `imagePath` from https://www.npmjs.com/package/@nextcloud/router
+ */
+ imagePath,
+ redirect,
+ reload,
+ requestToken: getRequestToken(),
+ /**
+ * @deprecated 19.0.0 use `linkTo` from https://www.npmjs.com/package/@nextcloud/router
+ */
+ linkTo,
+ /**
+ * @param {string} service service name
+ * @param {number} version OCS API version
+ * @return {string} OCS API base path
+ * @deprecated 19.0.0 use `generateOcsUrl` from https://www.npmjs.com/package/@nextcloud/router
+ */
+ linkToOCS: (service, version) => {
+ return generateOcsUrl(service, {}, {
+ ocsVersion: version || 1,
+ }) + '/'
+ },
+ /**
+ * @deprecated 19.0.0 use `generateRemoteUrl` from https://www.npmjs.com/package/@nextcloud/router
+ */
+ linkToRemote: generateRemoteUrl,
+ linkToRemoteBase,
+ /**
+ * Relative path to Nextcloud root.
+ * For example: "/nextcloud"
+ *
+ * @type {string}
+ *
+ * @deprecated 19.0.0 use `getRootUrl` from https://www.npmjs.com/package/@nextcloud/router
+ * @see OC#getRootPath
+ */
+ webroot,
+}
+
+// Keep the request token prop in sync
+subscribe('csrf-token-update', e => {
+ OC.requestToken = e.token
+
+ // Logging might help debug (Sentry) issues
+ console.info('OC.requestToken changed', e.token)
+})
diff --git a/core/src/OC/l10n.js b/core/src/OC/l10n.js
new file mode 100644
index 00000000000..02f912d6a99
--- /dev/null
+++ b/core/src/OC/l10n.js
@@ -0,0 +1,90 @@
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2014 ownCloud, Inc.
+ * SPDX-FileCopyrightText: 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import Handlebars from 'handlebars'
+import {
+ loadTranslations,
+ translate,
+ translatePlural,
+ register,
+ unregister,
+} from '@nextcloud/l10n'
+
+/**
+ * L10N namespace with localization functions.
+ *
+ * @namespace OC.L10n
+ * @deprecated 26.0.0 use https://www.npmjs.com/package/@nextcloud/l10n
+ */
+const L10n = {
+
+ /**
+ * Load an app's translation bundle if not loaded already.
+ *
+ * @deprecated 26.0.0 use `loadTranslations` from https://www.npmjs.com/package/@nextcloud/l10n
+ *
+ * @param {string} appName name of the app
+ * @param {Function} callback callback to be called when
+ * the translations are loaded
+ * @return {Promise} promise
+ */
+ load: loadTranslations,
+
+ /**
+ * Register an app's translation bundle.
+ *
+ * @deprecated 26.0.0 use `register` from https://www.npmjs.com/package/@nextcloud/l10
+ *
+ * @param {string} appName name of the app
+ * @param {Record<string, string>} bundle bundle
+ */
+ register,
+
+ /**
+ * @private
+ * @deprecated 26.0.0 use `unregister` from https://www.npmjs.com/package/@nextcloud/l10n
+ */
+ _unregister: unregister,
+
+ /**
+ * Translate a string
+ *
+ * @deprecated 26.0.0 use `translate` from https://www.npmjs.com/package/@nextcloud/l10n
+ *
+ * @param {string} app the id of the app for which to translate the string
+ * @param {string} text the string to translate
+ * @param {object} [vars] map of placeholder key to value
+ * @param {number} [count] number to replace %n with
+ * @param {Array} [options] options array
+ * @param {boolean} [options.escape=true] enable/disable auto escape of placeholders (by default enabled)
+ * @param {boolean} [options.sanitize=true] enable/disable sanitization (by default enabled)
+ * @return {string}
+ */
+ translate,
+
+ /**
+ * Translate a plural string
+ *
+ * @deprecated 26.0.0 use `translatePlural` from https://www.npmjs.com/package/@nextcloud/l10n
+ *
+ * @param {string} app the id of the app for which to translate the string
+ * @param {string} textSingular the string to translate for exactly one object
+ * @param {string} textPlural the string to translate for n objects
+ * @param {number} count number to determine whether to use singular or plural
+ * @param {object} [vars] map of placeholder key to value
+ * @param {Array} [options] options array
+ * @param {boolean} [options.escape=true] enable/disable auto escape of placeholders (by default enabled)
+ * @return {string} Translated string
+ */
+ translatePlural,
+}
+
+export default L10n
+
+Handlebars.registerHelper('t', function(app, text) {
+ return translate(app, text)
+})
diff --git a/core/src/OC/menu.js b/core/src/OC/menu.js
new file mode 100644
index 00000000000..4b4eb658592
--- /dev/null
+++ b/core/src/OC/menu.js
@@ -0,0 +1,125 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import _ from 'underscore'
+/** @typedef {import('jquery')} jQuery */
+import $ from 'jquery'
+
+import { menuSpeed } from './constants.js'
+
+export let currentMenu = null
+export let currentMenuToggle = null
+
+/**
+ * For menu toggling
+ *
+ * @param {jQuery} $toggle the toggle element
+ * @param {jQuery} $menuEl the menu container element
+ * @param {Function | undefined} toggle callback invoked everytime the menu is opened
+ * @param {boolean} headerMenu is this a top right header menu?
+ * @return {void}
+ */
+export const registerMenu = function($toggle, $menuEl, toggle, headerMenu) {
+ $menuEl.addClass('menu')
+ const isClickableElement = $toggle.prop('tagName') === 'A' || $toggle.prop('tagName') === 'BUTTON'
+
+ // On link and button, the enter key trigger a click event
+ // Only use the click to avoid two fired events
+ $toggle.on(isClickableElement ? 'click.menu' : 'click.menu keyup.menu', function(event) {
+ // prevent the link event (append anchor to URL)
+ event.preventDefault()
+
+ // allow enter key as a trigger
+ if (event.key && event.key !== 'Enter') {
+ return
+ }
+
+ if ($menuEl.is(currentMenu)) {
+ hideMenus()
+ return
+ } else if (currentMenu) {
+ // another menu was open?
+ // close it
+ hideMenus()
+ }
+
+ if (headerMenu === true) {
+ $menuEl.parent().addClass('openedMenu')
+ }
+
+ // Set menu to expanded
+ $toggle.attr('aria-expanded', true)
+
+ $menuEl.slideToggle(menuSpeed, toggle)
+ currentMenu = $menuEl
+ currentMenuToggle = $toggle
+ })
+}
+
+/**
+ * Unregister a previously registered menu
+ *
+ * @param {jQuery} $toggle the toggle element
+ * @param {jQuery} $menuEl the menu container element
+ */
+export const unregisterMenu = ($toggle, $menuEl) => {
+ // close menu if opened
+ if ($menuEl.is(currentMenu)) {
+ hideMenus()
+ }
+ $toggle.off('click.menu').removeClass('menutoggle')
+ $menuEl.removeClass('menu')
+}
+
+/**
+ * Hides any open menus
+ *
+ * @param {Function} complete callback when the hiding animation is done
+ */
+export const hideMenus = function(complete) {
+ if (currentMenu) {
+ const lastMenu = currentMenu
+ currentMenu.trigger(new $.Event('beforeHide'))
+ currentMenu.slideUp(menuSpeed, function() {
+ lastMenu.trigger(new $.Event('afterHide'))
+ if (complete) {
+ complete.apply(this, arguments)
+ }
+ })
+ }
+
+ // Set menu to closed
+ $('.menutoggle').attr('aria-expanded', false)
+ if (currentMenuToggle) {
+ currentMenuToggle.attr('aria-expanded', false)
+ }
+
+ $('.openedMenu').removeClass('openedMenu')
+ currentMenu = null
+ currentMenuToggle = null
+}
+
+/**
+ * Shows a given element as menu
+ *
+ * @param {object} [$toggle] menu toggle
+ * @param {object} $menuEl menu element
+ * @param {Function} complete callback when the showing animation is done
+ */
+export const showMenu = ($toggle, $menuEl, complete) => {
+ if ($menuEl.is(currentMenu)) {
+ return
+ }
+ hideMenus()
+ currentMenu = $menuEl
+ currentMenuToggle = $toggle
+ $menuEl.trigger(new $.Event('beforeShow'))
+ $menuEl.show()
+ $menuEl.trigger(new $.Event('afterShow'))
+ // no animation
+ if (_.isFunction(complete)) {
+ complete()
+ }
+}
diff --git a/core/src/OC/msg.js b/core/src/OC/msg.js
new file mode 100644
index 00000000000..655631a03ff
--- /dev/null
+++ b/core/src/OC/msg.js
@@ -0,0 +1,99 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import $ from 'jquery'
+
+/**
+ * A little class to manage a status field for a "saving" process.
+ * It can be used to display a starting message (e.g. "Saving...") and then
+ * replace it with a green success message or a red error message.
+ *
+ * @namespace OC.msg
+ */
+export default {
+ /**
+ * Displayes a "Saving..." message in the given message placeholder
+ *
+ * @param {object} selector Placeholder to display the message in
+ */
+ startSaving(selector) {
+ this.startAction(selector, t('core', 'Saving …'))
+ },
+
+ /**
+ * Displayes a custom message in the given message placeholder
+ *
+ * @param {object} selector Placeholder to display the message in
+ * @param {string} message Plain text message to display (no HTML allowed)
+ */
+ startAction(selector, message) {
+ $(selector).text(message)
+ .removeClass('success')
+ .removeClass('error')
+ .stop(true, true)
+ .show()
+ },
+
+ /**
+ * Displayes an success/error message in the given selector
+ *
+ * @param {object} selector Placeholder to display the message in
+ * @param {object} response Response of the server
+ * @param {object} response.data Data of the servers response
+ * @param {string} response.data.message Plain text message to display (no HTML allowed)
+ * @param {string} response.status is being used to decide whether the message
+ * is displayed as an error/success
+ */
+ finishedSaving(selector, response) {
+ this.finishedAction(selector, response)
+ },
+
+ /**
+ * Displayes an success/error message in the given selector
+ *
+ * @param {object} selector Placeholder to display the message in
+ * @param {object} response Response of the server
+ * @param {object} response.data Data of the servers response
+ * @param {string} response.data.message Plain text message to display (no HTML allowed)
+ * @param {string} response.status is being used to decide whether the message
+ * is displayed as an error/success
+ */
+ finishedAction(selector, response) {
+ if (response.status === 'success') {
+ this.finishedSuccess(selector, response.data.message)
+ } else {
+ this.finishedError(selector, response.data.message)
+ }
+ },
+
+ /**
+ * Displayes an success message in the given selector
+ *
+ * @param {object} selector Placeholder to display the message in
+ * @param {string} message Plain text success message to display (no HTML allowed)
+ */
+ finishedSuccess(selector, message) {
+ $(selector).text(message)
+ .addClass('success')
+ .removeClass('error')
+ .stop(true, true)
+ .delay(3000)
+ .fadeOut(900)
+ .show()
+ },
+
+ /**
+ * Displayes an error message in the given selector
+ *
+ * @param {object} selector Placeholder to display the message in
+ * @param {string} message Plain text error message to display (no HTML allowed)
+ */
+ finishedError(selector, message) {
+ $(selector).text(message)
+ .addClass('error')
+ .removeClass('success')
+ .show()
+ },
+}
diff --git a/core/src/OC/navigation.js b/core/src/OC/navigation.js
new file mode 100644
index 00000000000..b279b9a60f3
--- /dev/null
+++ b/core/src/OC/navigation.js
@@ -0,0 +1,13 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export const redirect = targetURL => { window.location = targetURL }
+
+/**
+ * Reloads the current page
+ *
+ * @deprecated 17.0.0 use window.location.reload directly
+ */
+export const reload = () => { window.location.reload() }
diff --git a/core/src/OC/notification.js b/core/src/OC/notification.js
new file mode 100644
index 00000000000..b658f4163bb
--- /dev/null
+++ b/core/src/OC/notification.js
@@ -0,0 +1,164 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import _ from 'underscore'
+/** @typedef {import('jquery')} jQuery */
+import $ from 'jquery'
+import { showMessage, TOAST_DEFAULT_TIMEOUT, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
+
+/**
+ * @todo Write documentation
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package instead
+ * @namespace OC.Notification
+ */
+export default {
+
+ updatableNotification: null,
+
+ getDefaultNotificationFunction: null,
+
+ /**
+ * @param {Function} callback callback function
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
+ */
+ setDefault(callback) {
+ this.getDefaultNotificationFunction = callback
+ },
+
+ /**
+ * Hides a notification.
+ *
+ * If a row is given, only hide that one.
+ * If no row is given, hide all notifications.
+ *
+ * @param {jQuery} [$row] notification row
+ * @param {Function} [callback] callback
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
+ */
+ hide($row, callback) {
+ if (_.isFunction($row)) {
+ // first arg is the callback
+ callback = $row
+ $row = undefined
+ }
+
+ if (!$row) {
+ console.error('Missing argument $row in OC.Notification.hide() call, caller needs to be adjusted to only dismiss its own notification')
+ return
+ }
+
+ // remove the row directly
+ $row.each(function() {
+ 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
+ }
+ })
+ if (callback) {
+ callback.call()
+ }
+ if (this.getDefaultNotificationFunction) {
+ this.getDefaultNotificationFunction()
+ }
+ },
+
+ /**
+ * Shows a notification as HTML without being sanitized before.
+ * If you pass unsanitized user input this may lead to a XSS vulnerability.
+ * Consider using show() instead of showHTML()
+ *
+ * @param {string} html Message to display
+ * @param {object} [options] options
+ * @param {string} [options.type] notification type
+ * @param {number} [options.timeout] timeout value, defaults to 0 (permanent)
+ * @return {jQuery} jQuery element for notification row
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
+ */
+ showHtml(html, options) {
+ options = options || {}
+ options.isHTML = true
+ options.timeout = (!options.timeout) ? TOAST_PERMANENT_TIMEOUT : options.timeout
+ const toast = showMessage(html, options)
+ toast.toastElement.toastify = toast
+ return $(toast.toastElement)
+ },
+
+ /**
+ * Shows a sanitized notification
+ *
+ * @param {string} text Message to display
+ * @param {object} [options] options
+ * @param {string} [options.type] notification type
+ * @param {number} [options.timeout] timeout value, defaults to 0 (permanent)
+ * @return {jQuery} jQuery element for notification row
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
+ */
+ 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) ? TOAST_PERMANENT_TIMEOUT : options.timeout
+ const toast = showMessage(escapeHTML(text), options)
+ toast.toastElement.toastify = toast
+ return $(toast.toastElement)
+ },
+
+ /**
+ * Updates (replaces) a sanitized notification.
+ *
+ * @param {string} text Message to display
+ * @return {jQuery} JQuery element for notification row
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
+ */
+ showUpdate(text) {
+ if (this.updatableNotification) {
+ this.updatableNotification.hideToast()
+ }
+ this.updatableNotification = showMessage(text, { timeout: TOAST_PERMANENT_TIMEOUT })
+ this.updatableNotification.toastElement.toastify = this.updatableNotification
+ return $(this.updatableNotification.toastElement)
+ },
+
+ /**
+ * Shows a notification that disappears after x seconds, default is
+ * 7 seconds
+ *
+ * @param {string} text Message to show
+ * @param {Array} [options] options array
+ * @param {number} [options.timeout] timeout in seconds, if this is 0 it will show the message permanently
+ * @param {boolean} [options.isHTML] an indicator for HTML notifications (true) or text (false)
+ * @param {string} [options.type] notification type
+ * @return {jQuery} the toast element
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
+ */
+ showTemporary(text, options) {
+ options = 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.
+ *
+ * @return {boolean}
+ * @deprecated 17.0.0 use the `@nextcloud/dialogs` package
+ */
+ isHidden() {
+ return !$('#content').find('.toastify').length
+ },
+}
diff --git a/core/src/OC/password-confirmation.js b/core/src/OC/password-confirmation.js
new file mode 100644
index 00000000000..621f7a0695f
--- /dev/null
+++ b/core/src/OC/password-confirmation.js
@@ -0,0 +1,26 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { confirmPassword, isPasswordConfirmationRequired } from '@nextcloud/password-confirmation'
+import '@nextcloud/password-confirmation/dist/style.css'
+
+/**
+ * @namespace OC.PasswordConfirmation
+ */
+export default {
+
+ requiresPasswordConfirmation() {
+ return isPasswordConfirmationRequired()
+ },
+
+ /**
+ * @param {Function} callback success callback function
+ * @param {object} options options currently not used by confirmPassword
+ * @param {Function} rejectCallback error callback function
+ */
+ requirePasswordConfirmation(callback, options, rejectCallback) {
+ confirmPassword().then(callback, rejectCallback)
+ },
+}
diff --git a/core/src/OC/plugins.js b/core/src/OC/plugins.js
new file mode 100644
index 00000000000..8212fc0b4ee
--- /dev/null
+++ b/core/src/OC/plugins.js
@@ -0,0 +1,70 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export default {
+
+ /**
+ * @type {Array.<OC.Plugin>}
+ */
+ _plugins: {},
+
+ /**
+ * Register plugin
+ *
+ * @param {string} targetName app name / class name to hook into
+ * @param {OC.Plugin} plugin plugin
+ */
+ register(targetName, plugin) {
+ let plugins = this._plugins[targetName]
+ if (!plugins) {
+ plugins = this._plugins[targetName] = []
+ }
+ plugins.push(plugin)
+ },
+
+ /**
+ * Returns all plugin registered to the given target
+ * name / app name / class name.
+ *
+ * @param {string} targetName app name / class name to hook into
+ * @return {Array.<OC.Plugin>} array of plugins
+ */
+ getPlugins(targetName) {
+ return this._plugins[targetName] || []
+ },
+
+ /**
+ * Call attach() on all plugins registered to the given target name.
+ *
+ * @param {string} targetName app name / class name
+ * @param {object} targetObject to be extended
+ * @param {object} [options] options
+ */
+ attach(targetName, targetObject, options) {
+ const plugins = this.getPlugins(targetName)
+ for (let i = 0; i < plugins.length; i++) {
+ if (plugins[i].attach) {
+ plugins[i].attach(targetObject, options)
+ }
+ }
+ },
+
+ /**
+ * Call detach() on all plugins registered to the given target name.
+ *
+ * @param {string} targetName app name / class name
+ * @param {object} targetObject to be extended
+ * @param {object} [options] options
+ */
+ detach(targetName, targetObject, options) {
+ const plugins = this.getPlugins(targetName)
+ for (let i = 0; i < plugins.length; i++) {
+ if (plugins[i].detach) {
+ plugins[i].detach(targetObject, options)
+ }
+ }
+ },
+
+}
diff --git a/core/src/OC/query-string.js b/core/src/OC/query-string.js
new file mode 100644
index 00000000000..df0f366133a
--- /dev/null
+++ b/core/src/OC/query-string.js
@@ -0,0 +1,75 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import $ from 'jquery'
+
+/**
+ * Parses a URL query string into a JS map
+ *
+ * @param {string} queryString query string in the format param1=1234&param2=abcde&param3=xyz
+ * @return {Record<string, string>} map containing key/values matching the URL parameters
+ */
+export const parse = queryString => {
+ let pos
+ let components
+ const result = {}
+ let key
+ if (!queryString) {
+ return null
+ }
+ pos = queryString.indexOf('?')
+ if (pos >= 0) {
+ queryString = queryString.substr(pos + 1)
+ }
+ const parts = queryString.replace(/\+/g, '%20').split('&')
+ for (let i = 0; i < parts.length; i++) {
+ // split on first equal sign
+ const part = parts[i]
+ pos = part.indexOf('=')
+ if (pos >= 0) {
+ components = [
+ part.substr(0, pos),
+ part.substr(pos + 1),
+ ]
+ } else {
+ // key only
+ components = [part]
+ }
+ if (!components.length) {
+ continue
+ }
+ key = decodeURIComponent(components[0])
+ if (!key) {
+ continue
+ }
+ // if equal sign was there, return string
+ if (components.length > 1) {
+ result[key] = decodeURIComponent(components[1])
+ } else {
+ // no equal sign => null value
+ result[key] = null
+ }
+ }
+ return result
+}
+
+/**
+ * Builds a URL query from a JS map.
+ *
+ * @param {Record<string, string>} params map containing key/values matching the URL parameters
+ * @return {string} String containing a URL query (without question) mark
+ */
+export const build = params => {
+ if (!params) {
+ return ''
+ }
+ return $.map(params, function(value, key) {
+ let s = encodeURIComponent(key)
+ if (value !== null && typeof (value) !== 'undefined') {
+ s += '=' + encodeURIComponent(value)
+ }
+ return s
+ }).join('&')
+}
diff --git a/core/src/OC/requesttoken.ts b/core/src/OC/requesttoken.ts
new file mode 100644
index 00000000000..8ecf0b3de7e
--- /dev/null
+++ b/core/src/OC/requesttoken.ts
@@ -0,0 +1,49 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { emit } from '@nextcloud/event-bus'
+import { generateUrl } from '@nextcloud/router'
+
+/**
+ * Get the current CSRF token.
+ */
+export function getRequestToken(): string {
+ return document.head.dataset.requesttoken!
+}
+
+/**
+ * Set a new CSRF token (e.g. because of session refresh).
+ * This also emits an event bus event for the updated token.
+ *
+ * @param token - The new token
+ * @fires Error - If the passed token is not a potential valid token
+ */
+export function setRequestToken(token: string): void {
+ if (!token || typeof token !== 'string') {
+ throw new Error('Invalid CSRF token given', { cause: { token } })
+ }
+
+ document.head.dataset.requesttoken = token
+ emit('csrf-token-update', { token })
+}
+
+/**
+ * Fetch the request token from the API.
+ * This does also set it on the current context, see `setRequestToken`.
+ *
+ * @fires Error - If the request failed
+ */
+export async function fetchRequestToken(): Promise<string> {
+ const url = generateUrl('/csrftoken')
+
+ const response = await fetch(url)
+ if (!response.ok) {
+ throw new Error('Could not fetch CSRF token from API', { cause: response })
+ }
+
+ const { token } = await response.json()
+ setRequestToken(token)
+ return token
+}
diff --git a/core/src/OC/routing.js b/core/src/OC/routing.js
new file mode 100644
index 00000000000..4b81714d6f0
--- /dev/null
+++ b/core/src/OC/routing.js
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {
+ getRootUrl as realGetRootUrl,
+} from '@nextcloud/router'
+
+/**
+ * Creates a relative url for remote use
+ *
+ * @param {string} service id
+ * @return {string} the url
+ */
+export const linkToRemoteBase = service => {
+ return realGetRootUrl() + '/remote.php/' + service
+}
diff --git a/core/src/OC/theme.js b/core/src/OC/theme.js
new file mode 100644
index 00000000000..af45c37de7e
--- /dev/null
+++ b/core/src/OC/theme.js
@@ -0,0 +1,6 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export const theme = window._theme || {}
diff --git a/core/src/OC/util-history.js b/core/src/OC/util-history.js
new file mode 100644
index 00000000000..7ecd0e098c6
--- /dev/null
+++ b/core/src/OC/util-history.js
@@ -0,0 +1,168 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import _ from 'underscore'
+import OC from './index.js'
+
+/**
+ * Utility class for the history API,
+ * includes fallback to using the URL hash when
+ * the browser doesn't support the history API.
+ *
+ * @namespace OC.Util.History
+ */
+export default {
+
+ _handlers: [],
+
+ /**
+ * Push the current URL parameters to the history stack
+ * and change the visible URL.
+ * Note: this includes a workaround for IE8/IE9 that uses
+ * the hash part instead of the search part.
+ *
+ * @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
+ * @param {boolean} [replace] whether to replace instead of pushing
+ */
+ _pushState(params, url, replace) {
+ let strParams
+ if (typeof (params) === 'string') {
+ strParams = params
+ } else {
+ strParams = OC.buildQueryString(params)
+ }
+
+ if (window.history.pushState) {
+ url = url || location.pathname + '?' + strParams
+ // Workaround for bug with SVG and window.history.pushState on Firefox < 51
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=652991
+ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
+ if (isFirefox && parseInt(navigator.userAgent.split('/').pop()) < 51) {
+ const patterns = document.querySelectorAll('[fill^="url(#"], [stroke^="url(#"], [filter^="url(#invert"]')
+ for (let i = 0, ii = patterns.length, pattern; i < ii; i++) {
+ pattern = patterns[i]
+ // eslint-disable-next-line no-self-assign
+ pattern.style.fill = pattern.style.fill
+ // eslint-disable-next-line no-self-assign
+ pattern.style.stroke = pattern.style.stroke
+ pattern.removeAttribute('filter')
+ pattern.setAttribute('filter', 'url(#invert)')
+ }
+ }
+ if (replace) {
+ window.history.replaceState(params, '', url)
+ } else {
+ window.history.pushState(params, '', url)
+ }
+ } else {
+ // use URL hash for IE8
+ window.location.hash = '?' + strParams
+ // inhibit next onhashchange that just added itself
+ // to the event queue
+ this._cancelPop = true
+ }
+ },
+
+ /**
+ * Push the current URL parameters to the history stack
+ * and change the visible URL.
+ * Note: this includes a workaround for IE8/IE9 that uses
+ * the hash part instead of the search part.
+ *
+ * @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(params, url) {
+ this._pushState(params, url, false)
+ },
+
+ /**
+ * Push the current URL parameters to the history stack
+ * and change the visible URL.
+ * Note: this includes a workaround for IE8/IE9 that uses
+ * the hash part instead of the search part.
+ *
+ * @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
+ */
+ replaceState(params, url) {
+ this._pushState(params, url, true)
+ },
+
+ /**
+ * Add a popstate handler
+ *
+ * @param {Function} handler handler
+ */
+ addOnPopStateHandler(handler) {
+ this._handlers.push(handler)
+ },
+
+ /**
+ * Parse a query string from the hash part of the URL.
+ * (workaround for IE8 / IE9)
+ *
+ * @return {string}
+ */
+ _parseHashQuery() {
+ const hash = window.location.hash
+ const pos = hash.indexOf('?')
+ if (pos >= 0) {
+ return hash.substr(pos + 1)
+ }
+ if (hash.length) {
+ // remove hash sign
+ return hash.substr(1)
+ }
+ return ''
+ },
+
+ _decodeQuery(query) {
+ return query.replace(/\+/g, ' ')
+ },
+
+ /**
+ * Parse the query/search part of the URL.
+ * Also try and parse it from the URL hash (for IE8)
+ *
+ * @return {object} map of parameters
+ */
+ parseUrlQuery() {
+ const query = this._parseHashQuery()
+ let params
+ // try and parse from URL hash first
+ if (query) {
+ params = OC.parseQueryString(this._decodeQuery(query))
+ }
+ // else read from query attributes
+ params = _.extend(params || {}, OC.parseQueryString(this._decodeQuery(location.search)))
+ return params || {}
+ },
+
+ _onPopState(e) {
+ if (this._cancelPop) {
+ this._cancelPop = false
+ return
+ }
+ let params
+ if (!this._handlers.length) {
+ return
+ }
+ params = (e && e.state)
+ if (_.isString(params)) {
+ params = OC.parseQueryString(params)
+ } else if (!params) {
+ params = this.parseUrlQuery() || {}
+ }
+ for (let i = 0; i < this._handlers.length; i++) {
+ this._handlers[i](params)
+ }
+ },
+}
diff --git a/core/src/OC/util.js b/core/src/OC/util.js
new file mode 100644
index 00000000000..c46d9a141b1
--- /dev/null
+++ b/core/src/OC/util.js
@@ -0,0 +1,244 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import moment from 'moment'
+
+import History from './util-history.js'
+import OC from './index.js'
+import { formatFileSize as humanFileSize } from '@nextcloud/files'
+
+/**
+ * @param {any} t -
+ */
+function chunkify(t) {
+ // Adapted from http://my.opera.com/GreyWyvern/blog/show.dml/1671288
+ const tz = []
+ let x = 0
+ let y = -1
+ let n = 0
+ let c
+
+ while (x < t.length) {
+ c = t.charAt(x)
+ // only include the dot in strings
+ const m = ((!n && c === '.') || (c >= '0' && c <= '9'))
+ if (m !== n) {
+ // next chunk
+ y++
+ tz[y] = ''
+ n = m
+ }
+ tz[y] += c
+ x++
+ }
+ return tz
+}
+
+/**
+ * Utility functions
+ *
+ * @namespace OC.Util
+ */
+export default {
+
+ History,
+
+ /**
+ * @deprecated use https://nextcloud.github.io/nextcloud-files/functions/formatFileSize.html
+ */
+ humanFileSize,
+
+ /**
+ * Returns a file size in bytes from a humanly readable string
+ * Makes 2kB to 2048.
+ * Inspired by computerFileSize in helper.php
+ *
+ * @param {string} string file size in human-readable format
+ * @return {number} or null if string could not be parsed
+ *
+ *
+ */
+ computerFileSize(string) {
+ if (typeof string !== 'string') {
+ return null
+ }
+
+ const s = string.toLowerCase().trim()
+ 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,
+ }
+
+ const matches = s.match(/^[\s+]?([0-9]*)(\.([0-9]+))?( +)?([kmgtp]?b?)$/i)
+ if (matches !== null) {
+ bytes = parseFloat(s)
+ if (!isFinite(bytes)) {
+ return null
+ }
+ } else {
+ return null
+ }
+ if (matches[5]) {
+ bytes = bytes * bytesArray[matches[5]]
+ }
+
+ bytes = Math.round(bytes)
+ return bytes
+ },
+
+ /**
+ * @param {string|number} timestamp timestamp
+ * @param {string} format date format, see momentjs docs
+ * @return {string} timestamp formatted as requested
+ */
+ formatDate(timestamp, format) {
+ if (window.TESTING === undefined) {
+ OC.debug && 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)
+ },
+
+ /**
+ * @param {string|number} timestamp timestamp
+ * @return {string} human readable difference from now
+ */
+ relativeModifiedDate(timestamp) {
+ if (window.TESTING === undefined) {
+ OC.debug && 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')
+ }
+ return moment(timestamp).fromNow()
+ },
+
+ /**
+ * Returns the width of a generic browser scrollbar
+ *
+ * @return {number} width of scrollbar
+ */
+ getScrollBarWidth() {
+ if (this._scrollBarWidth) {
+ return this._scrollBarWidth
+ }
+
+ const inner = document.createElement('p')
+ inner.style.width = '100%'
+ inner.style.height = '200px'
+
+ const outer = document.createElement('div')
+ outer.style.position = 'absolute'
+ outer.style.top = '0px'
+ outer.style.left = '0px'
+ outer.style.visibility = 'hidden'
+ outer.style.width = '200px'
+ outer.style.height = '150px'
+ outer.style.overflow = 'hidden'
+ outer.appendChild(inner)
+
+ document.body.appendChild(outer)
+ const w1 = inner.offsetWidth
+ outer.style.overflow = 'scroll'
+ let w2 = inner.offsetWidth
+ if (w1 === w2) {
+ w2 = outer.clientWidth
+ }
+
+ document.body.removeChild(outer)
+
+ this._scrollBarWidth = (w1 - w2)
+
+ return this._scrollBarWidth
+ },
+
+ /**
+ * Remove the time component from a given date
+ *
+ * @param {Date} date date
+ * @return {Date} date with stripped time
+ */
+ 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())
+ },
+
+ /**
+ * Compare two strings to provide a natural sort
+ *
+ * @param {string} a first string to compare
+ * @param {string} b second string to compare
+ * @return {number} -1 if b comes before a, 1 if a comes before b
+ * or 0 if the strings are identical
+ */
+ naturalSortCompare(a, b) {
+ let x
+ const aa = chunkify(a)
+ const bb = chunkify(b)
+
+ for (x = 0; aa[x] && bb[x]; x++) {
+ if (aa[x] !== bb[x]) {
+ const aNum = Number(aa[x]); const bNum = Number(bb[x])
+ // note: == is correct here
+ /* eslint-disable-next-line */
+ if (aNum == aa[x] && bNum == bb[x]) {
+ return aNum - bNum
+ } else {
+ // Note: This locale setting isn't supported by all browsers but for the ones
+ // that do there will be more consistency between client-server sorting
+ return aa[x].localeCompare(bb[x], OC.getLanguage())
+ }
+ }
+ }
+ return aa.length - bb.length
+ },
+
+ /**
+ * Calls the callback in a given interval until it returns true
+ *
+ * @param {Function} callback function to call on success
+ * @param {number} interval in milliseconds
+ */
+ waitFor(callback, interval) {
+ const internalCallback = function() {
+ if (callback() !== true) {
+ setTimeout(internalCallback, interval)
+ }
+ }
+
+ internalCallback()
+ },
+
+ /**
+ * Checks if a cookie with the given name is present and is set to the provided value.
+ *
+ * @param {string} name name of the cookie
+ * @param {string} value value of the cookie
+ * @return {boolean} true if the cookie with the given name has the given value
+ */
+ isCookieSetToValue(name, value) {
+ const cookies = document.cookie.split(';')
+ for (let i = 0; i < cookies.length; i++) {
+ const cookie = cookies[i].split('=')
+ if (cookie[0].trim() === name && cookie[1].trim() === value) {
+ return true
+ }
+ }
+ return false
+ },
+}
diff --git a/core/src/OC/webroot.js b/core/src/OC/webroot.js
new file mode 100644
index 00000000000..cbe5a6190e1
--- /dev/null
+++ b/core/src/OC/webroot.js
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+let webroot = window._oc_webroot
+
+if (typeof webroot === 'undefined') {
+ webroot = location.pathname
+ const pos = webroot.indexOf('/index.php/')
+ if (pos !== -1) {
+ webroot = webroot.substr(0, pos)
+ } else {
+ webroot = webroot.substr(0, webroot.lastIndexOf('/'))
+ }
+}
+
+export default webroot
diff --git a/core/src/OC/xhr-error.js b/core/src/OC/xhr-error.js
new file mode 100644
index 00000000000..233aaf60350
--- /dev/null
+++ b/core/src/OC/xhr-error.js
@@ -0,0 +1,102 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import _ from 'underscore'
+import $ from 'jquery'
+
+import OC from './index.js'
+import Notification from './notification.js'
+import { getCurrentUser } from '@nextcloud/auth'
+import { showWarning } from '@nextcloud/dialogs'
+
+/**
+ * Warn users that the connection to the server was lost temporarily
+ *
+ * This function is throttled to prevent stacked notifications.
+ * After 7sec the first notification is gone, then we can show another one
+ * if necessary.
+ */
+export const ajaxConnectionLostHandler = _.throttle(() => {
+ showWarning(t('core', 'Connection to server lost'))
+}, 7 * 1000, { trailing: false })
+
+/**
+ * Process ajax error, redirects to main page
+ * if an error/auth error status was returned.
+ *
+ * @param {XMLHttpRequest} xhr xhr request
+ */
+export const processAjaxError = xhr => {
+ // purposefully aborted request ?
+ // OC._userIsNavigatingAway needed to distinguish Ajax calls cancelled by navigating away
+ // from calls cancelled by failed cross-domain Ajax due to SSO redirect
+ if (xhr.status === 0 && (xhr.statusText === 'abort' || xhr.statusText === 'timeout' || OC._reloadCalled)) {
+ return
+ }
+
+ if ([302, 303, 307, 401].includes(xhr.status) && getCurrentUser()) {
+ // sometimes "beforeunload" happens later, so need to defer the reload a bit
+ setTimeout(function() {
+ if (!OC._userIsNavigatingAway && !OC._reloadCalled) {
+ let timer = 0
+ const seconds = 5
+ const interval = setInterval(function() {
+ Notification.showUpdate(n('core', 'Problem loading page, reloading in %n second', 'Problem loading page, reloading in %n seconds', seconds - timer))
+ if (timer >= seconds) {
+ clearInterval(interval)
+ OC.reload()
+ }
+ timer++
+ }, 1000, // 1 second interval
+ )
+
+ // only call reload once
+ OC._reloadCalled = true
+ }
+ }, 100)
+ } else if (xhr.status === 0) {
+ // Connection lost (e.g. WiFi disconnected or server is down)
+ setTimeout(function() {
+ if (!OC._userIsNavigatingAway && !OC._reloadCalled) {
+ // TODO: call method above directly
+ OC._ajaxConnectionLostHandler()
+ }
+ }, 100)
+ }
+}
+
+/**
+ * Registers XmlHttpRequest object for global error processing.
+ *
+ * This means that if this XHR object returns 401 or session timeout errors,
+ * the current page will automatically be reloaded.
+ *
+ * @param {XMLHttpRequest} xhr xhr request
+ */
+export const registerXHRForErrorProcessing = xhr => {
+ const loadCallback = () => {
+ if (xhr.readyState !== 4) {
+ return
+ }
+
+ if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
+ return
+ }
+
+ // fire jquery global ajax error handler
+ $(document).trigger(new $.Event('ajaxError'), xhr)
+ }
+
+ const errorCallback = () => {
+ // fire jquery global ajax error handler
+ $(document).trigger(new $.Event('ajaxError'), xhr)
+ }
+
+ if (xhr.addEventListener) {
+ xhr.addEventListener('load', loadCallback)
+ xhr.addEventListener('error', errorCallback)
+ }
+
+}