2 * This object handles ajax requests for pages. It also
3 * handles the reloading of the main menu and scripts.
9 * @var {boolean} active Whether we are busy
13 * @var {object} source The object whose event initialized the request
17 * @var {object} xhr A reference to the ajax request that is currently running
21 * @var {object} lockedTargets, list of locked targets
24 // eslint-disable-next-line valid-jsdoc
26 * @var {Function} callback Callback to execute after a successful request
27 * Used by CommonActions from common.js
29 callback: function () {},
31 * @var {boolean} debug Makes noise in your Firebug console
35 * @var {object} $msgbox A reference to a jQuery object that links to a message
36 * box that is generated by Functions.ajaxShowMessage()
40 * Given the filename of a script, returns a hash to be
41 * used to refer to all the events registered for the file
43 * @param {string} key key The filename for which to get the event name
47 hash: function (key) {
49 /* https://burtleburtle.net/bob/hash/doobs.html#one */
51 var len = newKey.length;
54 for (; i < len; ++i) {
55 hash += newKey.charCodeAt(i);
62 return Math.abs(hash);
65 * Registers an onload event for a file
67 * @param {string} file The filename for which to register the event
68 * @param {Function} func The function to execute when the page is ready
70 * @return {self} For chaining
72 registerOnload: function (file, func) {
73 var eventName = 'onload_' + AJAX.hash(file);
74 $(document).on(eventName, func);
76 // eslint-disable-next-line no-console
78 // no need to translate
79 'Registered event ' + eventName + ' for file ' + file
85 * Registers a teardown event for a file. This is useful to execute functions
86 * that unbind events for page elements that are about to be removed.
88 * @param {string} file The filename for which to register the event
89 * @param {Function} func The function to execute when
90 * the page is about to be torn down
92 * @return {self} For chaining
94 registerTeardown: function (file, func) {
95 var eventName = 'teardown_' + AJAX.hash(file);
96 $(document).on(eventName, func);
98 // eslint-disable-next-line no-console
100 // no need to translate
101 'Registered event ' + eventName + ' for file ' + file
107 * Called when a page has finished loading, once for every
108 * file that registered to the onload event of that file.
110 * @param {string} file The filename for which to fire the event
114 fireOnload: function (file) {
115 var eventName = 'onload_' + AJAX.hash(file);
116 $(document).trigger(eventName);
118 // eslint-disable-next-line no-console
120 // no need to translate
121 'Fired event ' + eventName + ' for file ' + file
126 * Called just before a page is torn down, once for every
127 * file that registered to the teardown event of that file.
129 * @param {string} file The filename for which to fire the event
133 fireTeardown: function (file) {
134 var eventName = 'teardown_' + AJAX.hash(file);
135 $(document).triggerHandler(eventName);
137 // eslint-disable-next-line no-console
139 // no need to translate
140 'Fired event ' + eventName + ' for file ' + file
145 * function to handle lock page mechanism
147 * @param event the event object
151 lockPageHandler: function (event) {
152 // don't consider checkbox event
153 if (typeof event.target !== 'undefined') {
154 if (event.target.type === 'checkbox') {
163 if (event.data.value === 3) {
164 newHash = event.data.content;
168 // Don't lock on enter.
169 if (0 === event.charCode) {
173 lockId = $(this).data('lock-id');
174 if (typeof lockId === 'undefined') {
178 * @todo Fix Code mirror does not give correct full value (query)
179 * in textarea, it returns only the change in content.
181 if (event.data.value === 1) {
182 newHash = AJAX.hash($(this).val());
184 newHash = AJAX.hash($(this).is(':checked'));
186 oldHash = $(this).data('val-hash');
188 // Set lock if old value !== new value
189 // otherwise release lock
190 if (oldHash !== newHash) {
191 AJAX.lockedTargets[lockId] = true;
193 delete AJAX.lockedTargets[lockId];
195 // Show lock icon if locked targets is not empty.
196 // otherwise remove lock icon
197 if (!jQuery.isEmptyObject(AJAX.lockedTargets)) {
198 $('#lock_page_icon').html(Functions.getImage('s_lock', Messages.strLockToolTip).toString());
200 $('#lock_page_icon').html('');
208 resetLock: function () {
209 AJAX.lockedTargets = {};
210 $('#lock_page_icon').html('');
213 replace: function (content) {
214 $('#floating_menubar').html(content)
215 // Remove duplicate wrapper
216 // TODO: don't send it in the response
217 .children().first().remove();
218 $('#topmenu').menuResizer(Functions.mainMenuResizerCallback);
222 * Event handler for clicks on links and form submissions
224 * @param {KeyboardEvent} event Event data
226 * @return {boolean | void}
228 requestHandler: function (event) {
229 // In some cases we don't want to handle the request here and either
230 // leave the browser deal with it natively (e.g: file download)
231 // or leave an existing ajax event handler present elsewhere deal with it
232 var href = $(this).attr('href');
233 if (typeof event !== 'undefined' && (event.shiftKey || event.ctrlKey || event.metaKey)) {
235 } else if ($(this).attr('target')) {
237 } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
238 // reset the lockedTargets object, as specified AJAX operation has finished
241 } else if (href && href.match(/^#/)) {
243 } else if (href && href.match(/^mailto/)) {
245 } else if ($(this).hasClass('ui-datepicker-next') ||
246 $(this).hasClass('ui-datepicker-prev')
251 if (typeof event !== 'undefined') {
252 event.preventDefault();
253 event.stopImmediatePropagation();
256 // triggers a confirm dialog if:
257 // the user has performed some operations on loaded page
258 // the user clicks on some link, (won't trigger for buttons)
259 // the click event is not triggered by script
260 if (typeof event !== 'undefined' && event.type === 'click' &&
261 event.isTrigger !== true &&
262 !jQuery.isEmptyObject(AJAX.lockedTargets) &&
263 confirm(Messages.strConfirmNavigation) === false
268 var isLink = !! href || false;
269 var previousLinkAborted = false;
271 if (AJAX.active === true) {
272 // Cancel the old request if abortable, when the user requests
273 // something else. Otherwise silently bail out, as there is already
274 // a request well in progress.
276 // In case of a link request, attempt aborting
278 if (AJAX.xhr.status === 0 && AJAX.xhr.statusText === 'abort') {
280 AJAX.$msgbox = Functions.ajaxShowMessage(Messages.strAbortedRequest);
283 previousLinkAborted = true;
289 // In case submitting a form, don't attempt aborting
294 AJAX.source = $(this);
296 $('html, body').animate({ scrollTop: 0 }, 'fast');
298 var url = isLink ? href : $(this).attr('action');
299 var argsep = CommonParams.get('arg_separator');
300 var params = 'ajax_request=true' + argsep + 'ajax_page_request=true';
301 var dataPost = AJAX.source.getPostData();
303 params += argsep + $(this).serialize();
304 } else if (dataPost) {
305 params += argsep + dataPost;
310 // eslint-disable-next-line no-console
311 console.log('Loading: ' + url); // no need to translate
316 AJAX.$msgbox = Functions.ajaxShowMessage();
317 // Save reference for the new link request
318 AJAX.xhr = $.get(url, params, AJAX.responseHandler);
322 if (previousLinkAborted) {
323 // hack: there is already an aborted entry on stack
324 // so just modify the aborted one
325 history.replaceState(state, null, href);
327 history.pushState(state, null, href);
331 * Manually fire the onsubmit event for the form, if any.
332 * The event was saved in the jQuery data object by an onload
333 * handler defined below. Workaround for bug #3583316
335 var onsubmit = $(this).data('onsubmit');
336 // Submit the request if there is no onsubmit handler
337 // or if it returns a value that evaluates to true
338 if (typeof onsubmit !== 'function' || onsubmit.apply(this, [event])) {
340 AJAX.$msgbox = Functions.ajaxShowMessage();
341 if ($(this).attr('id') === 'login_form') {
342 $.post(url, params, AJAX.loginResponseHandler);
344 $.post(url, params, AJAX.responseHandler);
350 * Response handler to handle login request from login modal after session expiration
352 * To refer to self use 'AJAX', instead of 'this' as this function
353 * is called in the jQuery context.
355 * @param {object} data Event data
359 loginResponseHandler: function (data) {
360 if (typeof data === 'undefined' || data === null) {
363 Functions.ajaxRemoveMessage(AJAX.$msgbox);
365 CommonParams.set('token', data.new_token);
367 AJAX.scriptHandler.load([]);
369 if (data.displayMessage) {
370 $('#page_content').prepend(data.displayMessage);
371 Functions.highlightSql($('#page_content'));
374 $('#pma_errors').remove();
377 if (data.errSubmitMsg) {
378 msg = data.errSubmitMsg;
381 $('<div></div>', { id : 'pma_errors', class : 'clearfloat d-print-none' })
382 .insertAfter('#selflink')
383 .append(data.errors);
384 // bind for php error reporting forms (bottom)
385 $('#pma_ignore_errors_bottom').on('click', function (e) {
387 Functions.ignorePhpErrors();
389 $('#pma_ignore_all_errors_bottom').on('click', function (e) {
391 Functions.ignorePhpErrors(false);
393 // In case of 'sendErrorReport'='always'
394 // submit the hidden error reporting form.
395 if (data.sendErrorAlways === '1' &&
396 data.stopErrorReportLoop !== '1'
398 $('#pma_report_errors_form').trigger('submit');
399 Functions.ajaxShowMessage(Messages.phpErrorsBeingSubmitted, false);
400 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
401 } else if (data.promptPhpErrors) {
402 // otherwise just prompt user if it is set so.
403 msg = msg + Messages.phpErrorsFound;
404 // scroll to bottom where all the errors are displayed.
405 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
409 Functions.ajaxShowMessage(msg, false);
410 // bind for php error reporting forms (popup)
411 $('#pma_ignore_errors_popup').on('click', function () {
412 Functions.ignorePhpErrors();
414 $('#pma_ignore_all_errors_popup').on('click', function () {
415 Functions.ignorePhpErrors(false);
418 if (typeof data.success !== 'undefined' && data.success) {
419 // reload page if user trying to login has changed
420 if (CommonParams.get('user') !== data.params.user) {
421 window.location = 'index.php';
422 Functions.ajaxShowMessage(Messages.strLoading, false);
427 // remove the login modal if the login is successful otherwise show error.
428 if (typeof data.logged_in !== 'undefined' && data.logged_in === 1) {
429 if ($('#modalOverlay').length) {
430 $('#modalOverlay').remove();
432 $('fieldset.disabled_for_expiration').removeAttr('disabled').removeClass('disabled_for_expiration');
433 AJAX.fireTeardown('functions.js');
434 AJAX.fireOnload('functions.js');
436 if (typeof data.new_token !== 'undefined') {
437 $('input[name=token]').val(data.new_token);
439 } else if (typeof data.logged_in !== 'undefined' && data.logged_in === 0) {
440 $('#modalOverlay').replaceWith(data.error);
442 Functions.ajaxShowMessage(data.error, false);
445 Functions.handleRedirectAndReload(data);
446 if (data.fieldWithError) {
447 $(':input.error').removeClass('error');
448 $('#' + data.fieldWithError).addClass('error');
453 * Called after the request that was initiated by this.requestHandler()
454 * has completed successfully or with a caught error. For completely
455 * failed requests or requests with uncaught errors, see the .ajaxError
456 * handler at the bottom of this file.
458 * To refer to self use 'AJAX', instead of 'this' as this function
459 * is called in the jQuery context.
461 * @param {object} data Event data
465 responseHandler: function (data) {
466 if (typeof data === 'undefined' || data === null) {
469 if (typeof data.success !== 'undefined' && data.success) {
470 $('html, body').animate({ scrollTop: 0 }, 'fast');
471 Functions.ajaxRemoveMessage(AJAX.$msgbox);
474 Functions.ajaxShowMessage(data.redirect, false);
480 AJAX.scriptHandler.reset(function () {
481 if (data.reloadNavigation) {
485 $('title').replaceWith(data.title);
492 history.replaceState(state, null);
493 AJAX.handleMenu.replace(data.menu);
495 if (data.disableNaviSettings) {
496 Navigation.disableSettings();
498 Navigation.ensureSettings(data.selflink);
501 // Remove all containers that may have
502 // been added outside of #page_content
504 .not('#pma_navigation')
505 .not('#floating_menubar')
506 .not('#page_nav_icons')
507 .not('#page_content')
512 .not('#pma_console_container')
513 .not('#prefs_autoload')
515 // Replace #page_content with new content
516 if (data.message && data.message.length > 0) {
517 $('#page_content').replaceWith(
518 '<div id=\'page_content\'>' + data.message + '</div>'
520 Functions.highlightSql($('#page_content'));
521 Functions.checkNumberOfFields();
525 var source = data.selflink.split('?')[0];
526 // Check for faulty links
527 var $selflinkReplace = {
528 'index.php?route=/import': 'index.php?route=/table/sql',
529 'index.php?route=/table/chart': 'index.php?route=/sql',
530 'index.php?route=/table/gis-visualization': 'index.php?route=/sql'
532 if ($selflinkReplace[source]) {
533 var replacement = $selflinkReplace[source];
534 data.selflink = data.selflink.replace(source, replacement);
536 $('#selflink').find('> a').attr('href', data.selflink);
539 CommonParams.setAll(data.params);
542 AJAX.scriptHandler.load(data.scripts);
544 if (data.displayMessage) {
545 $('#page_content').prepend(data.displayMessage);
546 Functions.highlightSql($('#page_content'));
549 $('#pma_errors').remove();
552 if (data.errSubmitMsg) {
553 msg = data.errSubmitMsg;
556 $('<div></div>', { id : 'pma_errors', class : 'clearfloat d-print-none' })
557 .insertAfter('#selflink')
558 .append(data.errors);
559 // bind for php error reporting forms (bottom)
560 $('#pma_ignore_errors_bottom').on('click', function (e) {
562 Functions.ignorePhpErrors();
564 $('#pma_ignore_all_errors_bottom').on('click', function (e) {
566 Functions.ignorePhpErrors(false);
568 // In case of 'sendErrorReport'='always'
569 // submit the hidden error reporting form.
570 if (data.sendErrorAlways === '1' &&
571 data.stopErrorReportLoop !== '1'
573 $('#pma_report_errors_form').trigger('submit');
574 Functions.ajaxShowMessage(Messages.phpErrorsBeingSubmitted, false);
575 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
576 } else if (data.promptPhpErrors) {
577 // otherwise just prompt user if it is set so.
578 msg = msg + Messages.phpErrorsFound;
579 // scroll to bottom where all the errors are displayed.
580 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
583 Functions.ajaxShowMessage(msg, false);
584 // bind for php error reporting forms (popup)
585 $('#pma_ignore_errors_popup').on('click', function () {
586 Functions.ignorePhpErrors();
588 $('#pma_ignore_all_errors_popup').on('click', function () {
589 Functions.ignorePhpErrors(false);
592 if (typeof AJAX.callback === 'function') {
593 AJAX.callback.call();
595 AJAX.callback = function () {};
598 Functions.ajaxShowMessage(data.error, false);
599 Functions.ajaxRemoveMessage(AJAX.$msgbox);
600 var $ajaxError = $('<div></div>');
601 $ajaxError.attr({ 'id': 'ajaxError' });
602 $('#page_content').append($ajaxError);
603 $ajaxError.html(data.error);
604 $('html, body').animate({ scrollTop: $(document).height() }, 200);
607 Functions.handleRedirectAndReload(data);
608 if (data.fieldWithError) {
609 $(':input.error').removeClass('error');
610 $('#' + data.fieldWithError).addClass('error');
615 * This object is in charge of downloading scripts,
616 * keeping track of what's downloaded and firing
617 * the onload event for them when the page is ready.
621 * @var {string[]} scripts The list of files already downloaded
625 * @var {string} scriptsVersion version of phpMyAdmin from which the
626 * scripts have been loaded
628 scriptsVersion: null,
630 * @var {string[]} scriptsToBeLoaded The list of files that
631 * need to be downloaded
633 scriptsToBeLoaded: [],
635 * @var {string[]} scriptsToBeFired The list of files for which
636 * to fire the onload and unload events
638 scriptsToBeFired: [],
639 scriptsCompleted: false,
641 * Records that a file has been downloaded
643 * @param {string} file The filename
644 * @param {string} fire Whether this file will be registering
645 * onload/teardown events
647 * @return {self} For chaining
649 add: function (file, fire) {
650 this.scripts.push(file);
652 // Record whether to fire any events for the file
653 // This is necessary to correctly tear down the initial page
654 this.scriptsToBeFired.push(file);
659 * Download a list of js files in one request
661 * @param {string[]} files An array of filenames and flags
662 * @param {Function} callback
666 load: function (files, callback) {
669 // Clear loaded scripts if they are from another version of phpMyAdmin.
670 // Depends on common params being set before loading scripts in responseHandler
671 if (self.scriptsVersion === null) {
672 self.scriptsVersion = CommonParams.get('version');
673 } else if (self.scriptsVersion !== CommonParams.get('version')) {
675 self.scriptsVersion = CommonParams.get('version');
677 self.scriptsCompleted = false;
678 self.scriptsToBeFired = [];
679 // We need to first complete list of files to load
680 // as next loop will directly fire requests to load them
681 // and that triggers removal of them from
682 // self.scriptsToBeLoaded
684 self.scriptsToBeLoaded.push(files[i].name);
686 self.scriptsToBeFired.push(files[i].name);
690 var script = files[i].name;
691 // Only for scripts that we don't already have
692 if ($.inArray(script, self.scripts) === -1) {
694 this.appendScript(script, callback);
696 self.done(script, callback);
699 // Trigger callback if there is nothing else to load
700 self.done(null, callback);
703 * Called whenever all files are loaded
705 * @param {string} script
706 * @param {Function?} callback
710 done: function (script, callback) {
711 if ($.inArray(script, this.scriptsToBeFired)) {
712 AJAX.fireOnload(script);
714 if ($.inArray(script, this.scriptsToBeLoaded)) {
715 this.scriptsToBeLoaded.splice($.inArray(script, this.scriptsToBeLoaded), 1);
717 if (script === null) {
718 this.scriptsCompleted = true;
720 /* We need to wait for last signal (with null) or last script load */
721 AJAX.active = (this.scriptsToBeLoaded.length > 0) || ! this.scriptsCompleted;
722 /* Run callback on last script */
723 if (! AJAX.active && typeof callback === 'function') {
728 * Appends a script element to the head to load the scripts
730 * @param {string} name
731 * @param {Function} callback
735 appendScript: function (name, callback) {
736 var head = document.head || document.getElementsByTagName('head')[0];
737 var script = document.createElement('script');
740 script.type = 'text/javascript';
741 var file = name.indexOf('vendor/') !== -1 ? name : 'dist/' + name;
742 script.src = 'js/' + file + '?' + 'v=' + encodeURIComponent(CommonParams.get('version'));
743 script.async = false;
744 script.onload = function () {
745 self.done(name, callback);
747 head.appendChild(script);
750 * Fires all the teardown event handlers for the current page
751 * and rebinds all forms and links to the request handler
753 * @param {Function} callback The callback to call after resetting
757 reset: function (callback) {
758 for (var i in this.scriptsToBeFired) {
759 AJAX.fireTeardown(this.scriptsToBeFired[i]);
761 this.scriptsToBeFired = [];
763 * Re-attach a generic event handler to clicks
764 * on pages and submissions of forms
766 $(document).off('click', 'a').on('click', 'a', AJAX.requestHandler);
767 $(document).off('submit', 'form').on('submit', 'form', AJAX.requestHandler);
774 * Here we register a function that will remove the onsubmit event from all
775 * forms that will be handled by the generic page loader. We then save this
776 * event handler in the "jQuery data", so that we can fire it up later in
777 * AJAX.requestHandler().
781 AJAX.registerOnload('functions.js', function () {
782 // Registering the onload event for functions.js
783 // ensures that it will be fired for all pages
784 $('form').not('.ajax').not('.disableAjax').each(function () {
785 if ($(this).attr('onsubmit')) {
786 $(this).data('onsubmit', this.onsubmit).attr('onsubmit', '');
790 var $pageContent = $('#page_content');
792 * Workaround for passing submit button name,value on ajax form submit
793 * by appending hidden element with submit button name and value.
795 $pageContent.on('click', 'form input[type=submit]', function () {
796 var buttonName = $(this).attr('name');
797 if (typeof buttonName === 'undefined') {
800 $(this).closest('form').append($('<input>', {
803 'value': $(this).val()
808 * Attach event listener to events when user modify visible
809 * Input,Textarea and select fields to make changes in forms
813 'form.lock-page textarea, ' +
814 'form.lock-page input[type="text"], ' +
815 'form.lock-page input[type="number"], ' +
816 'form.lock-page select',
822 'form.lock-page input[type="checkbox"], ' +
823 'form.lock-page input[type="radio"]',
828 * Reset lock when lock-page form reset event is fired
829 * Note: reset does not bubble in all browser so attach to
832 $('form.lock-page').on('reset', function () {
838 * Page load event handler
841 var menuContent = $('<div></div>')
842 .append($('#server-breadcrumb').clone())
843 .append($('#topmenucontainer').clone())
846 // set initial state reload
847 var initState = ('state' in window.history && window.history.state !== null);
848 var initURL = $('#selflink').find('> a').attr('href') || location.href;
853 history.replaceState(state, null);
855 $(window).on('popstate', function (event) {
856 var initPop = (! initState && location.href === initURL);
858 // check if popstate fired on first page itself
862 var state = event.originalEvent.state;
863 if (state && state.menu) {
864 AJAX.$msgbox = Functions.ajaxShowMessage();
865 var params = 'ajax_request=true' + CommonParams.get('arg_separator') + 'ajax_page_request=true';
866 var url = state.url || location.href;
867 $.get(url, params, AJAX.responseHandler);
868 // TODO: Check if sometimes menu is not retrieved from server,
869 // Not sure but it seems menu was missing only for printview which
870 // been removed lately, so if it's right some dead menu checks/fallbacks
871 // may need to be removed from this file and Header.php
872 // AJAX.handleMenu.replace(event.originalEvent.state.menu);
878 * Attach a generic event handler to clicks
879 * on pages and submissions of forms
881 $(document).on('click', 'a', AJAX.requestHandler);
882 $(document).on('submit', 'form', AJAX.requestHandler);
885 * Gracefully handle fatal server errors
886 * (e.g: 500 - Internal server error)
888 $(document).on('ajaxError', function (event, request) {
890 // eslint-disable-next-line no-console
891 console.log('AJAX error: status=' + request.status + ', text=' + request.statusText);
893 // Don't handle aborted requests
894 if (request.status !== 0 || request.statusText !== 'abort') {
896 var state = request.state();
898 if (request.status !== 0) {
899 details += '<div>' + Functions.escapeHtml(Functions.sprintf(Messages.strErrorCode, request.status)) + '</div>';
901 details += '<div>' + Functions.escapeHtml(Functions.sprintf(Messages.strErrorText, request.statusText + ' (' + state + ')')) + '</div>';
902 if (state === 'rejected' || state === 'timeout') {
903 details += '<div>' + Functions.escapeHtml(Messages.strErrorConnection) + '</div>';
905 Functions.ajaxShowMessage(
906 '<div class="alert alert-danger" role="alert">' +
907 Messages.strErrorProcessingRequest +