Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>tags/v17.0.0beta1
@@ -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)); | |||
} | |||
*/ |
@@ -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); | |||
@@ -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; |
@@ -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') | |||
}) | |||
} |
@@ -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) | |||
} | |||
}) | |||
} |
@@ -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) | |||
} | |||
}) | |||
} |
@@ -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') |
@@ -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() | |||
} |
@@ -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 () { | |||
} |
@@ -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(); | |||
}); |
@@ -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) | |||
} |