js.js 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357
  1. /* global oc_isadmin */
  2. var oc_debug;
  3. var oc_webroot;
  4. var oc_current_user = document.getElementsByTagName('head')[0].getAttribute('data-user');
  5. var oc_requesttoken = document.getElementsByTagName('head')[0].getAttribute('data-requesttoken');
  6. window.oc_config = window.oc_config || {};
  7. if (typeof oc_webroot === "undefined") {
  8. oc_webroot = location.pathname;
  9. var pos = oc_webroot.indexOf('/index.php/');
  10. if (pos !== -1) {
  11. oc_webroot = oc_webroot.substr(0, pos);
  12. }
  13. else {
  14. oc_webroot = oc_webroot.substr(0, oc_webroot.lastIndexOf('/'));
  15. }
  16. }
  17. /** @namespace OCP */
  18. var OCP = Object.assign({}, window.OCP);
  19. /**
  20. * @namespace OC
  21. */
  22. Object.assign(window.OC, {
  23. PERMISSION_NONE:0,
  24. PERMISSION_CREATE:4,
  25. PERMISSION_READ:1,
  26. PERMISSION_UPDATE:2,
  27. PERMISSION_DELETE:8,
  28. PERMISSION_SHARE:16,
  29. PERMISSION_ALL:31,
  30. TAG_FAVORITE: '_$!<Favorite>!$_',
  31. /* jshint camelcase: false */
  32. /**
  33. * Relative path to Nextcloud root.
  34. * For example: "/nextcloud"
  35. *
  36. * @type string
  37. *
  38. * @deprecated since 8.2, use OC.getRootPath() instead
  39. * @see OC#getRootPath
  40. */
  41. webroot:oc_webroot,
  42. /**
  43. * Capabilities
  44. *
  45. * @type array
  46. */
  47. _capabilities: window.oc_capabilities || null,
  48. appswebroots:(typeof oc_appswebroots !== 'undefined') ? oc_appswebroots:false,
  49. /**
  50. * Currently logged in user or null if none
  51. *
  52. * @type String
  53. * @deprecated use {@link OC.getCurrentUser} instead
  54. */
  55. currentUser:(typeof oc_current_user!=='undefined')?oc_current_user:false,
  56. config: window.oc_config,
  57. appConfig: window.oc_appconfig || {},
  58. theme: window.oc_defaults || {},
  59. coreApps:['', 'admin','log','core/search','settings','core','3rdparty'],
  60. requestToken: oc_requesttoken,
  61. menuSpeed: 50,
  62. /**
  63. * Get an absolute url to a file in an app
  64. * @param {string} app the id of the app the file belongs to
  65. * @param {string} file the file path relative to the app folder
  66. * @return {string} Absolute URL to a file
  67. */
  68. linkTo:function(app,file){
  69. return OC.filePath(app,'',file);
  70. },
  71. /**
  72. * Creates a relative url for remote use
  73. * @param {string} service id
  74. * @return {string} the url
  75. */
  76. linkToRemoteBase:function(service) {
  77. return OC.getRootPath() + '/remote.php/' + service;
  78. },
  79. /**
  80. * @brief Creates an absolute url for remote use
  81. * @param {string} service id
  82. * @return {string} the url
  83. */
  84. linkToRemote:function(service) {
  85. return window.location.protocol + '//' + window.location.host + OC.linkToRemoteBase(service);
  86. },
  87. /**
  88. * Gets the base path for the given OCS API service.
  89. * @param {string} service name
  90. * @param {int} version OCS API version
  91. * @return {string} OCS API base path
  92. */
  93. linkToOCS: function(service, version) {
  94. version = (version !== 2) ? 1 : 2;
  95. return window.location.protocol + '//' + window.location.host + OC.getRootPath() + '/ocs/v' + version + '.php/' + service + '/';
  96. },
  97. /**
  98. * Generates the absolute url for the given relative url, which can contain parameters.
  99. * Parameters will be URL encoded automatically.
  100. * @param {string} url
  101. * @param [params] params
  102. * @param [options] options
  103. * @param {bool} [options.escape=true] enable/disable auto escape of placeholders (by default enabled)
  104. * @return {string} Absolute URL for the given relative URL
  105. */
  106. generateUrl: function(url, params, options) {
  107. var defaultOptions = {
  108. escape: true
  109. },
  110. allOptions = options || {};
  111. _.defaults(allOptions, defaultOptions);
  112. var _build = function (text, vars) {
  113. vars = vars || [];
  114. return text.replace(/{([^{}]*)}/g,
  115. function (a, b) {
  116. var r = (vars[b]);
  117. if(allOptions.escape) {
  118. return (typeof r === 'string' || typeof r === 'number') ? encodeURIComponent(r) : encodeURIComponent(a);
  119. } else {
  120. return (typeof r === 'string' || typeof r === 'number') ? r : a;
  121. }
  122. }
  123. );
  124. };
  125. if (url.charAt(0) !== '/') {
  126. url = '/' + url;
  127. }
  128. if(oc_config.modRewriteWorking == true) {
  129. return OC.getRootPath() + _build(url, params);
  130. }
  131. return OC.getRootPath() + '/index.php' + _build(url, params);
  132. },
  133. /**
  134. * Get the absolute url for a file in an app
  135. * @param {string} app the id of the app
  136. * @param {string} type the type of the file to link to (e.g. css,img,ajax.template)
  137. * @param {string} file the filename
  138. * @return {string} Absolute URL for a file in an app
  139. */
  140. filePath:function(app,type,file){
  141. var isCore=OC.coreApps.indexOf(app)!==-1,
  142. link=OC.getRootPath();
  143. if(file.substring(file.length-3) === 'php' && !isCore){
  144. link+='/index.php/apps/' + app;
  145. if (file != 'index.php') {
  146. link+='/';
  147. if(type){
  148. link+=encodeURI(type + '/');
  149. }
  150. link+= file;
  151. }
  152. }else if(file.substring(file.length-3) !== 'php' && !isCore){
  153. link=OC.appswebroots[app];
  154. if(type){
  155. link+= '/'+type+'/';
  156. }
  157. if(link.substring(link.length-1) !== '/'){
  158. link+='/';
  159. }
  160. link+=file;
  161. }else{
  162. if ((app == 'settings' || app == 'core' || app == 'search') && type == 'ajax') {
  163. link+='/index.php/';
  164. }
  165. else {
  166. link+='/';
  167. }
  168. if(!isCore){
  169. link+='apps/';
  170. }
  171. if (app !== '') {
  172. app+='/';
  173. link+=app;
  174. }
  175. if(type){
  176. link+=type+'/';
  177. }
  178. link+=file;
  179. }
  180. return link;
  181. },
  182. /**
  183. * Check if a user file is allowed to be handled.
  184. * @param {string} file to check
  185. */
  186. fileIsBlacklisted: function(file) {
  187. return !!(file.match(oc_config.blacklist_files_regex));
  188. },
  189. /**
  190. * Redirect to the target URL, can also be used for downloads.
  191. * @param {string} targetURL URL to redirect to
  192. */
  193. redirect: function(targetURL) {
  194. window.location = targetURL;
  195. },
  196. /**
  197. * Reloads the current page
  198. */
  199. reload: function() {
  200. window.location.reload();
  201. },
  202. /**
  203. * Protocol that is used to access this Nextcloud instance
  204. * @return {string} Used protocol
  205. */
  206. getProtocol: function() {
  207. return window.location.protocol.split(':')[0];
  208. },
  209. /**
  210. * Returns the host used to access this Nextcloud instance
  211. * Host is sometimes the same as the hostname but now always.
  212. *
  213. * Examples:
  214. * http://example.com => example.com
  215. * https://example.com => example.com
  216. * http://example.com:8080 => example.com:8080
  217. *
  218. * @return {string} host
  219. *
  220. * @since 8.2
  221. */
  222. getHost: function() {
  223. return window.location.host;
  224. },
  225. /**
  226. * Returns the hostname used to access this Nextcloud instance
  227. * The hostname is always stripped of the port
  228. *
  229. * @return {string} hostname
  230. * @since 9.0
  231. */
  232. getHostName: function() {
  233. return window.location.hostname;
  234. },
  235. /**
  236. * Returns the port number used to access this Nextcloud instance
  237. *
  238. * @return {int} port number
  239. *
  240. * @since 8.2
  241. */
  242. getPort: function() {
  243. return window.location.port;
  244. },
  245. /**
  246. * Returns the web root path where this Nextcloud instance
  247. * is accessible, with a leading slash.
  248. * For example "/nextcloud".
  249. *
  250. * @return {string} web root path
  251. *
  252. * @since 8.2
  253. */
  254. getRootPath: function() {
  255. return OC.webroot;
  256. },
  257. /**
  258. * Returns the capabilities
  259. *
  260. * @return {array} capabilities
  261. *
  262. * @since 14.0
  263. */
  264. getCapabilities: function() {
  265. return OC._capabilities;
  266. },
  267. /**
  268. * Returns the currently logged in user or null if there is no logged in
  269. * user (public page mode)
  270. *
  271. * @return {OC.CurrentUser} user spec
  272. * @since 9.0.0
  273. */
  274. getCurrentUser: function() {
  275. if (_.isUndefined(this._currentUserDisplayName)) {
  276. this._currentUserDisplayName = document.getElementsByTagName('head')[0].getAttribute('data-user-displayname');
  277. }
  278. return {
  279. uid: this.currentUser,
  280. displayName: this._currentUserDisplayName
  281. };
  282. },
  283. /**
  284. * get the absolute path to an image file
  285. * if no extension is given for the image, it will automatically decide
  286. * between .png and .svg based on what the browser supports
  287. * @param {string} app the app id to which the image belongs
  288. * @param {string} file the name of the image file
  289. * @return {string}
  290. */
  291. imagePath:function(app,file){
  292. if(file.indexOf('.')==-1){//if no extension is given, use svg
  293. file+='.svg';
  294. }
  295. return OC.filePath(app,'img',file);
  296. },
  297. /**
  298. * URI-Encodes a file path but keep the path slashes.
  299. *
  300. * @param path path
  301. * @return encoded path
  302. */
  303. encodePath: function(path) {
  304. if (!path) {
  305. return path;
  306. }
  307. var parts = path.split('/');
  308. var result = [];
  309. for (var i = 0; i < parts.length; i++) {
  310. result.push(encodeURIComponent(parts[i]));
  311. }
  312. return result.join('/');
  313. },
  314. /**
  315. * Load a script for the server and load it. If the script is already loaded,
  316. * the event handler will be called directly
  317. * @param {string} app the app id to which the script belongs
  318. * @param {string} script the filename of the script
  319. * @param ready event handler to be called when the script is loaded
  320. * @deprecated 16.0.0 Use OCP.Loader.loadScript
  321. */
  322. addScript:function(app,script,ready){
  323. var deferred, path=OC.filePath(app,'js',script+'.js');
  324. if(!OC.addScript.loaded[path]) {
  325. deferred = $.Deferred();
  326. $.getScript(path, function() {
  327. deferred.resolve();
  328. });
  329. OC.addScript.loaded[path] = deferred;
  330. } else {
  331. if (ready) {
  332. ready();
  333. }
  334. }
  335. return OC.addScript.loaded[path];
  336. },
  337. /**
  338. * Loads a CSS file
  339. * @param {string} app the app id to which the css style belongs
  340. * @param {string} style the filename of the css file
  341. * @deprecated 16.0.0 Use OCP.Loader.loadStylesheet
  342. */
  343. addStyle:function(app,style){
  344. var path=OC.filePath(app,'css',style+'.css');
  345. if(OC.addStyle.loaded.indexOf(path)===-1){
  346. OC.addStyle.loaded.push(path);
  347. if (document.createStyleSheet) {
  348. document.createStyleSheet(path);
  349. } else {
  350. style=$('<link rel="stylesheet" type="text/css" href="'+path+'"/>');
  351. $('head').append(style);
  352. }
  353. }
  354. },
  355. /**
  356. * Loads translations for the given app asynchronously.
  357. *
  358. * @param {String} app app name
  359. * @param {Function} callback callback to call after loading
  360. * @return {Promise}
  361. */
  362. addTranslations: function(app, callback) {
  363. return OC.L10N.load(app, callback);
  364. },
  365. /**
  366. * Returns the base name of the given path.
  367. * For example for "/abc/somefile.txt" it will return "somefile.txt"
  368. *
  369. * @param {String} path
  370. * @return {String} base name
  371. */
  372. basename: function(path) {
  373. return path.replace(/\\/g,'/').replace( /.*\//, '' );
  374. },
  375. /**
  376. * Returns the dir name of the given path.
  377. * For example for "/abc/somefile.txt" it will return "/abc"
  378. *
  379. * @param {String} path
  380. * @return {String} dir name
  381. */
  382. dirname: function(path) {
  383. return path.replace(/\\/g,'/').replace(/\/[^\/]*$/, '');
  384. },
  385. /**
  386. * Returns whether the given paths are the same, without
  387. * leading, trailing or doubled slashes and also removing
  388. * the dot sections.
  389. *
  390. * @param {String} path1 first path
  391. * @param {String} path2 second path
  392. * @return {bool} true if the paths are the same
  393. *
  394. * @since 9.0
  395. */
  396. isSamePath: function(path1, path2) {
  397. var filterDot = function(p) {
  398. return p !== '.';
  399. };
  400. var pathSections1 = _.filter((path1 || '').split('/'), filterDot);
  401. var pathSections2 = _.filter((path2 || '').split('/'), filterDot);
  402. path1 = OC.joinPaths.apply(OC, pathSections1);
  403. path2 = OC.joinPaths.apply(OC, pathSections2);
  404. return path1 === path2;
  405. },
  406. /**
  407. * Join path sections
  408. *
  409. * @param {...String} path sections
  410. *
  411. * @return {String} joined path, any leading or trailing slash
  412. * will be kept
  413. *
  414. * @since 8.2
  415. */
  416. joinPaths: function() {
  417. if (arguments.length < 1) {
  418. return '';
  419. }
  420. var path = '';
  421. // convert to array
  422. var args = Array.prototype.slice.call(arguments);
  423. // discard empty arguments
  424. args = _.filter(args, function(arg) {
  425. return arg.length > 0;
  426. });
  427. if (args.length < 1) {
  428. return '';
  429. }
  430. var lastArg = args[args.length - 1];
  431. var leadingSlash = args[0].charAt(0) === '/';
  432. var trailingSlash = lastArg.charAt(lastArg.length - 1) === '/';
  433. var sections = [];
  434. var i;
  435. for (i = 0; i < args.length; i++) {
  436. sections = sections.concat(args[i].split('/'));
  437. }
  438. var first = !leadingSlash;
  439. for (i = 0; i < sections.length; i++) {
  440. if (sections[i] !== '') {
  441. if (first) {
  442. first = false;
  443. } else {
  444. path += '/';
  445. }
  446. path += sections[i];
  447. }
  448. }
  449. if (trailingSlash) {
  450. // add it back
  451. path += '/';
  452. }
  453. return path;
  454. },
  455. /**
  456. * Dialog helper for jquery dialogs.
  457. *
  458. * @namespace OC.dialogs
  459. */
  460. dialogs:OCdialogs,
  461. /**
  462. * Parses a URL query string into a JS map
  463. * @param {string} queryString query string in the format param1=1234&param2=abcde&param3=xyz
  464. * @return {Object.<string, string>} map containing key/values matching the URL parameters
  465. */
  466. parseQueryString:function(queryString){
  467. var parts,
  468. pos,
  469. components,
  470. result = {},
  471. key,
  472. value;
  473. if (!queryString){
  474. return null;
  475. }
  476. pos = queryString.indexOf('?');
  477. if (pos >= 0){
  478. queryString = queryString.substr(pos + 1);
  479. }
  480. parts = queryString.replace(/\+/g, '%20').split('&');
  481. for (var i = 0; i < parts.length; i++){
  482. // split on first equal sign
  483. var part = parts[i];
  484. pos = part.indexOf('=');
  485. if (pos >= 0) {
  486. components = [
  487. part.substr(0, pos),
  488. part.substr(pos + 1)
  489. ];
  490. }
  491. else {
  492. // key only
  493. components = [part];
  494. }
  495. if (!components.length){
  496. continue;
  497. }
  498. key = decodeURIComponent(components[0]);
  499. if (!key){
  500. continue;
  501. }
  502. // if equal sign was there, return string
  503. if (components.length > 1) {
  504. result[key] = decodeURIComponent(components[1]);
  505. }
  506. // no equal sign => null value
  507. else {
  508. result[key] = null;
  509. }
  510. }
  511. return result;
  512. },
  513. /**
  514. * Builds a URL query from a JS map.
  515. * @param {Object.<string, string>} params map containing key/values matching the URL parameters
  516. * @return {string} String containing a URL query (without question) mark
  517. */
  518. buildQueryString: function(params) {
  519. if (!params) {
  520. return '';
  521. }
  522. return $.map(params, function(value, key) {
  523. var s = encodeURIComponent(key);
  524. if (value !== null && typeof(value) !== 'undefined') {
  525. s += '=' + encodeURIComponent(value);
  526. }
  527. return s;
  528. }).join('&');
  529. },
  530. /**
  531. * Opens a popup with the setting for an app.
  532. * @param {string} appid The ID of the app e.g. 'calendar', 'contacts' or 'files'.
  533. * @param {boolean|string} loadJS If true 'js/settings.js' is loaded. If it's a string
  534. * it will attempt to load a script by that name in the 'js' directory.
  535. * @param {boolean} [cache] If true the javascript file won't be forced refreshed. Defaults to true.
  536. * @param {string} [scriptName] The name of the PHP file to load. Defaults to 'settings.php' in
  537. * the root of the app directory hierarchy.
  538. */
  539. appSettings:function(args) {
  540. if(typeof args === 'undefined' || typeof args.appid === 'undefined') {
  541. throw { name: 'MissingParameter', message: 'The parameter appid is missing' };
  542. }
  543. var props = {scriptName:'settings.php', cache:true};
  544. $.extend(props, args);
  545. var settings = $('#appsettings');
  546. if(settings.length === 0) {
  547. throw { name: 'MissingDOMElement', message: 'There has be be an element with id "appsettings" for the popup to show.' };
  548. }
  549. var popup = $('#appsettings_popup');
  550. if(popup.length === 0) {
  551. $('body').prepend('<div class="popup hidden" id="appsettings_popup"></div>');
  552. popup = $('#appsettings_popup');
  553. popup.addClass(settings.hasClass('topright') ? 'topright' : 'bottomleft');
  554. }
  555. if(popup.is(':visible')) {
  556. popup.hide().remove();
  557. } else {
  558. var arrowclass = settings.hasClass('topright') ? 'up' : 'left';
  559. var jqxhr = $.get(OC.filePath(props.appid, '', props.scriptName), function(data) {
  560. popup.html(data).ready(function() {
  561. popup.prepend('<span class="arrow '+arrowclass+'"></span><h2>'+t('core', 'Settings')+'</h2><a class="close"></a>').show();
  562. popup.find('.close').bind('click', function() {
  563. popup.remove();
  564. });
  565. if(typeof props.loadJS !== 'undefined') {
  566. var scriptname;
  567. if(props.loadJS === true) {
  568. scriptname = 'settings.js';
  569. } else if(typeof props.loadJS === 'string') {
  570. scriptname = props.loadJS;
  571. } else {
  572. throw { name: 'InvalidParameter', message: 'The "loadJS" parameter must be either boolean or a string.' };
  573. }
  574. if(props.cache) {
  575. $.ajaxSetup({cache: true});
  576. }
  577. $.getScript(OC.filePath(props.appid, 'js', scriptname))
  578. .fail(function(jqxhr, settings, e) {
  579. throw e;
  580. });
  581. }
  582. }).show();
  583. }, 'html');
  584. }
  585. },
  586. /**
  587. * For menu toggling
  588. * @todo Write documentation
  589. *
  590. * @param {jQuery} $toggle
  591. * @param {jQuery} $menuEl
  592. * @param {function|undefined} toggle callback invoked everytime the menu is opened
  593. * @param {boolean} headerMenu is this a top right header menu?
  594. * @returns {undefined}
  595. */
  596. registerMenu: function($toggle, $menuEl, toggle, headerMenu) {
  597. var self = this;
  598. $menuEl.addClass('menu');
  599. // On link, the enter key trigger a click event
  600. // Only use the click to avoid two fired events
  601. $toggle.on($toggle.prop('tagName') === 'A'
  602. ? 'click.menu'
  603. : 'click.menu keyup.menu', function(event) {
  604. // prevent the link event (append anchor to URL)
  605. event.preventDefault();
  606. // allow enter key as a trigger
  607. if (event.key && event.key !== "Enter") {
  608. return;
  609. }
  610. if ($menuEl.is(OC._currentMenu)) {
  611. self.hideMenus();
  612. return;
  613. }
  614. // another menu was open?
  615. else if (OC._currentMenu) {
  616. // close it
  617. self.hideMenus();
  618. }
  619. if (headerMenu === true) {
  620. $menuEl.parent().addClass('openedMenu');
  621. }
  622. // Set menu to expanded
  623. $toggle.attr('aria-expanded', true);
  624. $menuEl.slideToggle(OC.menuSpeed, toggle);
  625. OC._currentMenu = $menuEl;
  626. OC._currentMenuToggle = $toggle;
  627. });
  628. },
  629. /**
  630. * @todo Write documentation
  631. */
  632. unregisterMenu: function($toggle, $menuEl) {
  633. // close menu if opened
  634. if ($menuEl.is(OC._currentMenu)) {
  635. this.hideMenus();
  636. }
  637. $toggle.off('click.menu').removeClass('menutoggle');
  638. $menuEl.removeClass('menu');
  639. },
  640. /**
  641. * Hides any open menus
  642. *
  643. * @param {Function} complete callback when the hiding animation is done
  644. */
  645. hideMenus: function(complete) {
  646. if (OC._currentMenu) {
  647. var lastMenu = OC._currentMenu;
  648. OC._currentMenu.trigger(new $.Event('beforeHide'));
  649. OC._currentMenu.slideUp(OC.menuSpeed, function() {
  650. lastMenu.trigger(new $.Event('afterHide'));
  651. if (complete) {
  652. complete.apply(this, arguments);
  653. }
  654. });
  655. }
  656. // Set menu to closed
  657. $('.menutoggle').attr('aria-expanded', false);
  658. $('.openedMenu').removeClass('openedMenu');
  659. OC._currentMenu = null;
  660. OC._currentMenuToggle = null;
  661. },
  662. /**
  663. * Shows a given element as menu
  664. *
  665. * @param {Object} [$toggle=null] menu toggle
  666. * @param {Object} $menuEl menu element
  667. * @param {Function} complete callback when the showing animation is done
  668. */
  669. showMenu: function($toggle, $menuEl, complete) {
  670. if ($menuEl.is(OC._currentMenu)) {
  671. return;
  672. }
  673. this.hideMenus();
  674. OC._currentMenu = $menuEl;
  675. OC._currentMenuToggle = $toggle;
  676. $menuEl.trigger(new $.Event('beforeShow'));
  677. $menuEl.show();
  678. $menuEl.trigger(new $.Event('afterShow'));
  679. // no animation
  680. if (_.isFunction(complete)) {
  681. complete();
  682. }
  683. },
  684. /**
  685. * Returns the user's locale as a BCP 47 compliant language tag
  686. *
  687. * @return {String} locale string
  688. */
  689. getCanonicalLocale: function() {
  690. var locale = this.getLocale();
  691. return typeof locale === 'string' ? locale.replace(/_/g, '-') : locale;
  692. },
  693. /**
  694. * Returns the user's locale
  695. *
  696. * @return {String} locale string
  697. */
  698. getLocale: function() {
  699. return $('html').data('locale');
  700. },
  701. /**
  702. * Returns the user's language
  703. *
  704. * @returns {String} language string
  705. */
  706. getLanguage: function () {
  707. return $('html').prop('lang');
  708. },
  709. /**
  710. * Returns whether the current user is an administrator
  711. *
  712. * @return {bool} true if the user is an admin, false otherwise
  713. * @since 9.0.0
  714. */
  715. isUserAdmin: function() {
  716. return oc_isadmin;
  717. },
  718. /**
  719. * Warn users that the connection to the server was lost temporarily
  720. *
  721. * This function is throttled to prevent stacked notfications.
  722. * After 7sec the first notification is gone, then we can show another one
  723. * if necessary.
  724. */
  725. _ajaxConnectionLostHandler: _.throttle(function() {
  726. OC.Notification.showTemporary(t('core', 'Connection to server lost'));
  727. }, 7 * 1000, {trailing: false}),
  728. /**
  729. * Process ajax error, redirects to main page
  730. * if an error/auth error status was returned.
  731. */
  732. _processAjaxError: function(xhr) {
  733. var self = this;
  734. // purposefully aborted request ?
  735. // this._userIsNavigatingAway needed to distinguish ajax calls cancelled by navigating away
  736. // from calls cancelled by failed cross-domain ajax due to SSO redirect
  737. if (xhr.status === 0 && (xhr.statusText === 'abort' || xhr.statusText === 'timeout' || self._reloadCalled)) {
  738. return;
  739. }
  740. if (_.contains([302, 303, 307, 401], xhr.status) && OC.currentUser) {
  741. // sometimes "beforeunload" happens later, so need to defer the reload a bit
  742. setTimeout(function() {
  743. if (!self._userIsNavigatingAway && !self._reloadCalled) {
  744. var timer = 0;
  745. var seconds = 5;
  746. var interval = setInterval( function() {
  747. OC.Notification.showUpdate(n('core', 'Problem loading page, reloading in %n second', 'Problem loading page, reloading in %n seconds', seconds - timer));
  748. if (timer >= seconds) {
  749. clearInterval(interval);
  750. OC.reload();
  751. }
  752. timer++;
  753. }, 1000 // 1 second interval
  754. );
  755. // only call reload once
  756. self._reloadCalled = true;
  757. }
  758. }, 100);
  759. } else if(xhr.status === 0) {
  760. // Connection lost (e.g. WiFi disconnected or server is down)
  761. setTimeout(function() {
  762. if (!self._userIsNavigatingAway && !self._reloadCalled) {
  763. self._ajaxConnectionLostHandler();
  764. }
  765. }, 100);
  766. }
  767. },
  768. /**
  769. * Registers XmlHttpRequest object for global error processing.
  770. *
  771. * This means that if this XHR object returns 401 or session timeout errors,
  772. * the current page will automatically be reloaded.
  773. *
  774. * @param {XMLHttpRequest} xhr
  775. */
  776. registerXHRForErrorProcessing: function(xhr) {
  777. var loadCallback = function() {
  778. if (xhr.readyState !== 4) {
  779. return;
  780. }
  781. if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
  782. return;
  783. }
  784. // fire jquery global ajax error handler
  785. $(document).trigger(new $.Event('ajaxError'), xhr);
  786. };
  787. var errorCallback = function() {
  788. // fire jquery global ajax error handler
  789. $(document).trigger(new $.Event('ajaxError'), xhr);
  790. };
  791. if (xhr.addEventListener) {
  792. xhr.addEventListener('load', loadCallback);
  793. xhr.addEventListener('error', errorCallback);
  794. }
  795. }
  796. });
  797. OC.addStyle.loaded=[];
  798. OC.addScript.loaded=[];
  799. /**
  800. * Initializes core
  801. */
  802. function initCore() {
  803. /**
  804. * Disable automatic evaluation of responses for $.ajax() functions (and its
  805. * higher-level alternatives like $.get() and $.post()).
  806. *
  807. * If a response to a $.ajax() request returns a content type of "application/javascript"
  808. * JQuery would previously execute the response body. This is a pretty unexpected
  809. * behaviour and can result in a bypass of our Content-Security-Policy as well as
  810. * multiple unexpected XSS vectors.
  811. */
  812. $.ajaxSetup({
  813. contents: {
  814. script: false
  815. }
  816. });
  817. /**
  818. * Disable execution of eval in jQuery. We do require an allowed eval CSP
  819. * configuration at the moment for handlebars et al. But for jQuery there is
  820. * not much of a reason to execute JavaScript directly via eval.
  821. *
  822. * This thus mitigates some unexpected XSS vectors.
  823. */
  824. jQuery.globalEval = function(){};
  825. /**
  826. * Set users locale to moment.js as soon as possible
  827. */
  828. moment.locale(OC.getLocale());
  829. var userAgent = window.navigator.userAgent;
  830. var msie = userAgent.indexOf('MSIE ');
  831. var trident = userAgent.indexOf('Trident/');
  832. var edge = userAgent.indexOf('Edge/');
  833. if (msie > 0 || trident > 0) {
  834. // (IE 10 or older) || IE 11
  835. $('html').addClass('ie');
  836. } else if (edge > 0) {
  837. // for edge
  838. $('html').addClass('edge');
  839. }
  840. // css variables fallback for IE
  841. if (msie > 0 || trident > 0) {
  842. cssVars({
  843. watch: true
  844. });
  845. }
  846. $(window).on('unload.main', function() {
  847. OC._unloadCalled = true;
  848. });
  849. $(window).on('beforeunload.main', function() {
  850. // super-trick thanks to http://stackoverflow.com/a/4651049
  851. // in case another handler displays a confirmation dialog (ex: navigating away
  852. // during an upload), there are two possible outcomes: user clicked "ok" or
  853. // "cancel"
  854. // first timeout handler is called after unload dialog is closed
  855. setTimeout(function() {
  856. OC._userIsNavigatingAway = true;
  857. // second timeout event is only called if user cancelled (Chrome),
  858. // but in other browsers it might still be triggered, so need to
  859. // set a higher delay...
  860. setTimeout(function() {
  861. if (!OC._unloadCalled) {
  862. OC._userIsNavigatingAway = false;
  863. }
  864. }, 10000);
  865. },1);
  866. });
  867. $(document).on('ajaxError.main', function( event, request, settings ) {
  868. if (settings && settings.allowAuthErrors) {
  869. return;
  870. }
  871. OC._processAjaxError(request);
  872. });
  873. /**
  874. * Calls the server periodically to ensure that session and CSRF
  875. * token doesn't expire
  876. */
  877. function initSessionHeartBeat() {
  878. // interval in seconds
  879. var interval = NaN;
  880. if (oc_config.session_lifetime) {
  881. interval = Math.floor(oc_config.session_lifetime / 2);
  882. }
  883. interval = isNaN(interval)? 900: interval;
  884. // minimum one minute
  885. interval = Math.max(60, interval);
  886. // max interval in seconds set to 24 hours
  887. interval = Math.min(24 * 3600, interval);
  888. var url = OC.generateUrl('/csrftoken');
  889. setInterval(function() {
  890. $.ajax(url).then(function(resp) {
  891. oc_requesttoken = resp.token;
  892. OC.requestToken = resp.token;
  893. }).fail(function(e) {
  894. console.error('session heartbeat failed', e);
  895. });
  896. }, interval * 1000);
  897. }
  898. // session heartbeat (defaults to enabled)
  899. if (typeof(oc_config.session_keepalive) === 'undefined' ||
  900. !!oc_config.session_keepalive) {
  901. initSessionHeartBeat();
  902. }
  903. OC.registerMenu($('#expand'), $('#expanddiv'), false, true);
  904. // toggle for menus
  905. //$(document).on('mouseup.closemenus keyup', function(event) {
  906. $(document).on('mouseup.closemenus', function(event) {
  907. // allow enter as a trigger
  908. // if (event.key && event.key !== "Enter") {
  909. // return;
  910. // }
  911. var $el = $(event.target);
  912. if ($el.closest('.menu').length || $el.closest('.menutoggle').length) {
  913. // don't close when clicking on the menu directly or a menu toggle
  914. return false;
  915. }
  916. OC.hideMenus();
  917. });
  918. /**
  919. * Set up the main menu toggle to react to media query changes.
  920. * If the screen is small enough, the main menu becomes a toggle.
  921. * If the screen is bigger, the main menu is not a toggle any more.
  922. */
  923. function setupMainMenu() {
  924. // init the more-apps menu
  925. OC.registerMenu($('#more-apps > a'), $('#navigation'));
  926. // toggle the navigation
  927. var $toggle = $('#header .header-appname-container');
  928. var $navigation = $('#navigation');
  929. var $appmenu = $('#appmenu');
  930. // init the menu
  931. OC.registerMenu($toggle, $navigation);
  932. $toggle.data('oldhref', $toggle.attr('href'));
  933. $toggle.attr('href', '#');
  934. $navigation.hide();
  935. // show loading feedback on more apps list
  936. $navigation.delegate('a', 'click', function(event) {
  937. var $app = $(event.target);
  938. if(!$app.is('a')) {
  939. $app = $app.closest('a');
  940. }
  941. if(event.which === 1 && !event.ctrlKey && !event.metaKey) {
  942. $app.find('svg').remove();
  943. $app.find('div').remove(); // prevent odd double-clicks
  944. // no need for theming, loader is already inverted on dark mode
  945. // but we need it over the primary colour
  946. $app.prepend($('<div/>').addClass('icon-loading-small'));
  947. } else {
  948. // Close navigation when opening app in
  949. // a new tab
  950. OC.hideMenus(function(){return false;});
  951. }
  952. });
  953. $navigation.delegate('a', 'mouseup', function(event) {
  954. if(event.which === 2) {
  955. // Close navigation when opening app in
  956. // a new tab via middle click
  957. OC.hideMenus(function(){return false;});
  958. }
  959. });
  960. // show loading feedback on visible apps list
  961. $appmenu.delegate('li:not(#more-apps) > a', 'click', function(event) {
  962. var $app = $(event.target);
  963. if(!$app.is('a')) {
  964. $app = $app.closest('a');
  965. }
  966. if(event.which === 1 && !event.ctrlKey && !event.metaKey && $app.parent('#more-apps').length === 0) {
  967. $app.find('svg').remove();
  968. $app.find('div').remove(); // prevent odd double-clicks
  969. $app.prepend($('<div/>').addClass(
  970. OCA.Theming && OCA.Theming.inverted
  971. ? 'icon-loading-small'
  972. : 'icon-loading-small-dark'
  973. ));
  974. } else {
  975. // Close navigation when opening app in
  976. // a new tab
  977. OC.hideMenus(function(){return false;});
  978. }
  979. });
  980. }
  981. function setupUserMenu() {
  982. var $menu = $('#header #settings');
  983. // show loading feedback
  984. $menu.delegate('a', 'click', function(event) {
  985. var $page = $(event.target);
  986. if (!$page.is('a')) {
  987. $page = $page.closest('a');
  988. }
  989. if(event.which === 1 && !event.ctrlKey && !event.metaKey) {
  990. $page.find('img').remove();
  991. $page.find('div').remove(); // prevent odd double-clicks
  992. $page.prepend($('<div/>').addClass('icon-loading-small'));
  993. } else {
  994. // Close navigation when opening menu entry in
  995. // a new tab
  996. OC.hideMenus(function(){return false;});
  997. }
  998. });
  999. $menu.delegate('a', 'mouseup', function(event) {
  1000. if(event.which === 2) {
  1001. // Close navigation when opening app in
  1002. // a new tab via middle click
  1003. OC.hideMenus(function(){return false;});
  1004. }
  1005. });
  1006. }
  1007. function setupContactsMenu() {
  1008. new OC.ContactsMenu({
  1009. el: $('#contactsmenu .menu'),
  1010. trigger: $('#contactsmenu .menutoggle')
  1011. });
  1012. }
  1013. setupMainMenu();
  1014. setupUserMenu();
  1015. setupContactsMenu();
  1016. // move triangle of apps dropdown to align with app name triangle
  1017. // 2 is the additional offset between the triangles
  1018. if($('#navigation').length) {
  1019. $('#header #nextcloud + .menutoggle').on('click', function(){
  1020. $('#menu-css-helper').remove();
  1021. var caretPosition = $('.header-appname + .icon-caret').offset().left - 2;
  1022. if(caretPosition > 255) {
  1023. // if the app name is longer than the menu, just put the triangle in the middle
  1024. return;
  1025. } else {
  1026. $('head').append('<style id="menu-css-helper">#navigation:after { left: '+ caretPosition +'px; }</style>');
  1027. }
  1028. });
  1029. $('#header #appmenu .menutoggle').on('click', function() {
  1030. $('#appmenu').toggleClass('menu-open');
  1031. if($('#appmenu').is(':visible')) {
  1032. $('#menu-css-helper').remove();
  1033. }
  1034. });
  1035. }
  1036. var resizeMenu = function() {
  1037. var appList = $('#appmenu li');
  1038. var rightHeaderWidth = $('.header-right').outerWidth();
  1039. var headerWidth = $('header').outerWidth();
  1040. var usePercentualAppMenuLimit = 0.33;
  1041. var minAppsDesktop = 8;
  1042. var availableWidth = headerWidth - $('#nextcloud').outerWidth() - (rightHeaderWidth > 210 ? rightHeaderWidth : 210)
  1043. var isMobile = $(window).width() < 768;
  1044. if (!isMobile) {
  1045. availableWidth = availableWidth * usePercentualAppMenuLimit;
  1046. }
  1047. var appCount = Math.floor((availableWidth / $(appList).width()));
  1048. if (isMobile && appCount > minAppsDesktop) {
  1049. appCount = minAppsDesktop;
  1050. }
  1051. if (!isMobile && appCount < minAppsDesktop) {
  1052. appCount = minAppsDesktop;
  1053. }
  1054. // show at least 2 apps in the popover
  1055. if(appList.length-1-appCount >= 1) {
  1056. appCount--;
  1057. }
  1058. $('#more-apps a').removeClass('active');
  1059. var lastShownApp;
  1060. for (var k = 0; k < appList.length-1; k++) {
  1061. var name = $(appList[k]).data('id');
  1062. if(k < appCount) {
  1063. $(appList[k]).removeClass('hidden');
  1064. $('#apps li[data-id=' + name + ']').addClass('in-header');
  1065. lastShownApp = appList[k];
  1066. } else {
  1067. $(appList[k]).addClass('hidden');
  1068. $('#apps li[data-id=' + name + ']').removeClass('in-header');
  1069. // move active app to last position if it is active
  1070. if(appCount > 0 && $(appList[k]).children('a').hasClass('active')) {
  1071. $(lastShownApp).addClass('hidden');
  1072. $('#apps li[data-id=' + $(lastShownApp).data('id') + ']').removeClass('in-header');
  1073. $(appList[k]).removeClass('hidden');
  1074. $('#apps li[data-id=' + name + ']').addClass('in-header');
  1075. }
  1076. }
  1077. }
  1078. // show/hide more apps icon
  1079. if($('#apps li:not(.in-header)').length === 0) {
  1080. $('#more-apps').hide();
  1081. $('#navigation').hide();
  1082. } else {
  1083. $('#more-apps').show();
  1084. }
  1085. };
  1086. $(window).resize(resizeMenu);
  1087. setTimeout(resizeMenu, 0);
  1088. // just add snapper for logged in users
  1089. if($('#app-navigation').length && !$('html').hasClass('lte9')) {
  1090. // App sidebar on mobile
  1091. var snapper = new Snap({
  1092. element: document.getElementById('app-content'),
  1093. disable: 'right',
  1094. maxPosition: 300, // $navigation-width
  1095. minDragDistance: 100
  1096. });
  1097. $('#app-content').prepend('<div id="app-navigation-toggle" class="icon-menu" style="display:none;" tabindex="0"></div>');
  1098. var toggleSnapperOnButton = function(){
  1099. if(snapper.state().state == 'left'){
  1100. snapper.close();
  1101. } else {
  1102. snapper.open('left');
  1103. }
  1104. };
  1105. $('#app-navigation-toggle').click(function(){
  1106. toggleSnapperOnButton();
  1107. });
  1108. $('#app-navigation-toggle').keypress(function(e) {
  1109. if(e.which == 13) {
  1110. toggleSnapperOnButton();
  1111. }
  1112. });
  1113. // close sidebar when switching navigation entry
  1114. var $appNavigation = $('#app-navigation');
  1115. $appNavigation.delegate('a, :button', 'click', function(event) {
  1116. var $target = $(event.target);
  1117. // don't hide navigation when changing settings or adding things
  1118. if($target.is('.app-navigation-noclose') ||
  1119. $target.closest('.app-navigation-noclose').length) {
  1120. return;
  1121. }
  1122. if($target.is('.app-navigation-entry-utils-menu-button') ||
  1123. $target.closest('.app-navigation-entry-utils-menu-button').length) {
  1124. return;
  1125. }
  1126. if($target.is('.add-new') ||
  1127. $target.closest('.add-new').length) {
  1128. return;
  1129. }
  1130. if($target.is('#app-settings') ||
  1131. $target.closest('#app-settings').length) {
  1132. return;
  1133. }
  1134. snapper.close();
  1135. });
  1136. var navigationBarSlideGestureEnabled = false;
  1137. var navigationBarSlideGestureAllowed = true;
  1138. var navigationBarSlideGestureEnablePending = false;
  1139. OC.allowNavigationBarSlideGesture = function() {
  1140. navigationBarSlideGestureAllowed = true;
  1141. if (navigationBarSlideGestureEnablePending) {
  1142. snapper.enable();
  1143. navigationBarSlideGestureEnabled = true;
  1144. navigationBarSlideGestureEnablePending = false;
  1145. }
  1146. };
  1147. OC.disallowNavigationBarSlideGesture = function() {
  1148. navigationBarSlideGestureAllowed = false;
  1149. if (navigationBarSlideGestureEnabled) {
  1150. var endCurrentDrag = true;
  1151. snapper.disable(endCurrentDrag);
  1152. navigationBarSlideGestureEnabled = false;
  1153. navigationBarSlideGestureEnablePending = true;
  1154. }
  1155. };
  1156. var toggleSnapperOnSize = function() {
  1157. if($(window).width() > 768) {
  1158. snapper.close();
  1159. snapper.disable();
  1160. navigationBarSlideGestureEnabled = false;
  1161. navigationBarSlideGestureEnablePending = false;
  1162. } else if (navigationBarSlideGestureAllowed) {
  1163. snapper.enable();
  1164. navigationBarSlideGestureEnabled = true;
  1165. navigationBarSlideGestureEnablePending = false;
  1166. } else {
  1167. navigationBarSlideGestureEnablePending = true;
  1168. }
  1169. };
  1170. $(window).resize(_.debounce(toggleSnapperOnSize, 250));
  1171. // initial call
  1172. toggleSnapperOnSize();
  1173. }
  1174. // Update live timestamps every 30 seconds
  1175. setInterval(function() {
  1176. $('.live-relative-timestamp').each(function() {
  1177. $(this).text(OC.Util.relativeModifiedDate(parseInt($(this).attr('data-timestamp'), 10)));
  1178. });
  1179. }, 30 * 1000);
  1180. OC.PasswordConfirmation.init();
  1181. }
  1182. $(document).ready(initCore);
  1183. /**
  1184. // fallback to hashchange when no history support
  1185. if (window.history.pushState) {
  1186. window.onpopstate = _.bind(OC.Util.History._onPopState, OC.Util.History);
  1187. }
  1188. else {
  1189. $(window).on('hashchange', _.bind(OC.Util.History._onPopState, OC.Util.History));
  1190. }
  1191. /**
  1192. * Get a variable by name
  1193. * @param {string} name
  1194. * @return {*}
  1195. */
  1196. OC.get=function(name) {
  1197. var namespaces = name.split(".");
  1198. var tail = namespaces.pop();
  1199. var context=window;
  1200. for(var i = 0; i < namespaces.length; i++) {
  1201. context = context[namespaces[i]];
  1202. if(!context){
  1203. return false;
  1204. }
  1205. }
  1206. return context[tail];
  1207. };
  1208. /**
  1209. * Set a variable by name
  1210. * @param {string} name
  1211. * @param {*} value
  1212. */
  1213. OC.set=function(name, value) {
  1214. var namespaces = name.split(".");
  1215. var tail = namespaces.pop();
  1216. var context=window;
  1217. for(var i = 0; i < namespaces.length; i++) {
  1218. if(!context[namespaces[i]]){
  1219. context[namespaces[i]]={};
  1220. }
  1221. context = context[namespaces[i]];
  1222. }
  1223. context[tail]=value;
  1224. };