diff --git a/core/js/dist/login.js b/core/js/dist/login.js index 9ae0db355b25bc6625378cf9c418d4135b79ebdc..5e26d0ed3a96f89daba4d87be5bf1a1be6e475fc 100644 Binary files a/core/js/dist/login.js and b/core/js/dist/login.js differ diff --git a/core/js/dist/login.js.map b/core/js/dist/login.js.map index d9d5d6481fa32d72f4c670a994c38b7dd1a6b563..a4c07a576c01d227e77c9879ae0c57d4308a12de 100644 Binary files a/core/js/dist/login.js.map and b/core/js/dist/login.js.map differ diff --git a/core/js/dist/main.js b/core/js/dist/main.js index 1fa5e33fdc426cf3642df1cb5b127ff8f26c0c46..a18aff614c71a7f56148aaa8c2a840779d476379 100644 Binary files a/core/js/dist/main.js and b/core/js/dist/main.js differ diff --git a/core/js/dist/main.js.map b/core/js/dist/main.js.map index d25d87438d9e99007bd1f76d9d470b1ed6141a0a..9e0c0a8c684ada832dad20af28b82c29a3498f23 100644 Binary files a/core/js/dist/main.js.map and b/core/js/dist/main.js.map differ diff --git a/core/js/js.js b/core/js/js.js index 13e0e11b0fbb34f5b0f1080d274629738146cbc4..f00b7bc2420cfda1bcfd02aacf26c97488979d5d 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -469,461 +469,3 @@ Object.assign(window.OC, { } }); -/** - * Initializes core - */ -function initCore() { - /** - * Disable automatic evaluation of responses for $.ajax() functions (and its - * higher-level alternatives like $.get() and $.post()). - * - * If a response to a $.ajax() request returns a content type of "application/javascript" - * JQuery would previously execute the response body. This is a pretty unexpected - * behaviour and can result in a bypass of our Content-Security-Policy as well as - * multiple unexpected XSS vectors. - */ - $.ajaxSetup({ - contents: { - script: false - } - }); - - /** - * Disable execution of eval in jQuery. We do require an allowed eval CSP - * configuration at the moment for handlebars et al. But for jQuery there is - * not much of a reason to execute JavaScript directly via eval. - * - * This thus mitigates some unexpected XSS vectors. - */ - jQuery.globalEval = function(){}; - - /** - * Set users locale to moment.js as soon as possible - */ - moment.locale(OC.getLocale()); - - var userAgent = window.navigator.userAgent; - var msie = userAgent.indexOf('MSIE '); - var trident = userAgent.indexOf('Trident/'); - var edge = userAgent.indexOf('Edge/'); - - if (msie > 0 || trident > 0) { - // (IE 10 or older) || IE 11 - $('html').addClass('ie'); - } else if (edge > 0) { - // for edge - $('html').addClass('edge'); - } - - // css variables fallback for IE - if (msie > 0 || trident > 0 || edge > 0) { - console.info('Legacy browser detected, applying css vars polyfill') - cssVars({ - watch: true, - // set edge < 16 as incompatible - onlyLegacy: !(/Edge\/([0-9]{2})\./i.test(navigator.userAgent) - && parseInt(/Edge\/([0-9]{2})\./i.exec(navigator.userAgent)[1]) < 16) - }); - } - - $(window).on('unload.main', function() { - OC._unloadCalled = true; - }); - $(window).on('beforeunload.main', function() { - // super-trick thanks to http://stackoverflow.com/a/4651049 - // in case another handler displays a confirmation dialog (ex: navigating away - // during an upload), there are two possible outcomes: user clicked "ok" or - // "cancel" - - // first timeout handler is called after unload dialog is closed - setTimeout(function() { - OC._userIsNavigatingAway = true; - - // second timeout event is only called if user cancelled (Chrome), - // but in other browsers it might still be triggered, so need to - // set a higher delay... - setTimeout(function() { - if (!OC._unloadCalled) { - OC._userIsNavigatingAway = false; - } - }, 10000); - },1); - }); - $(document).on('ajaxError.main', function( event, request, settings ) { - if (settings && settings.allowAuthErrors) { - return; - } - OC._processAjaxError(request); - }); - - /** - * Calls the server periodically to ensure that session and CSRF - * token doesn't expire - */ - function initSessionHeartBeat() { - // interval in seconds - var interval = NaN; - if (OC.config.session_lifetime) { - interval = Math.floor(OC.config.session_lifetime / 2); - } - interval = isNaN(interval)? 900: interval; - - // minimum one minute - interval = Math.max(60, interval); - // max interval in seconds set to 24 hours - interval = Math.min(24 * 3600, interval); - - var url = OC.generateUrl('/csrftoken'); - setInterval(function() { - $.ajax(url).then(function(resp) { - oc_requesttoken = resp.token; - OC.requestToken = resp.token; - }).fail(function(e) { - console.error('session heartbeat failed', e); - }); - }, interval * 1000); - } - - // session heartbeat (defaults to enabled) - if (typeof(OC.config.session_keepalive) === 'undefined' || - !!OC.config.session_keepalive) { - - initSessionHeartBeat(); - } - - OC.registerMenu($('#expand'), $('#expanddiv'), false, true); - - // toggle for menus - //$(document).on('mouseup.closemenus keyup', function(event) { - $(document).on('mouseup.closemenus', function(event) { - - // allow enter as a trigger - // if (event.key && event.key !== "Enter") { - // return; - // } - - var $el = $(event.target); - if ($el.closest('.menu').length || $el.closest('.menutoggle').length) { - // don't close when clicking on the menu directly or a menu toggle - return false; - } - - OC.hideMenus(); - }); - - /** - * Set up the main menu toggle to react to media query changes. - * If the screen is small enough, the main menu becomes a toggle. - * If the screen is bigger, the main menu is not a toggle any more. - */ - function setupMainMenu() { - - // init the more-apps menu - OC.registerMenu($('#more-apps > a'), $('#navigation')); - - // toggle the navigation - var $toggle = $('#header .header-appname-container'); - var $navigation = $('#navigation'); - var $appmenu = $('#appmenu'); - - // init the menu - OC.registerMenu($toggle, $navigation); - $toggle.data('oldhref', $toggle.attr('href')); - $toggle.attr('href', '#'); - $navigation.hide(); - - // show loading feedback on more apps list - $navigation.delegate('a', 'click', function(event) { - var $app = $(event.target); - if(!$app.is('a')) { - $app = $app.closest('a'); - } - if(event.which === 1 && !event.ctrlKey && !event.metaKey) { - $app.find('svg').remove(); - $app.find('div').remove(); // prevent odd double-clicks - // no need for theming, loader is already inverted on dark mode - // but we need it over the primary colour - $app.prepend($('<div/>').addClass('icon-loading-small')); - } else { - // Close navigation when opening app in - // a new tab - OC.hideMenus(function(){return false;}); - } - }); - - $navigation.delegate('a', 'mouseup', function(event) { - if(event.which === 2) { - // Close navigation when opening app in - // a new tab via middle click - OC.hideMenus(function(){return false;}); - } - }); - - // show loading feedback on visible apps list - $appmenu.delegate('li:not(#more-apps) > a', 'click', function(event) { - var $app = $(event.target); - if(!$app.is('a')) { - $app = $app.closest('a'); - } - if(event.which === 1 && !event.ctrlKey && !event.metaKey && $app.parent('#more-apps').length === 0) { - $app.find('svg').remove(); - $app.find('div').remove(); // prevent odd double-clicks - $app.prepend($('<div/>').addClass( - OCA.Theming && OCA.Theming.inverted - ? 'icon-loading-small' - : 'icon-loading-small-dark' - )); - } else { - // Close navigation when opening app in - // a new tab - OC.hideMenus(function(){return false;}); - } - }); - } - - function setupUserMenu() { - var $menu = $('#header #settings'); - - // show loading feedback - $menu.delegate('a', 'click', function(event) { - var $page = $(event.target); - if (!$page.is('a')) { - $page = $page.closest('a'); - } - if(event.which === 1 && !event.ctrlKey && !event.metaKey) { - $page.find('img').remove(); - $page.find('div').remove(); // prevent odd double-clicks - $page.prepend($('<div/>').addClass('icon-loading-small')); - } else { - // Close navigation when opening menu entry in - // a new tab - OC.hideMenus(function(){return false;}); - } - }); - - $menu.delegate('a', 'mouseup', function(event) { - if(event.which === 2) { - // Close navigation when opening app in - // a new tab via middle click - OC.hideMenus(function(){return false;}); - } - }); - } - - function setupContactsMenu() { - new OC.ContactsMenu({ - el: $('#contactsmenu .menu'), - trigger: $('#contactsmenu .menutoggle') - }); - } - - setupMainMenu(); - setupUserMenu(); - setupContactsMenu(); - - // move triangle of apps dropdown to align with app name triangle - // 2 is the additional offset between the triangles - if($('#navigation').length) { - $('#header #nextcloud + .menutoggle').on('click', function(){ - $('#menu-css-helper').remove(); - var caretPosition = $('.header-appname + .icon-caret').offset().left - 2; - if(caretPosition > 255) { - // if the app name is longer than the menu, just put the triangle in the middle - return; - } else { - $('head').append('<style id="menu-css-helper">#navigation:after { left: '+ caretPosition +'px; }</style>'); - } - }); - $('#header #appmenu .menutoggle').on('click', function() { - $('#appmenu').toggleClass('menu-open'); - if($('#appmenu').is(':visible')) { - $('#menu-css-helper').remove(); - } - }); - } - - var resizeMenu = function() { - var appList = $('#appmenu li'); - var rightHeaderWidth = $('.header-right').outerWidth(); - var headerWidth = $('header').outerWidth(); - var usePercentualAppMenuLimit = 0.33; - var minAppsDesktop = 8; - var availableWidth = headerWidth - $('#nextcloud').outerWidth() - (rightHeaderWidth > 210 ? rightHeaderWidth : 210) - var isMobile = $(window).width() < 768; - if (!isMobile) { - availableWidth = availableWidth * usePercentualAppMenuLimit; - } - var appCount = Math.floor((availableWidth / $(appList).width())); - if (isMobile && appCount > minAppsDesktop) { - appCount = minAppsDesktop; - } - if (!isMobile && appCount < minAppsDesktop) { - appCount = minAppsDesktop; - } - - // show at least 2 apps in the popover - if(appList.length-1-appCount >= 1) { - appCount--; - } - - $('#more-apps a').removeClass('active'); - var lastShownApp; - for (var k = 0; k < appList.length-1; k++) { - var name = $(appList[k]).data('id'); - if(k < appCount) { - $(appList[k]).removeClass('hidden'); - $('#apps li[data-id=' + name + ']').addClass('in-header'); - lastShownApp = appList[k]; - } else { - $(appList[k]).addClass('hidden'); - $('#apps li[data-id=' + name + ']').removeClass('in-header'); - // move active app to last position if it is active - if(appCount > 0 && $(appList[k]).children('a').hasClass('active')) { - $(lastShownApp).addClass('hidden'); - $('#apps li[data-id=' + $(lastShownApp).data('id') + ']').removeClass('in-header'); - $(appList[k]).removeClass('hidden'); - $('#apps li[data-id=' + name + ']').addClass('in-header'); - } - } - } - - // show/hide more apps icon - if($('#apps li:not(.in-header)').length === 0) { - $('#more-apps').hide(); - $('#navigation').hide(); - } else { - $('#more-apps').show(); - } - }; - $(window).resize(resizeMenu); - setTimeout(resizeMenu, 0); - - // just add snapper for logged in users - // and if the app doesn't handle the nav slider itself - if($('#app-navigation').length && !$('html').hasClass('lte9') - && !$('#app-content').hasClass('no-snapper')) { - - // App sidebar on mobile - var snapper = new Snap({ - element: document.getElementById('app-content'), - disable: 'right', - maxPosition: 300, // $navigation-width - minDragDistance: 100 - }); - - $('#app-content').prepend('<div id="app-navigation-toggle" class="icon-menu" style="display:none;" tabindex="0"></div>'); - - var toggleSnapperOnButton = function(){ - if(snapper.state().state == 'left'){ - snapper.close(); - } else { - snapper.open('left'); - } - }; - - $('#app-navigation-toggle').click(function(){ - toggleSnapperOnButton(); - }); - - $('#app-navigation-toggle').keypress(function(e) { - if(e.which == 13) { - toggleSnapperOnButton(); - } - }); - - // close sidebar when switching navigation entry - var $appNavigation = $('#app-navigation'); - $appNavigation.delegate('a, :button', 'click', function(event) { - var $target = $(event.target); - // don't hide navigation when changing settings or adding things - if($target.is('.app-navigation-noclose') || - $target.closest('.app-navigation-noclose').length) { - return; - } - if($target.is('.app-navigation-entry-utils-menu-button') || - $target.closest('.app-navigation-entry-utils-menu-button').length) { - return; - } - if($target.is('.add-new') || - $target.closest('.add-new').length) { - return; - } - if($target.is('#app-settings') || - $target.closest('#app-settings').length) { - return; - } - snapper.close(); - }); - - var navigationBarSlideGestureEnabled = false; - var navigationBarSlideGestureAllowed = true; - var navigationBarSlideGestureEnablePending = false; - - OC.allowNavigationBarSlideGesture = function() { - navigationBarSlideGestureAllowed = true; - - if (navigationBarSlideGestureEnablePending) { - snapper.enable(); - - navigationBarSlideGestureEnabled = true; - navigationBarSlideGestureEnablePending = false; - } - }; - - OC.disallowNavigationBarSlideGesture = function() { - navigationBarSlideGestureAllowed = false; - - if (navigationBarSlideGestureEnabled) { - var endCurrentDrag = true; - snapper.disable(endCurrentDrag); - - navigationBarSlideGestureEnabled = false; - navigationBarSlideGestureEnablePending = true; - } - }; - - var toggleSnapperOnSize = function() { - if($(window).width() > 768) { - snapper.close(); - snapper.disable(); - - navigationBarSlideGestureEnabled = false; - navigationBarSlideGestureEnablePending = false; - } else if (navigationBarSlideGestureAllowed) { - snapper.enable(); - - navigationBarSlideGestureEnabled = true; - navigationBarSlideGestureEnablePending = false; - } else { - navigationBarSlideGestureEnablePending = true; - } - }; - - $(window).resize(_.debounce(toggleSnapperOnSize, 250)); - - // initial call - toggleSnapperOnSize(); - - } - - // Update live timestamps every 30 seconds - setInterval(function() { - $('.live-relative-timestamp').each(function() { - $(this).text(OC.Util.relativeModifiedDate(parseInt($(this).attr('data-timestamp'), 10))); - }); - }, 30 * 1000); - - OC.PasswordConfirmation.init(); -} - -$(document).ready(initCore); - -/** -// fallback to hashchange when no history support -if (window.history.pushState) { - window.onpopstate = _.bind(OC.Util.History._onPopState, OC.Util.History); -} -else { - $(window).on('hashchange', _.bind(OC.Util.History._onPopState, OC.Util.History)); -} - */ diff --git a/core/js/tests/specs/coreSpec.js b/core/js/tests/specs/coreSpec.js index 047b5ec1eb501c3d979ac2ef9d63bb56dd2d91a5..a785cf49db817e767d7a9ab865d2bac5ae3cd65f 100644 --- a/core/js/tests/specs/coreSpec.js +++ b/core/js/tests/specs/coreSpec.js @@ -368,13 +368,11 @@ describe('Core base tests', function() { describe('Session heartbeat', function() { var clock, oldConfig, - routeStub, counter; beforeEach(function() { clock = sinon.useFakeTimers(); oldConfig = OC.config; - routeStub = sinon.stub(OC, 'generateUrl').returns('/csrftoken'); counter = 0; fakeServer.autoRespond = true; @@ -389,7 +387,6 @@ describe('Core base tests', function() { clock.restore(); /* jshint camelcase: false */ OC.config = oldConfig; - routeStub.restore(); $(document).off('ajaxError'); $(document).off('ajaxComplete'); }); @@ -400,7 +397,6 @@ describe('Core base tests', function() { session_lifetime: 300 }; window.initCore(); - expect(routeStub.calledWith('/csrftoken')).toEqual(true); expect(counter).toEqual(0); @@ -427,7 +423,6 @@ describe('Core base tests', function() { session_lifetime: 300 }; window.initCore(); - expect(routeStub.notCalled).toEqual(true); expect(counter).toEqual(0); diff --git a/core/src/OC/appconfig.js b/core/src/OC/appconfig.js index ee7efb8a3ac780576e663354f81544b7e887d754..1248475ce77f8b88c871a83e0f515d442f5791c5 100644 --- a/core/src/OC/appconfig.js +++ b/core/src/OC/appconfig.js @@ -26,7 +26,7 @@ export const appConfig = window.oc_appconfig || {} * @namespace * @deprecated 16.0.0 Use OCP.AppConfig instead */ -const AppConfig = { +export const AppConfig = { /** * @deprecated Use OCP.AppConfig.getValue() instead */ @@ -69,5 +69,3 @@ const AppConfig = { } }; - -export default AppConfig; diff --git a/core/src/components/ContactsMenu.js b/core/src/components/ContactsMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..8243b9ca1f26a3b9cdb087d6f4d23ba7d9cc78fb --- /dev/null +++ b/core/src/components/ContactsMenu.js @@ -0,0 +1,34 @@ +/* + * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import $ from 'jquery' + +import OC from '../OC' + +/** + * @todo move to contacts menu code https://github.com/orgs/nextcloud/projects/31#card-21213129 + */ +export const setUp = () => { + new OC.ContactsMenu({ + el: $('#contactsmenu .menu'), + trigger: $('#contactsmenu .menutoggle') + }) +} diff --git a/core/src/components/MainMenu.js b/core/src/components/MainMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..40eca1ecd9dbe0adc8d964be27c67b984ee738bb --- /dev/null +++ b/core/src/components/MainMenu.js @@ -0,0 +1,93 @@ +/* + * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import $ from 'jquery' + +import OC from '../OC' + +/** + * Set up the main menu toggle to react to media query changes. + * If the screen is small enough, the main menu becomes a toggle. + * If the screen is bigger, the main menu is not a toggle any more. + */ +export const setUp = () => { + // init the more-apps menu + OC.registerMenu($('#more-apps > a'), $('#navigation')) + + // toggle the navigation + const $toggle = $('#header .header-appname-container') + const $navigation = $('#navigation') + const $appmenu = $('#appmenu') + + // init the menu + OC.registerMenu($toggle, $navigation) + $toggle.data('oldhref', $toggle.attr('href')) + $toggle.attr('href', '#') + $navigation.hide() + + // show loading feedback on more apps list + $navigation.delegate('a', 'click', event => { + let $app = $(event.target) + if (!$app.is('a')) { + $app = $app.closest('a') + } + if (event.which === 1 && !event.ctrlKey && !event.metaKey) { + $app.find('svg').remove() + $app.find('div').remove() // prevent odd double-clicks + // no need for theming, loader is already inverted on dark mode + // but we need it over the primary colour + $app.prepend($('<div/>').addClass('icon-loading-small')) + } else { + // Close navigation when opening app in + // a new tab + OC.hideMenus(() => false) + } + }) + + $navigation.delegate('a', 'mouseup', event => { + if (event.which === 2) { + // Close navigation when opening app in + // a new tab via middle click + OC.hideMenus(() => false) + } + }) + + // show loading feedback on visible apps list + $appmenu.delegate('li:not(#more-apps) > a', 'click', event => { + let $app = $(event.target) + if (!$app.is('a')) { + $app = $app.closest('a') + } + if (event.which === 1 && !event.ctrlKey && !event.metaKey && $app.parent('#more-apps').length === 0) { + $app.find('svg').remove() + $app.find('div').remove() // prevent odd double-clicks + $app.prepend($('<div/>').addClass( + OCA.Theming && OCA.Theming.inverted + ? 'icon-loading-small' + : 'icon-loading-small-dark' + )) + } else { + // Close navigation when opening app in + // a new tab + OC.hideMenus(() => false) + } + }) +} diff --git a/core/src/components/UserMenu.js b/core/src/components/UserMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..a9e7d8725bba5463de3f86bf9c1ef8a3e18279e3 --- /dev/null +++ b/core/src/components/UserMenu.js @@ -0,0 +1,53 @@ +/* + * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import OC from '../OC' + +import $ from 'jquery' + +export const setUp = () => { + const $menu = $('#header #settings') + + // show loading feedback + $menu.delegate('a', 'click', event => { + let $page = $(event.target) + if (!$page.is('a')) { + $page = $page.closest('a') + } + if (event.which === 1 && !event.ctrlKey && !event.metaKey) { + $page.find('img').remove() + $page.find('div').remove() // prevent odd double-clicks + $page.prepend($('<div/>').addClass('icon-loading-small')) + } else { + // Close navigation when opening menu entry in + // a new tab + OC.hideMenus(() => false) + } + }) + + $menu.delegate('a', 'mouseup', event => { + if (event.which === 2) { + // Close navigation when opening app in + // a new tab via middle click + OC.hideMenus(() => false) + } + }) +} diff --git a/core/src/globals.js b/core/src/globals.js index 312952fe90c0aa6c6ca374c119cc603417e9f2b0..65ad3148ec6f681eaa3efa9afc4c14a8e2761b53 100644 --- a/core/src/globals.js +++ b/core/src/globals.js @@ -19,7 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import appswebroots from "./OC/appswebroots"; +import {initCore} from './init' const warnIfNotTesting = function() { if (window.TESTING === undefined) { @@ -115,6 +115,7 @@ window['md5'] = md5 window['moment'] = moment window['OC'] = OC +setDeprecatedProp('initCore', initCore, 'this is an internal function') setDeprecatedProp('oc_appswebroots', OC.appswebroots, 'use OC.appswebroots instead') setDeprecatedProp('oc_config', OC.config, 'use OC.config instead') setDeprecatedProp('oc_current_user', OC.getCurrentUser().uid, 'use OC.getCurrentUser().uid instead') diff --git a/core/src/init.js b/core/src/init.js new file mode 100644 index 0000000000000000000000000000000000000000..ebf99ffd6401ebfe47b8f9baae8c7ba0af93b0db --- /dev/null +++ b/core/src/init.js @@ -0,0 +1,307 @@ +/* + * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import _ from 'underscore' +import $ from 'jquery' +import moment from 'moment' + +import {initSessionHeartBeat} from './session-heartbeat' +import OC from './OC/index' +import {setUp as setUpContactsMenu} from './components/ContactsMenu' +import {setUp as setUpMainMenu} from './components/MainMenu' +import {setUp as setUpUserMenu} from './components/UserMenu' +import PasswordConfirmation from './OC/password-confirmation' + +const resizeMenu = () => { + const appList = $('#appmenu li') + const rightHeaderWidth = $('.header-right').outerWidth() + const headerWidth = $('header').outerWidth() + const usePercentualAppMenuLimit = 0.33 + const minAppsDesktop = 8 + let availableWidth = headerWidth - $('#nextcloud').outerWidth() - (rightHeaderWidth > 210 ? rightHeaderWidth : 210) + const isMobile = $(window).width() < 768 + if (!isMobile) { + availableWidth = availableWidth * usePercentualAppMenuLimit + } + let appCount = Math.floor((availableWidth / $(appList).width())) + if (isMobile && appCount > minAppsDesktop) { + appCount = minAppsDesktop + } + if (!isMobile && appCount < minAppsDesktop) { + appCount = minAppsDesktop + } + + // show at least 2 apps in the popover + if (appList.length - 1 - appCount >= 1) { + appCount-- + } + + $('#more-apps a').removeClass('active') + let lastShownApp + for (let k = 0; k < appList.length - 1; k++) { + const name = $(appList[k]).data('id') + if (k < appCount) { + $(appList[k]).removeClass('hidden') + $('#apps li[data-id=' + name + ']').addClass('in-header') + lastShownApp = appList[k] + } else { + $(appList[k]).addClass('hidden') + $('#apps li[data-id=' + name + ']').removeClass('in-header') + // move active app to last position if it is active + if (appCount > 0 && $(appList[k]).children('a').hasClass('active')) { + $(lastShownApp).addClass('hidden') + $('#apps li[data-id=' + $(lastShownApp).data('id') + ']').removeClass('in-header') + $(appList[k]).removeClass('hidden') + $('#apps li[data-id=' + name + ']').addClass('in-header') + } + } + } + + // show/hide more apps icon + if ($('#apps li:not(.in-header)').length === 0) { + $('#more-apps').hide() + $('#navigation').hide() + } else { + $('#more-apps').show() + } +} + +const initLiveTimestamps = () => { + // Update live timestamps every 30 seconds + setInterval(() => { + $('.live-relative-timestamp').each(function () { + $(this).text(OC.Util.relativeModifiedDate(parseInt($(this).attr('data-timestamp'), 10))) + }) + }, 30 * 1000) +} + +/** + * Initializes core + */ +export const initCore = () => { + /** + * Set users locale to moment.js as soon as possible + */ + moment.locale(OC.getLocale()) + + const userAgent = window.navigator.userAgent + const msie = userAgent.indexOf('MSIE ') + const trident = userAgent.indexOf('Trident/') + const edge = userAgent.indexOf('Edge/') + + if (msie > 0 || trident > 0) { + // (IE 10 or older) || IE 11 + $('html').addClass('ie') + } else if (edge > 0) { + // for edge + $('html').addClass('edge') + } + + // css variables fallback for IE + if (msie > 0 || trident > 0 || edge > 0) { + console.info('Legacy browser detected, applying css vars polyfill') + cssVars({ + watch: true, + // set edge < 16 as incompatible + onlyLegacy: !(/Edge\/([0-9]{2})\./i.test(navigator.userAgent) + && parseInt(/Edge\/([0-9]{2})\./i.exec(navigator.userAgent)[1]) < 16) + }) + } + + $(window).on('unload.main', () => OC._unloadCalled = true) + $(window).on('beforeunload.main', () => { + // super-trick thanks to http://stackoverflow.com/a/4651049 + // in case another handler displays a confirmation dialog (ex: navigating away + // during an upload), there are two possible outcomes: user clicked "ok" or + // "cancel" + + // first timeout handler is called after unload dialog is closed + setTimeout(() => { + OC._userIsNavigatingAway = true + + // second timeout event is only called if user cancelled (Chrome), + // but in other browsers it might still be triggered, so need to + // set a higher delay... + setTimeout(() => { + if (!OC._unloadCalled) { + OC._userIsNavigatingAway = false + } + }, 10000) + }, 1) + }) + $(document).on('ajaxError.main', function (event, request, settings) { + if (settings && settings.allowAuthErrors) { + return + } + OC._processAjaxError(request) + }) + + initSessionHeartBeat(); + + OC.registerMenu($('#expand'), $('#expanddiv'), false, true) + + // toggle for menus + $(document).on('mouseup.closemenus', event => { + const $el = $(event.target) + if ($el.closest('.menu').length || $el.closest('.menutoggle').length) { + // don't close when clicking on the menu directly or a menu toggle + return false + } + + OC.hideMenus() + }) + + setUpMainMenu() + setUpUserMenu() + setUpContactsMenu() + + // move triangle of apps dropdown to align with app name triangle + // 2 is the additional offset between the triangles + if ($('#navigation').length) { + $('#header #nextcloud + .menutoggle').on('click', () => { + $('#menu-css-helper').remove() + const caretPosition = $('.header-appname + .icon-caret').offset().left - 2 + if (caretPosition > 255) { + // if the app name is longer than the menu, just put the triangle in the middle + return + } else { + $('head').append('<style id="menu-css-helper">#navigation:after { left: ' + caretPosition + 'px }</style>') + } + }) + $('#header #appmenu .menutoggle').on('click', () => { + $('#appmenu').toggleClass('menu-open') + if ($('#appmenu').is(':visible')) { + $('#menu-css-helper').remove() + } + }) + } + + $(window).resize(resizeMenu) + setTimeout(resizeMenu, 0) + + // just add snapper for logged in users + // and if the app doesn't handle the nav slider itself + if ($('#app-navigation').length && !$('html').hasClass('lte9') + && !$('#app-content').hasClass('no-snapper')) { + + // App sidebar on mobile + const snapper = new Snap({ + element: document.getElementById('app-content'), + disable: 'right', + maxPosition: 300, // $navigation-width + minDragDistance: 100 + }) + + $('#app-content').prepend('<div id="app-navigation-toggle" class="icon-menu" style="display:none" tabindex="0"></div>') + + const toggleSnapperOnButton = () => { + if (snapper.state().state === 'left') { + snapper.close() + } else { + snapper.open('left') + } + } + + $('#app-navigation-toggle').click(toggleSnapperOnButton) + $('#app-navigation-toggle').keypress(e => { + if (e.which === 13) { + toggleSnapperOnButton() + } + }) + + // close sidebar when switching navigation entry + const $appNavigation = $('#app-navigation') + $appNavigation.delegate('a, :button', 'click', event => { + const $target = $(event.target) + // don't hide navigation when changing settings or adding things + if ($target.is('.app-navigation-noclose') || + $target.closest('.app-navigation-noclose').length) { + return + } + if ($target.is('.app-navigation-entry-utils-menu-button') || + $target.closest('.app-navigation-entry-utils-menu-button').length) { + return + } + if ($target.is('.add-new') || + $target.closest('.add-new').length) { + return + } + if ($target.is('#app-settings') || + $target.closest('#app-settings').length) { + return + } + snapper.close() + }) + + let navigationBarSlideGestureEnabled = false + let navigationBarSlideGestureAllowed = true + let navigationBarSlideGestureEnablePending = false + + OC.allowNavigationBarSlideGesture = () => { + navigationBarSlideGestureAllowed = true + + if (navigationBarSlideGestureEnablePending) { + snapper.enable() + + navigationBarSlideGestureEnabled = true + navigationBarSlideGestureEnablePending = false + } + } + + OC.disallowNavigationBarSlideGesture = () => { + navigationBarSlideGestureAllowed = false + + if (navigationBarSlideGestureEnabled) { + const endCurrentDrag = true + snapper.disable(endCurrentDrag) + + navigationBarSlideGestureEnabled = false + navigationBarSlideGestureEnablePending = true + } + } + + const toggleSnapperOnSize = () => { + if ($(window).width() > 768) { + snapper.close() + snapper.disable() + + navigationBarSlideGestureEnabled = false + navigationBarSlideGestureEnablePending = false + } else if (navigationBarSlideGestureAllowed) { + snapper.enable() + + navigationBarSlideGestureEnabled = true + navigationBarSlideGestureEnablePending = false + } else { + navigationBarSlideGestureEnablePending = true + } + } + + $(window).resize(_.debounce(toggleSnapperOnSize, 250)) + + // initial call + toggleSnapperOnSize() + + } + + initLiveTimestamps() + PasswordConfirmation.init() +} diff --git a/core/src/jquery/index.js b/core/src/jquery/index.js index 71659137b3efb8344583cc0b46f7d26472bea2bb..43b379fd2e505b886da7c453967db25a5adc36fe 100644 --- a/core/src/jquery/index.js +++ b/core/src/jquery/index.js @@ -19,6 +19,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +import $ from 'jquery' + import './avatar' import './contactsmenu' import './exists' @@ -33,3 +35,28 @@ import './ui-fixes' import './css/jquery-ui-fixes.scss' import './css/jquery.ocdialog.scss' + +/** + * Disable automatic evaluation of responses for $.ajax() functions (and its + * higher-level alternatives like $.get() and $.post()). + * + * If a response to a $.ajax() request returns a content type of "application/javascript" + * JQuery would previously execute the response body. This is a pretty unexpected + * behaviour and can result in a bypass of our Content-Security-Policy as well as + * multiple unexpected XSS vectors. + */ +$.ajaxSetup({ + contents: { + script: false + } +}) + +/** + * Disable execution of eval in jQuery. We do require an allowed eval CSP + * configuration at the moment for handlebars et al. But for jQuery there is + * not much of a reason to execute JavaScript directly via eval. + * + * This thus mitigates some unexpected XSS vectors. + */ +$.globalEval = function () { +} diff --git a/core/src/main.js b/core/src/main.js index 4d963a1795cfe88ccdd7953aed1479d75b9c13e7..1f8f12c1a0fabd64bcddece58b371ae50e0ec566 100644 --- a/core/src/main.js +++ b/core/src/main.js @@ -19,14 +19,20 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +import $ from 'jquery' import '@babel/polyfill' import './Polyfill/index' +// If you remove the line below, tests won't pass +import OC from './OC/index' + import './globals' -import $ from 'jquery' import './jquery/index' +import {initCore} from './init' import {registerAppsSlideToggle} from './OC/apps' $(document).ready(function () { + initCore(); + registerAppsSlideToggle(); }); diff --git a/core/src/session-heartbeat.js b/core/src/session-heartbeat.js new file mode 100644 index 0000000000000000000000000000000000000000..5d1f7177cf7197d5e3f659f7e8d9ef3151f2038d --- /dev/null +++ b/core/src/session-heartbeat.js @@ -0,0 +1,76 @@ +/* + * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import $ from 'jquery' + +import {generateUrl} from './OC/routing' +import OC from './OC' + +/** + * session heartbeat (defaults to enabled) + * @return {boolean} + */ +const keepSessionAlive = () => { + return OC.config.session_keepalive === undefined + || !!OC.config.session_keepalive +} + +/** + * get interval in seconds + * @return {Number} + */ +const getInterval = () => { + let interval = NaN + if (OC.config.session_lifetime) { + interval = Math.floor(OC.config.session_lifetime / 2) + } + + // minimum one minute, max 24 hours, default 15 minutes + return Math.min( + 24 * 3600, + Math.max( + 60, + isNaN(interval) ? 900 : interval + ) + ) +} + +/** + * Calls the server periodically to ensure that session and CSRF + * token doesn't expire + */ +export const initSessionHeartBeat = () => { + if (!keepSessionAlive()) { + console.info('session heartbeat disabled') + return; + } + + setInterval(() => { + $.ajax(generateUrl('/csrftoken')) + .then(resp => { + oc_requesttoken = resp.token + OC.requestToken = resp.token + }) + .fail(e => { + console.error('session heartbeat failed', e) + }) + }, getInterval() * 1000) +}