/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ /** * Icinga.Loader * * This is where we take care of XHR requests, responses and failures. */ (function(Icinga, $) { 'use strict'; Icinga.Loader = function (icinga) { /** * YES, we need Icinga */ this.icinga = icinga; /** * Our base url */ this.baseUrl = icinga.config.baseUrl; this.failureNotice = null; /** * Pending requests */ this.requests = {}; this.iconCache = {}; this.autorefreshEnabled = true; }; Icinga.Loader.prototype = { initialize: function () { this.icinga.timer.register(this.autorefresh, this, 500); }, /** * Load the given URL to the given target * * @param {string} url URL to be loaded * @param {object} target Target jQuery element * @param {object} data Optional parameters, usually for POST requests * @param {string} method HTTP method, default is 'GET' * @param {string} action How to handle the response ('replace' or 'append'), default is 'replace' * @param {boolean} autorefresh Whether the cause is a autorefresh or not * @param {object} progressTimer A timer to be stopped when the request is done */ loadUrl: function (url, $target, data, method, action, autorefresh, progressTimer) { var id = null; // Default method is GET if ('undefined' === typeof method) { method = 'GET'; } if ('undefined' === typeof action) { action = 'replace'; } if ('undefined' === typeof autorefresh) { autorefresh = false; } this.icinga.logger.debug('Loading ', url, ' to ', $target); // We should do better and ignore requests without target and/or id if (typeof $target !== 'undefined' && $target.attr('id')) { id = $target.attr('id'); } // If we have a pending request for the same target... if (typeof this.requests[id] !== 'undefined') { if (autorefresh) { return false; } // ... ignore the new request if it is already pending with the same URL. Only abort GETs, as those // are the only methods that are guaranteed to return the same value if (this.requests[id].url === url && method === 'GET') { this.icinga.logger.debug('Request to ', url, ' is already running for ', $target); return this.requests[id]; } // ...or abort the former request otherwise this.icinga.logger.debug( 'Aborting pending request loading ', url, ' to ', $target ); this.requests[id].abort(); } // Not sure whether we need this Accept-header var headers = { 'X-Icinga-Accept': 'text/html' }; // Ask for a new window id in case we don't already have one if (this.icinga.ui.hasWindowId()) { headers['X-Icinga-WindowId'] = this.icinga.ui.getWindowId(); } else { headers['X-Icinga-WindowId'] = 'undefined'; } // This is jQuery's default content type var contentType = 'application/x-www-form-urlencoded; charset=UTF-8'; var isFormData = typeof window.FormData !== 'undefined' && data instanceof window.FormData; if (isFormData) { // Setting false is mandatory as the form's data // won't be recognized by the server otherwise contentType = false; } var self = this; var req = $.ajax({ type : method, url : url, data : data, headers: headers, context: self, contentType: contentType, processData: ! isFormData }); req.$target = $target; req.url = url; req.done(this.onResponse); req.fail(this.onFailure); req.complete(this.onComplete); req.autorefresh = autorefresh; req.action = action; req.addToHistory = true; req.progressTimer = progressTimer; if (id) { this.requests[id] = req; } if (! autorefresh) { req.$target.addClass('impact'); } this.icinga.ui.refreshDebug(); return req; }, /** * Mimic XHR form submission by using an iframe * * @param {object} $form The form being submitted * @param {string} action The form's action URL * @param {object} $target The target container */ submitFormToIframe: function ($form, action, $target) { var self = this; $form.prop('action', self.icinga.utils.addUrlParams(action, { '_frameUpload': true })); $form.prop('target', 'fileupload-frame-target'); $('#fileupload-frame-target').on('load', function (event) { var $frame = $(event.target); var $contents = $frame.contents(); var $redirectMeta = $contents.find('meta[name="redirectUrl"]'); if ($redirectMeta.length) { self.redirectToUrl($redirectMeta.attr('content'), $target); } else { // Fetch the frame's new content and paste it into the target self.renderContentToContainer( $contents.find('body').html(), $target, 'replace' ); } $frame.prop('src', 'about:blank'); // Clear the frame's dom $frame.off('load'); // Unbind the event as it's set on demand }); }, /** * Create an URL relative to the Icinga base Url, still unused * * @param {string} url Relative url */ url: function (url) { if (typeof url === 'undefined') { return this.baseUrl; } return this.baseUrl + url; }, stopPendingRequestsFor: function ($el) { var id; if (typeof $el === 'undefined' || ! (id = $el.attr('id'))) { return; } if (typeof this.requests[id] !== 'undefined') { this.requests[id].abort(); } }, filterAutorefreshingContainers: function () { return $(this).data('icingaRefresh') > 0; }, autorefresh: function () { var self = this; if (self.autorefreshEnabled !== true) { return; } $('.container').filter(this.filterAutorefreshingContainers).each(function (idx, el) { var $el = $(el); var id = $el.attr('id'); if (typeof self.requests[id] !== 'undefined') { self.icinga.logger.debug('No refresh, request pending for ', id); return; } var interval = $el.data('icingaRefresh'); var lastUpdate = $el.data('lastUpdate'); if (typeof interval === 'undefined' || ! interval) { self.icinga.logger.info('No interval, setting default', id); interval = 10; } if (typeof lastUpdate === 'undefined' || ! lastUpdate) { self.icinga.logger.info('No lastUpdate, setting one', id); $el.data('lastUpdate',(new Date()).getTime()); return; } interval = interval * 1000; // TODO: if ((lastUpdate + interval) > (new Date()).getTime()) { // self.icinga.logger.info( // 'Skipping refresh', // id, // lastUpdate, // interval, // (new Date()).getTime() // ); return; } if (self.loadUrl($el.data('icingaUrl'), $el, undefined, undefined, undefined, true) === false) { self.icinga.logger.debug( 'NOT autorefreshing ' + id + ', even if ' + interval + ' ms passed. Request pending?' ); } else { self.icinga.logger.debug( 'Autorefreshing ' + id + ' ' + interval + ' ms passed' ); } el = null; }); }, /** * Disable the autorefresh mechanism */ disableAutorefresh: function () { this.autorefreshEnabled = false; }, /** * Enable the autorefresh mechanism */ enableAutorefresh: function () { this.autorefreshEnabled = true; }, processNotificationHeader: function(req) { var header = req.getResponseHeader('X-Icinga-Notification'); var self = this; if (! header) return false; var list = header.split('&'); $.each(list, function(idx, el) { var parts = decodeURIComponent(el).split(' '); self.createNotice(parts.shift(), parts.join(' ')); }); return true; }, addUrlFlag: function(url, flag) { if (url.match(/\?/)) { return url + '&' + flag; } else { return url + '?' + flag; } }, /** * Process the X-Icinga-Redirect HTTP Response Header * * If the response includes the X-Icinga-Redirect header, redirects to the URL associated with the header. * * @param {object} req Current request * * @returns {boolean} Whether we're about to redirect */ processRedirectHeader: function(req) { var icinga = this.icinga, redirect = req.getResponseHeader('X-Icinga-Redirect'); if (! redirect) { return false; } redirect = decodeURIComponent(redirect); if (redirect.match(/__SELF__/)) { if (req.autorefresh) { // Redirect to the current window's URL in case it's an auto-refresh request. If authenticated // externally this ensures seamless re-login if the session's expired redirect = redirect.replace( /__SELF__/, encodeURIComponent( document.location.pathname + document.location.search + document.location.hash ) ); } else { // Redirect to the URL which required authentication. When clicking a link this ensures that we // redirect to the link's URL instead of the current window's URL (see above) redirect = redirect.replace(/__SELF__/, req.url); } } this.redirectToUrl( redirect, req.$target, req.url, req.getResponseHeader('X-Icinga-Rerender-Layout'), req.forceFocus, req.getResponseHeader('X-Icinga-Refresh') ); return true; }, /** * Redirect to the given url * * @param {string} url * @param {object} $target * @param {string} origin * @param {boolean} rerenderLayout */ redirectToUrl: function (url, $target, origin, rerenderLayout, forceFocus, autoRefreshInterval) { var icinga = this.icinga; if (typeof rerenderLayout === 'undefined') { rerenderLayout = false; } icinga.logger.debug( 'Got redirect for ', $target, ', URL was ' + url ); if (rerenderLayout) { var parts = url.split(/#!/); url = parts.shift(); var redirectionUrl = this.addUrlFlag(url, 'renderLayout'); var r = this.loadUrl(redirectionUrl, $('#layout')); r.historyUrl = url; if (parts.length) { r.loadNext = parts; } else if (!! document.location.hash) { // Retain detail URL if the layout is rerendered parts = document.location.hash.split('#!').splice(1); if (parts.length) { r.loadNext = $.grep(parts, function (url) { if (url !== origin) { icinga.logger.debug('Retaining detail url ' + url); return true; } icinga.logger.debug('Discarding detail url ' + url + ' as it\'s the origin of the redirect'); return false; }); } } } else { if (url.match(/#!/)) { var parts = url.split(/#!/); icinga.ui.layout2col(); this.loadUrl(parts.shift(), $('#col1')); this.loadUrl(parts.shift(), $('#col2')); } else { if ($target.attr('id') === 'col2') { // TODO: multicol if ($('#col1').data('icingaUrl').split('?')[0] === url.split('?')[0]) { icinga.ui.layout1col(); $target = $('#col1'); delete(this.requests['col2']); } } var req = this.loadUrl(url, $target); req.forceFocus = url === origin ? forceFocus : null; req.autoRefreshInterval = autoRefreshInterval; } } }, cacheLoadedIcons: function($container) { // TODO: this is just a prototype, disabled for now return; var self = this; $('img.icon', $container).each(function(idx, img) { var src = $(img).attr('src'); if (typeof self.iconCache[src] !== 'undefined') { return; } var cache = new Image(); cache.src = src self.iconCache[src] = cache; }); }, /** * Handle successful XHR response */ onResponse: function (data, textStatus, req) { var self = this; if (this.failureNotice !== null) { if (! this.failureNotice.hasClass('fading-out')) { this.failureNotice.remove(); } this.failureNotice = null; } var url = req.url; this.icinga.logger.debug( 'Got response for ', req.$target, ', URL was ' + url ); this.processNotificationHeader(req); var cssreload = req.getResponseHeader('X-Icinga-Reload-Css'); if (cssreload) { this.icinga.ui.reloadCss(); } if (req.getResponseHeader('X-Icinga-Redirect')) { return; } // div helps getting an XML tree var $resp = $('