2 /* global isStorageSupported */ // js/config.js
3 /* global ErrorReport */ // js/error_report.js
4 /* global MicroHistory */ // js/microhistory.js
7 * This object handles ajax requests for pages. It also
8 * handles the reloading of the main menu and scripts.
12 * @var bool active Whether we are busy
16 * @var object source The object whose event initialized the request
20 * @var object xhr A reference to the ajax request that is currently running
24 * @var object lockedTargets, list of locked targets
28 * @var function Callback to execute after a successful request
29 * Used by PMA_commonFunctions from common.js
31 callback: function () {},
33 * @var bool debug Makes noise in your Firebug console
37 * @var object $msgbox A reference to a jQuery object that links to a message
38 * box that is generated by Functions.ajaxShowMessage()
42 * Given the filename of a script, returns a hash to be
43 * used to refer to all the events registered for the file
45 * @param key string key The filename for which to get the event name
49 hash: function (key) {
51 /* http://burtleburtle.net/bob/hash/doobs.html#one */
53 var len = newKey.length;
56 for (; i < len; ++i) {
57 hash += newKey.charCodeAt(i);
64 return Math.abs(hash);
67 * Registers an onload event for a file
69 * @param file string file The filename for which to register the event
70 * @param func function func The function to execute when the page is ready
72 * @return self For chaining
74 registerOnload: function (file, func) {
75 var eventName = 'onload_' + AJAX.hash(file);
76 $(document).on(eventName, func);
78 // eslint-disable-next-line no-console
80 // no need to translate
81 'Registered event ' + eventName + ' for file ' + file
87 * Registers a teardown event for a file. This is useful to execute functions
88 * that unbind events for page elements that are about to be removed.
90 * @param string file The filename for which to register the event
91 * @param function func The function to execute when
92 * the page is about to be torn down
94 * @return self For chaining
96 registerTeardown: function (file, func) {
97 var eventName = 'teardown_' + AJAX.hash(file);
98 $(document).on(eventName, func);
100 // eslint-disable-next-line no-console
102 // no need to translate
103 'Registered event ' + eventName + ' for file ' + file
109 * Called when a page has finished loading, once for every
110 * file that registered to the onload event of that file.
112 * @param string file The filename for which to fire the event
116 fireOnload: function (file) {
117 var eventName = 'onload_' + AJAX.hash(file);
118 $(document).trigger(eventName);
120 // eslint-disable-next-line no-console
122 // no need to translate
123 'Fired event ' + eventName + ' for file ' + file
128 * Called just before a page is torn down, once for every
129 * file that registered to the teardown event of that file.
131 * @param string file The filename for which to fire the event
135 fireTeardown: function (file) {
136 var eventName = 'teardown_' + AJAX.hash(file);
137 $(document).triggerHandler(eventName);
139 // eslint-disable-next-line no-console
141 // no need to translate
142 'Fired event ' + eventName + ' for file ' + file
147 * function to handle lock page mechanism
149 * @param event the event object
153 lockPageHandler: function (event) {
154 // don't consider checkbox event
155 if (typeof event.target !== 'undefined') {
156 if (event.target.type === 'checkbox') {
165 if (event.data.value === 3) {
166 newHash = event.data.content;
170 // Don't lock on enter.
171 if (0 === event.charCode) {
175 lockId = $(this).data('lock-id');
176 if (typeof lockId === 'undefined') {
180 * @todo Fix Code mirror does not give correct full value (query)
181 * in textarea, it returns only the change in content.
183 if (event.data.value === 1) {
184 newHash = AJAX.hash($(this).val());
186 newHash = AJAX.hash($(this).is(':checked'));
188 oldHash = $(this).data('val-hash');
190 // Set lock if old value !== new value
191 // otherwise release lock
192 if (oldHash !== newHash) {
193 AJAX.lockedTargets[lockId] = true;
195 delete AJAX.lockedTargets[lockId];
197 // Show lock icon if locked targets is not empty.
198 // otherwise remove lock icon
199 if (!jQuery.isEmptyObject(AJAX.lockedTargets)) {
200 $('#lock_page_icon').html(Functions.getImage('s_lock', Messages.strLockToolTip).toString());
202 $('#lock_page_icon').html('');
210 resetLock: function () {
211 AJAX.lockedTargets = {};
212 $('#lock_page_icon').html('');
215 replace: function (content) {
216 $('#floating_menubar').html(content)
217 // Remove duplicate wrapper
218 // TODO: don't send it in the response
219 .children().first().remove();
220 $('#topmenu').menuResizer(Functions.mainMenuResizerCallback);
224 * Event handler for clicks on links and form submissions
226 * @param object e Event data
230 requestHandler: function (event) {
231 // In some cases we don't want to handle the request here and either
232 // leave the browser deal with it natively (e.g: file download)
233 // or leave an existing ajax event handler present elsewhere deal with it
234 var href = $(this).attr('href');
235 if (typeof event !== 'undefined' && (event.shiftKey || event.ctrlKey)) {
237 } else if ($(this).attr('target')) {
239 } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
240 // reset the lockedTargets object, as specified AJAX operation has finished
243 } else if (href && href.match(/^#/)) {
245 } else if (href && href.match(/^mailto/)) {
247 } else if ($(this).hasClass('ui-datepicker-next') ||
248 $(this).hasClass('ui-datepicker-prev')
253 if (typeof event !== 'undefined') {
254 event.preventDefault();
255 event.stopImmediatePropagation();
258 // triggers a confirm dialog if:
259 // the user has performed some operations on loaded page
260 // the user clicks on some link, (won't trigger for buttons)
261 // the click event is not triggered by script
262 if (typeof event !== 'undefined' && event.type === 'click' &&
263 event.isTrigger !== true &&
264 !jQuery.isEmptyObject(AJAX.lockedTargets)
266 if (confirm(Messages.strConfirmNavigation) === false) {
269 if (isStorageSupported('localStorage')) {
270 window.localStorage.removeItem('autoSavedSql');
272 Cookies.set('autoSavedSql', '');
277 var isLink = !! href || false;
278 var previousLinkAborted = false;
280 if (AJAX.active === true) {
281 // Cancel the old request if abortable, when the user requests
282 // something else. Otherwise silently bail out, as there is already
283 // a request well in progress.
285 // In case of a link request, attempt aborting
287 if (AJAX.xhr.status === 0 && AJAX.xhr.statusText === 'abort') {
289 AJAX.$msgbox = Functions.ajaxShowMessage(Messages.strAbortedRequest);
292 previousLinkAborted = true;
298 // In case submitting a form, don't attempt aborting
303 AJAX.source = $(this);
305 $('html, body').animate({ scrollTop: 0 }, 'fast');
307 var url = isLink ? href : $(this).attr('action');
308 var argsep = CommonParams.get('arg_separator');
309 var params = 'ajax_request=true' + argsep + 'ajax_page_request=true';
310 var dataPost = AJAX.source.getPostData();
312 params += argsep + $(this).serialize();
313 } else if (dataPost) {
314 params += argsep + dataPost;
317 if (! (history && history.pushState)) {
318 // Add a list of menu hashes that we have in the cache to the request
319 params += MicroHistory.menus.getRequestParam();
323 // eslint-disable-next-line no-console
324 console.log('Loading: ' + url); // no need to translate
329 AJAX.$msgbox = Functions.ajaxShowMessage();
330 // Save reference for the new link request
331 AJAX.xhr = $.get(url, params, AJAX.responseHandler);
332 if (history && history.pushState) {
336 if (previousLinkAborted) {
337 // hack: there is already an aborted entry on stack
338 // so just modify the aborted one
339 history.replaceState(state, null, href);
341 history.pushState(state, null, href);
346 * Manually fire the onsubmit event for the form, if any.
347 * The event was saved in the jQuery data object by an onload
348 * handler defined below. Workaround for bug #3583316
350 var onsubmit = $(this).data('onsubmit');
351 // Submit the request if there is no onsubmit handler
352 // or if it returns a value that evaluates to true
353 if (typeof onsubmit !== 'function' || onsubmit.apply(this, [event])) {
355 AJAX.$msgbox = Functions.ajaxShowMessage();
356 if ($(this).attr('id') === 'login_form') {
357 $.post(url, params, AJAX.loginResponseHandler);
359 $.post(url, params, AJAX.responseHandler);
365 * Response handler to handle login request from login modal after session expiration
367 * To refer to self use 'AJAX', instead of 'this' as this function
368 * is called in the jQuery context.
370 * @param object data Event data
374 loginResponseHandler: function (data) {
375 if (typeof data === 'undefined' || data === null) {
378 Functions.ajaxRemoveMessage(AJAX.$msgbox);
380 CommonParams.set('token', data.new_token);
382 AJAX.scriptHandler.load([]);
384 if (data.displayMessage) {
385 $('#page_content').prepend(data.displayMessage);
386 Functions.highlightSql($('#page_content'));
389 $('#pma_errors').remove();
392 if (data.errSubmitMsg) {
393 msg = data.errSubmitMsg;
396 $('<div></div>', { id : 'pma_errors', class : 'clearfloat' })
397 .insertAfter('#selflink')
398 .append(data.errors);
399 // bind for php error reporting forms (bottom)
400 $('#pma_ignore_errors_bottom').on('click', function (e) {
402 Functions.ignorePhpErrors();
404 $('#pma_ignore_all_errors_bottom').on('click', function (e) {
406 Functions.ignorePhpErrors(false);
408 // In case of 'sendErrorReport'='always'
409 // submit the hidden error reporting form.
410 if (data.sendErrorAlways === '1' &&
411 data.stopErrorReportLoop !== '1'
413 $('#pma_report_errors_form').trigger('submit');
414 Functions.ajaxShowMessage(Messages.phpErrorsBeingSubmitted, false);
415 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
416 } else if (data.promptPhpErrors) {
417 // otherwise just prompt user if it is set so.
418 msg = msg + Messages.phpErrorsFound;
419 // scroll to bottom where all the errors are displayed.
420 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
424 Functions.ajaxShowMessage(msg, false);
425 // bind for php error reporting forms (popup)
426 $('#pma_ignore_errors_popup').on('click', function () {
427 Functions.ignorePhpErrors();
429 $('#pma_ignore_all_errors_popup').on('click', function () {
430 Functions.ignorePhpErrors(false);
433 if (typeof data.success !== 'undefined' && data.success) {
434 // reload page if user trying to login has changed
435 if (CommonParams.get('user') !== data.params.user) {
436 window.location = 'index.php';
437 Functions.ajaxShowMessage(Messages.strLoading, false);
442 // remove the login modal if the login is successful otherwise show error.
443 if (typeof data.logged_in !== 'undefined' && data.logged_in === 1) {
444 if ($('#modalOverlay').length) {
445 $('#modalOverlay').remove();
447 $('fieldset.disabled_for_expiration').removeAttr('disabled').removeClass('disabled_for_expiration');
448 AJAX.fireTeardown('functions.js');
449 AJAX.fireOnload('functions.js');
451 if (typeof data.new_token !== 'undefined') {
452 $('input[name=token]').val(data.new_token);
454 } else if (typeof data.logged_in !== 'undefined' && data.logged_in === 0) {
455 $('#modalOverlay').replaceWith(data.error);
457 Functions.ajaxShowMessage(data.error, false);
460 Functions.handleRedirectAndReload(data);
461 if (data.fieldWithError) {
462 $(':input.error').removeClass('error');
463 $('#' + data.fieldWithError).addClass('error');
468 * Called after the request that was initiated by this.requestHandler()
469 * has completed successfully or with a caught error. For completely
470 * failed requests or requests with uncaught errors, see the .ajaxError
471 * handler at the bottom of this file.
473 * To refer to self use 'AJAX', instead of 'this' as this function
474 * is called in the jQuery context.
476 * @param object e Event data
480 responseHandler: function (data) {
481 if (typeof data === 'undefined' || data === null) {
484 if (typeof data.success !== 'undefined' && data.success) {
485 $('html, body').animate({ scrollTop: 0 }, 'fast');
486 Functions.ajaxRemoveMessage(AJAX.$msgbox);
489 Functions.ajaxShowMessage(data.redirect, false);
495 AJAX.scriptHandler.reset(function () {
496 if (data.reloadNavigation) {
500 $('title').replaceWith(data.title);
503 if (history && history.pushState) {
508 history.replaceState(state, null);
509 AJAX.handleMenu.replace(data.menu);
511 MicroHistory.menus.replace(data.menu);
512 MicroHistory.menus.add(data.menuHash, data.menu);
514 } else if (data.menuHash) {
515 if (! (history && history.pushState)) {
516 MicroHistory.menus.replace(MicroHistory.menus.get(data.menuHash));
519 if (data.disableNaviSettings) {
520 Navigation.disableSettings();
522 Navigation.ensureSettings(data.selflink);
525 // Remove all containers that may have
526 // been added outside of #page_content
528 .not('#pma_navigation')
529 .not('#floating_menubar')
530 .not('#page_nav_icons')
531 .not('#page_content')
536 .not('#pma_console_container')
537 .not('#prefs_autoload')
539 // Replace #page_content with new content
540 if (data.message && data.message.length > 0) {
541 $('#page_content').replaceWith(
542 '<div id=\'page_content\'>' + data.message + '</div>'
544 Functions.highlightSql($('#page_content'));
545 Functions.checkNumberOfFields();
549 var source = data.selflink.split('?')[0];
550 // Check for faulty links
551 var $selflinkReplace = {
552 'index.php?route=/import': 'index.php?route=/table/sql',
553 'index.php?route=/table/chart': 'index.php?route=/sql',
554 'index.php?route=/table/gis-visualization': 'index.php?route=/sql'
556 if ($selflinkReplace[source]) {
557 var replacement = $selflinkReplace[source];
558 data.selflink = data.selflink.replace(source, replacement);
560 $('#selflink').find('> a').attr('href', data.selflink);
563 CommonParams.setAll(data.params);
566 AJAX.scriptHandler.load(data.scripts);
568 if (data.selflink && data.scripts && data.menuHash && data.params) {
569 if (! (history && history.pushState)) {
575 AJAX.source.attr('rel')
579 if (data.displayMessage) {
580 $('#page_content').prepend(data.displayMessage);
581 Functions.highlightSql($('#page_content'));
584 $('#pma_errors').remove();
587 if (data.errSubmitMsg) {
588 msg = data.errSubmitMsg;
591 $('<div></div>', { id : 'pma_errors', class : 'clearfloat' })
592 .insertAfter('#selflink')
593 .append(data.errors);
594 // bind for php error reporting forms (bottom)
595 $('#pma_ignore_errors_bottom').on('click', function (e) {
597 Functions.ignorePhpErrors();
599 $('#pma_ignore_all_errors_bottom').on('click', function (e) {
601 Functions.ignorePhpErrors(false);
603 // In case of 'sendErrorReport'='always'
604 // submit the hidden error reporting form.
605 if (data.sendErrorAlways === '1' &&
606 data.stopErrorReportLoop !== '1'
608 $('#pma_report_errors_form').trigger('submit');
609 Functions.ajaxShowMessage(Messages.phpErrorsBeingSubmitted, false);
610 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
611 } else if (data.promptPhpErrors) {
612 // otherwise just prompt user if it is set so.
613 msg = msg + Messages.phpErrorsFound;
614 // scroll to bottom where all the errors are displayed.
615 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
618 Functions.ajaxShowMessage(msg, false);
619 // bind for php error reporting forms (popup)
620 $('#pma_ignore_errors_popup').on('click', function () {
621 Functions.ignorePhpErrors();
623 $('#pma_ignore_all_errors_popup').on('click', function () {
624 Functions.ignorePhpErrors(false);
627 if (typeof AJAX.callback === 'function') {
628 AJAX.callback.call();
630 AJAX.callback = function () {};
633 Functions.ajaxShowMessage(data.error, false);
634 Functions.ajaxRemoveMessage(AJAX.$msgbox);
635 var $ajaxError = $('<div></div>');
636 $ajaxError.attr({ 'id': 'ajaxError' });
637 $('#page_content').append($ajaxError);
638 $ajaxError.html(data.error);
639 $('html, body').animate({ scrollTop: $(document).height() }, 200);
642 Functions.handleRedirectAndReload(data);
643 if (data.fieldWithError) {
644 $(':input.error').removeClass('error');
645 $('#' + data.fieldWithError).addClass('error');
650 * This object is in charge of downloading scripts,
651 * keeping track of what's downloaded and firing
652 * the onload event for them when the page is ready.
656 * @var array scripts The list of files already downloaded
660 * @var string scriptsVersion version of phpMyAdmin from which the
661 * scripts have been loaded
663 scriptsVersion: null,
665 * @var array scriptsToBeLoaded The list of files that
666 * need to be downloaded
668 scriptsToBeLoaded: [],
670 * @var array scriptsToBeFired The list of files for which
671 * to fire the onload and unload events
673 scriptsToBeFired: [],
674 scriptsCompleted: false,
676 * Records that a file has been downloaded
678 * @param string file The filename
679 * @param string fire Whether this file will be registering
680 * onload/teardown events
682 * @return self For chaining
684 add: function (file, fire) {
685 this.scripts.push(file);
687 // Record whether to fire any events for the file
688 // This is necessary to correctly tear down the initial page
689 this.scriptsToBeFired.push(file);
694 * Download a list of js files in one request
696 * @param array files An array of filenames and flags
700 load: function (files, callback) {
703 // Clear loaded scripts if they are from another version of phpMyAdmin.
704 // Depends on common params being set before loading scripts in responseHandler
705 if (self.scriptsVersion === null) {
706 self.scriptsVersion = CommonParams.get('PMA_VERSION');
707 } else if (self.scriptsVersion !== CommonParams.get('PMA_VERSION')) {
709 self.scriptsVersion = CommonParams.get('PMA_VERSION');
711 self.scriptsCompleted = false;
712 self.scriptsToBeFired = [];
713 // We need to first complete list of files to load
714 // as next loop will directly fire requests to load them
715 // and that triggers removal of them from
716 // self.scriptsToBeLoaded
718 self.scriptsToBeLoaded.push(files[i].name);
720 self.scriptsToBeFired.push(files[i].name);
724 var script = files[i].name;
725 // Only for scripts that we don't already have
726 if ($.inArray(script, self.scripts) === -1) {
728 this.appendScript(script, callback);
730 self.done(script, callback);
733 // Trigger callback if there is nothing else to load
734 self.done(null, callback);
737 * Called whenever all files are loaded
741 done: function (script, callback) {
742 if (typeof ErrorReport !== 'undefined') {
743 ErrorReport.wrapGlobalFunctions();
745 if ($.inArray(script, this.scriptsToBeFired)) {
746 AJAX.fireOnload(script);
748 if ($.inArray(script, this.scriptsToBeLoaded)) {
749 this.scriptsToBeLoaded.splice($.inArray(script, this.scriptsToBeLoaded), 1);
751 if (script === null) {
752 this.scriptsCompleted = true;
754 /* We need to wait for last signal (with null) or last script load */
755 AJAX.active = (this.scriptsToBeLoaded.length > 0) || ! this.scriptsCompleted;
756 /* Run callback on last script */
757 if (! AJAX.active && typeof callback === 'function') {
762 * Appends a script element to the head to load the scripts
766 appendScript: function (name, callback) {
767 var head = document.head || document.getElementsByTagName('head')[0];
768 var script = document.createElement('script');
771 script.type = 'text/javascript';
772 script.src = 'js/' + name + '?' + 'v=' + encodeURIComponent(CommonParams.get('PMA_VERSION'));
773 script.async = false;
774 script.onload = function () {
775 self.done(name, callback);
777 head.appendChild(script);
780 * Fires all the teardown event handlers for the current page
781 * and rebinds all forms and links to the request handler
783 * @param function callback The callback to call after resetting
787 reset: function (callback) {
788 for (var i in this.scriptsToBeFired) {
789 AJAX.fireTeardown(this.scriptsToBeFired[i]);
791 this.scriptsToBeFired = [];
793 * Re-attach a generic event handler to clicks
794 * on pages and submissions of forms
796 $(document).off('click', 'a').on('click', 'a', AJAX.requestHandler);
797 $(document).off('submit', 'form').on('submit', 'form', AJAX.requestHandler);
798 if (! (history && history.pushState)) {
799 MicroHistory.update();
807 * Here we register a function that will remove the onsubmit event from all
808 * forms that will be handled by the generic page loader. We then save this
809 * event handler in the "jQuery data", so that we can fire it up later in
810 * AJAX.requestHandler().
814 AJAX.registerOnload('functions.js', function () {
815 // Registering the onload event for functions.js
816 // ensures that it will be fired for all pages
817 $('form').not('.ajax').not('.disableAjax').each(function () {
818 if ($(this).attr('onsubmit')) {
819 $(this).data('onsubmit', this.onsubmit).attr('onsubmit', '');
823 var $pageContent = $('#page_content');
825 * Workaround for passing submit button name,value on ajax form submit
826 * by appending hidden element with submit button name and value.
828 $pageContent.on('click', 'form input[type=submit]', function () {
829 var buttonName = $(this).attr('name');
830 if (typeof buttonName === 'undefined') {
833 $(this).closest('form').append($('<input>', {
836 'value': $(this).val()
841 * Attach event listener to events when user modify visible
842 * Input,Textarea and select fields to make changes in forms
846 'form.lock-page textarea, ' +
847 'form.lock-page input[type="text"], ' +
848 'form.lock-page input[type="number"], ' +
849 'form.lock-page select',
855 'form.lock-page input[type="checkbox"], ' +
856 'form.lock-page input[type="radio"]',
861 * Reset lock when lock-page form reset event is fired
862 * Note: reset does not bubble in all browser so attach to
865 $('form.lock-page').on('reset', function () {
871 * Page load event handler
874 var menuContent = $('<div></div>')
875 .append($('#server-breadcrumb').clone())
876 .append($('#topmenucontainer').clone())
878 if (history && history.pushState) {
879 // set initial state reload
880 var initState = ('state' in window.history && window.history.state !== null);
881 var initURL = $('#selflink').find('> a').attr('href') || location.href;
886 history.replaceState(state, null);
888 $(window).on('popstate', function (event) {
889 var initPop = (! initState && location.href === initURL);
891 // check if popstate fired on first page itself
895 var state = event.originalEvent.state;
896 if (state && state.menu) {
897 AJAX.$msgbox = Functions.ajaxShowMessage();
898 var params = 'ajax_request=true' + CommonParams.get('arg_separator') + 'ajax_page_request=true';
899 var url = state.url || location.href;
900 $.get(url, params, AJAX.responseHandler);
901 // TODO: Check if sometimes menu is not retrieved from server,
902 // Not sure but it seems menu was missing only for printview which
903 // been removed lately, so if it's right some dead menu checks/fallbacks
904 // may need to be removed from this file and Header.php
905 // AJAX.handleMenu.replace(event.originalEvent.state.menu);
909 // Fallback to microhistory mechanism
911 .load([{ 'name' : 'microhistory.js', 'fire' : 1 }], function () {
912 // The cache primer is set by the footer class
913 if (MicroHistory.primer.url) {
914 MicroHistory.menus.add(
915 MicroHistory.primer.menuHash,
920 // Queue up this event twice to make sure that we get a copy
921 // of the page after all other onload events have been fired
922 if (MicroHistory.primer.url) {
924 MicroHistory.primer.url,
925 MicroHistory.primer.scripts,
926 MicroHistory.primer.menuHash
935 * Attach a generic event handler to clicks
936 * on pages and submissions of forms
938 $(document).on('click', 'a', AJAX.requestHandler);
939 $(document).on('submit', 'form', AJAX.requestHandler);
942 * Gracefully handle fatal server errors
943 * (e.g: 500 - Internal server error)
945 $(document).on('ajaxError', function (event, request) {
947 // eslint-disable-next-line no-console
948 console.log('AJAX error: status=' + request.status + ', text=' + request.statusText);
950 // Don't handle aborted requests
951 if (request.status !== 0 || request.statusText !== 'abort') {
953 var state = request.state();
955 if (request.status !== 0) {
956 details += '<div>' + Functions.escapeHtml(Functions.sprintf(Messages.strErrorCode, request.status)) + '</div>';
958 details += '<div>' + Functions.escapeHtml(Functions.sprintf(Messages.strErrorText, request.statusText + ' (' + state + ')')) + '</div>';
959 if (state === 'rejected' || state === 'timeout') {
960 details += '<div>' + Functions.escapeHtml(Messages.strErrorConnection) + '</div>';
962 Functions.ajaxShowMessage(
963 '<div class="alert alert-danger" role="alert">' +
964 Messages.strErrorProcessingRequest +