1 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 * This object handles ajax requests for pages. It also
4 * handles the reloading of the main menu and scripts.
8 * @var bool active Whether we are busy
12 * @var object source The object whose event initialized the request
16 * @var object xhr A reference to the ajax request that is currently running
20 * @var object lockedTargets, list of locked targets
24 * @var function Callback to execute after a successful request
25 * Used by PMA_commonFunctions from common.js
27 _callback: function () {},
29 * @var bool _debug Makes noise in your Firebug console
33 * @var object $msgbox A reference to a jQuery object that links to a message
34 * box that is generated by PMA_ajaxShowMessage()
38 * Given the filename of a script, returns a hash to be
39 * used to refer to all the events registered for the file
41 * @param key string key The filename for which to get the event name
45 hash: function (key) {
46 /* http://burtleburtle.net/bob/hash/doobs.html#one */
51 for (; i < len; ++i) {
52 hash += key.charCodeAt(i);
59 return Math.abs(hash);
62 * Registers an onload event for a file
64 * @param file string file The filename for which to register the event
65 * @param func function func The function to execute when the page is ready
67 * @return self For chaining
69 registerOnload: function (file, func) {
70 var eventName = 'onload_' + AJAX.hash(file);
71 $(document).on(eventName, func);
74 // no need to translate
75 'Registered event ' + eventName + ' for file ' + file
81 * Registers a teardown event for a file. This is useful to execute functions
82 * that unbind events for page elements that are about to be removed.
84 * @param string file The filename for which to register the event
85 * @param function func The function to execute when
86 * the page is about to be torn down
88 * @return self For chaining
90 registerTeardown: function (file, func) {
91 var eventName = 'teardown_' + AJAX.hash(file);
92 $(document).on(eventName, func);
95 // no need to translate
96 'Registered event ' + eventName + ' for file ' + file
102 * Called when a page has finished loading, once for every
103 * file that registered to the onload event of that file.
105 * @param string file The filename for which to fire the event
109 fireOnload: function (file) {
110 var eventName = 'onload_' + AJAX.hash(file);
111 $(document).trigger(eventName);
114 // no need to translate
115 'Fired event ' + eventName + ' for file ' + file
120 * Called just before a page is torn down, once for every
121 * file that registered to the teardown event of that file.
123 * @param string file The filename for which to fire the event
127 fireTeardown: function (file) {
128 var eventName = 'teardown_' + AJAX.hash(file);
129 $(document).triggerHandler(eventName);
132 // no need to translate
133 'Fired event ' + eventName + ' for file ' + file
138 * function to handle lock page mechanism
140 * @param event the event object
144 lockPageHandler: function (event) {
149 if (event.data.value === 3) {
150 newHash = event.data.content;
154 // Don't lock on enter.
155 if (0 === event.charCode) {
159 lockId = $(this).data('lock-id');
160 if (typeof lockId === 'undefined') {
164 * @todo Fix Code mirror does not give correct full value (query)
165 * in textarea, it returns only the change in content.
167 if (event.data.value === 1) {
168 newHash = AJAX.hash($(this).val());
170 newHash = AJAX.hash($(this).is(':checked'));
172 oldHash = $(this).data('val-hash');
174 // Set lock if old value !== new value
175 // otherwise release lock
176 if (oldHash !== newHash) {
177 AJAX.lockedTargets[lockId] = true;
179 delete AJAX.lockedTargets[lockId];
181 // Show lock icon if locked targets is not empty.
182 // otherwise remove lock icon
183 if (!jQuery.isEmptyObject(AJAX.lockedTargets)) {
184 $('#lock_page_icon').html(PMA_getImage('s_lock', PMA_messages.strLockToolTip).toString());
186 $('#lock_page_icon').html('');
194 resetLock: function () {
195 AJAX.lockedTargets = {};
196 $('#lock_page_icon').html('');
199 replace: function (content) {
200 $('#floating_menubar').html(content)
201 // Remove duplicate wrapper
202 // TODO: don't send it in the response
203 .children().first().remove();
204 $('#topmenu').menuResizer(PMA_mainMenuResizerCallback);
208 * Event handler for clicks on links and form submissions
210 * @param object e Event data
214 requestHandler: function (event) {
215 // In some cases we don't want to handle the request here and either
216 // leave the browser deal with it natively (e.g: file download)
217 // or leave an existing ajax event handler present elsewhere deal with it
218 var href = $(this).attr('href');
219 if (typeof event !== 'undefined' && (event.shiftKey || event.ctrlKey)) {
221 } else if ($(this).attr('target')) {
223 } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
224 // reset the lockedTargets object, as specified AJAX operation has finished
227 } else if (href && href.match(/^#/)) {
229 } else if (href && href.match(/^mailto/)) {
231 } else if ($(this).hasClass('ui-datepicker-next') ||
232 $(this).hasClass('ui-datepicker-prev')
237 if (typeof event !== 'undefined') {
238 event.preventDefault();
239 event.stopImmediatePropagation();
242 // triggers a confirm dialog if:
243 // the user has performed some operations on loaded page
244 // the user clicks on some link, (won't trigger for buttons)
245 // the click event is not triggered by script
246 if (typeof event !== 'undefined' && event.type === 'click' &&
247 event.isTrigger !== true &&
248 !jQuery.isEmptyObject(AJAX.lockedTargets)
250 if (confirm(PMA_messages.strConfirmNavigation) === false) {
253 if (isStorageSupported('localStorage')) {
254 window.localStorage.removeItem('auto_saved_sql');
256 Cookies.set('auto_saved_sql', '');
261 var isLink = !! href || false;
262 var previousLinkAborted = false;
264 if (AJAX.active === true) {
265 // Cancel the old request if abortable, when the user requests
266 // something else. Otherwise silently bail out, as there is already
267 // a request well in progress.
269 // In case of a link request, attempt aborting
271 if (AJAX.xhr.status === 0 && AJAX.xhr.statusText === 'abort') {
273 AJAX.$msgbox = PMA_ajaxShowMessage(PMA_messages.strAbortedRequest);
276 previousLinkAborted = true;
282 // In case submitting a form, don't attempt aborting
287 AJAX.source = $(this);
289 $('html, body').animate({ scrollTop: 0 }, 'fast');
291 var url = isLink ? href : $(this).attr('action');
292 var argsep = PMA_commonParams.get('arg_separator');
293 var params = 'ajax_request=true' + argsep + 'ajax_page_request=true';
294 var dataPost = AJAX.source.getPostData();
296 params += argsep + $(this).serialize();
297 } else if (dataPost) {
298 params += argsep + dataPost;
301 if (! (history && history.pushState)) {
302 // Add a list of menu hashes that we have in the cache to the request
303 params += PMA_MicroHistory.menus.getRequestParam();
307 console.log('Loading: ' + url); // no need to translate
312 AJAX.$msgbox = PMA_ajaxShowMessage();
313 // Save reference for the new link request
314 AJAX.xhr = $.get(url, params, AJAX.responseHandler);
315 if (history && history.pushState) {
319 if (previousLinkAborted) {
320 // hack: there is already an aborted entry on stack
321 // so just modify the aborted one
322 history.replaceState(state, null, href);
324 history.pushState(state, null, href);
329 * Manually fire the onsubmit event for the form, if any.
330 * The event was saved in the jQuery data object by an onload
331 * handler defined below. Workaround for bug #3583316
333 var onsubmit = $(this).data('onsubmit');
334 // Submit the request if there is no onsubmit handler
335 // or if it returns a value that evaluates to true
336 if (typeof onsubmit !== 'function' || onsubmit.apply(this, [event])) {
338 AJAX.$msgbox = PMA_ajaxShowMessage();
339 if ($(this).attr('id') === 'login_form') {
340 $.post(url, params, AJAX.loginResponseHandler);
342 $.post(url, params, AJAX.responseHandler);
348 * Response handler to handle login request from login modal after session expiration
350 * To refer to self use 'AJAX', instead of 'this' as this function
351 * is called in the jQuery context.
353 * @param object data Event data
357 loginResponseHandler: function (data) {
358 if (typeof data === 'undefined' || data === null) {
361 PMA_ajaxRemoveMessage(AJAX.$msgbox);
363 PMA_commonParams.set('token', data.new_token);
365 AJAX.scriptHandler.load([]);
367 if (data._displayMessage) {
368 $('#page_content').prepend(data._displayMessage);
369 PMA_highlightSQL($('#page_content'));
372 $('#pma_errors').remove();
375 if (data._errSubmitMsg) {
376 msg = data._errSubmitMsg;
379 $('<div></div>', { id : 'pma_errors', class : 'clearfloat' })
380 .insertAfter('#selflink')
381 .append(data._errors);
382 // bind for php error reporting forms (bottom)
383 $('#pma_ignore_errors_bottom').on('click', function (e) {
385 PMA_ignorePhpErrors();
387 $('#pma_ignore_all_errors_bottom').on('click', function (e) {
389 PMA_ignorePhpErrors(false);
391 // In case of 'sendErrorReport'='always'
392 // submit the hidden error reporting form.
393 if (data._sendErrorAlways === '1' &&
394 data._stopErrorReportLoop !== '1'
396 $('#pma_report_errors_form').submit();
397 PMA_ajaxShowMessage(PMA_messages.phpErrorsBeingSubmitted, false);
398 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
399 } else if (data._promptPhpErrors) {
400 // otherwise just prompt user if it is set so.
401 msg = msg + PMA_messages.phpErrorsFound;
402 // scroll to bottom where all the errors are displayed.
403 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
407 PMA_ajaxShowMessage(msg, false);
408 // bind for php error reporting forms (popup)
409 $('#pma_ignore_errors_popup').on('click', function () {
410 PMA_ignorePhpErrors();
412 $('#pma_ignore_all_errors_popup').on('click', function () {
413 PMA_ignorePhpErrors(false);
416 if (typeof data.success !== 'undefined' && data.success) {
417 // reload page if user trying to login has changed
418 if (PMA_commonParams.get('user') !== data._params.user) {
419 window.location = 'index.php';
420 PMA_ajaxShowMessage(PMA_messages.strLoading, false);
425 // remove the login modal if the login is successful otherwise show error.
426 if (typeof data.logged_in !== 'undefined' && data.logged_in === 1) {
427 if ($('#modalOverlay').length) {
428 $('#modalOverlay').remove();
430 $('fieldset.disabled_for_expiration').removeAttr('disabled').removeClass('disabled_for_expiration');
431 AJAX.fireTeardown('functions.js');
432 AJAX.fireOnload('functions.js');
434 if (typeof data.new_token !== 'undefined') {
435 $('input[name=token]').val(data.new_token);
437 } else if (typeof data.logged_in !== 'undefined' && data.logged_in === 0) {
438 $('#modalOverlay').replaceWith(data.error);
440 PMA_ajaxShowMessage(data.error, false);
443 PMA_handleRedirectAndReload(data);
444 if (data.fieldWithError) {
445 $(':input.error').removeClass('error');
446 $('#' + data.fieldWithError).addClass('error');
451 * Called after the request that was initiated by this.requestHandler()
452 * has completed successfully or with a caught error. For completely
453 * failed requests or requests with uncaught errors, see the .ajaxError
454 * handler at the bottom of this file.
456 * To refer to self use 'AJAX', instead of 'this' as this function
457 * is called in the jQuery context.
459 * @param object e Event data
463 responseHandler: function (data) {
464 if (typeof data === 'undefined' || data === null) {
467 if (typeof data.success !== 'undefined' && data.success) {
468 $('html, body').animate({ scrollTop: 0 }, 'fast');
469 PMA_ajaxRemoveMessage(AJAX.$msgbox);
471 if (data._redirect) {
472 PMA_ajaxShowMessage(data._redirect, false);
478 AJAX.scriptHandler.reset(function () {
479 if (data._reloadNavigation) {
480 PMA_reloadNavigation();
483 $('title').replaceWith(data._title);
486 if (history && history.pushState) {
488 url : data._selflink,
491 history.replaceState(state, null);
492 AJAX.handleMenu.replace(data._menu);
494 PMA_MicroHistory.menus.replace(data._menu);
495 PMA_MicroHistory.menus.add(data._menuHash, data._menu);
497 } else if (data._menuHash) {
498 if (! (history && history.pushState)) {
499 PMA_MicroHistory.menus.replace(PMA_MicroHistory.menus.get(data._menuHash));
502 if (data._disableNaviSettings) {
503 PMA_disableNaviSettings();
505 PMA_ensureNaviSettings(data._selflink);
508 // Remove all containers that may have
509 // been added outside of #page_content
511 .not('#pma_navigation')
512 .not('#floating_menubar')
513 .not('#page_nav_icons')
514 .not('#page_content')
519 .not('#pma_console_container')
520 .not('#prefs_autoload')
522 // Replace #page_content with new content
523 if (data.message && data.message.length > 0) {
524 $('#page_content').replaceWith(
525 '<div id=\'page_content\'>' + data.message + '</div>'
527 PMA_highlightSQL($('#page_content'));
528 checkNumberOfFields();
531 if (data._selflink) {
532 var source = data._selflink.split('?')[0];
533 // Check for faulty links
534 $selflink_replace = {
535 'import.php': 'tbl_sql.php',
536 'tbl_chart.php': 'sql.php',
537 'tbl_gis_visualization.php': 'sql.php'
539 if ($selflink_replace[source]) {
540 var replacement = $selflink_replace[source];
541 data._selflink = data._selflink.replace(source, replacement);
543 $('#selflink').find('> a').attr('href', data._selflink);
546 PMA_commonParams.setAll(data._params);
549 AJAX.scriptHandler.load(data._scripts);
551 if (data._selflink && data._scripts && data._menuHash && data._params) {
552 if (! (history && history.pushState)) {
553 PMA_MicroHistory.add(
558 AJAX.source.attr('rel')
562 if (data._displayMessage) {
563 $('#page_content').prepend(data._displayMessage);
564 PMA_highlightSQL($('#page_content'));
567 $('#pma_errors').remove();
570 if (data._errSubmitMsg) {
571 msg = data._errSubmitMsg;
574 $('<div></div>', { id : 'pma_errors', class : 'clearfloat' })
575 .insertAfter('#selflink')
576 .append(data._errors);
577 // bind for php error reporting forms (bottom)
578 $('#pma_ignore_errors_bottom').on('click', function (e) {
580 PMA_ignorePhpErrors();
582 $('#pma_ignore_all_errors_bottom').on('click', function (e) {
584 PMA_ignorePhpErrors(false);
586 // In case of 'sendErrorReport'='always'
587 // submit the hidden error reporting form.
588 if (data._sendErrorAlways === '1' &&
589 data._stopErrorReportLoop !== '1'
591 $('#pma_report_errors_form').submit();
592 PMA_ajaxShowMessage(PMA_messages.phpErrorsBeingSubmitted, false);
593 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
594 } else if (data._promptPhpErrors) {
595 // otherwise just prompt user if it is set so.
596 msg = msg + PMA_messages.phpErrorsFound;
597 // scroll to bottom where all the errors are displayed.
598 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
601 PMA_ajaxShowMessage(msg, false);
602 // bind for php error reporting forms (popup)
603 $('#pma_ignore_errors_popup').on('click', function () {
604 PMA_ignorePhpErrors();
606 $('#pma_ignore_all_errors_popup').on('click', function () {
607 PMA_ignorePhpErrors(false);
610 if (typeof AJAX._callback === 'function') {
611 AJAX._callback.call();
613 AJAX._callback = function () {};
616 PMA_ajaxShowMessage(data.error, false);
617 PMA_ajaxRemoveMessage(AJAX.$msgbox);
618 $ajaxError = $('<div></div>');
619 $ajaxError.attr({ 'id': 'ajaxError' });
620 $('#page_content').append($ajaxError);
621 $ajaxError.html(data.error);
622 $('html, body').animate({ scrollTop: $(document).height() }, 200);
625 PMA_handleRedirectAndReload(data);
626 if (data.fieldWithError) {
627 $(':input.error').removeClass('error');
628 $('#' + data.fieldWithError).addClass('error');
633 * This object is in charge of downloading scripts,
634 * keeping track of what's downloaded and firing
635 * the onload event for them when the page is ready.
639 * @var array _scripts The list of files already downloaded
643 * @var string _scriptsVersion version of phpMyAdmin from which the
644 * scripts have been loaded
646 _scriptsVersion: null,
648 * @var array _scriptsToBeLoaded The list of files that
649 * need to be downloaded
651 _scriptsToBeLoaded: [],
653 * @var array _scriptsToBeFired The list of files for which
654 * to fire the onload and unload events
656 _scriptsToBeFired: [],
657 _scriptsCompleted: false,
659 * Records that a file has been downloaded
661 * @param string file The filename
662 * @param string fire Whether this file will be registering
663 * onload/teardown events
665 * @return self For chaining
667 add: function (file, fire) {
668 this._scripts.push(file);
670 // Record whether to fire any events for the file
671 // This is necessary to correctly tear down the initial page
672 this._scriptsToBeFired.push(file);
677 * Download a list of js files in one request
679 * @param array files An array of filenames and flags
683 load: function (files, callback) {
686 // Clear loaded scripts if they are from another version of phpMyAdmin.
687 // Depends on common params being set before loading scripts in responseHandler
688 if (self._scriptsVersion === null) {
689 self._scriptsVersion = PMA_commonParams.get('PMA_VERSION');
690 } else if (self._scriptsVersion !== PMA_commonParams.get('PMA_VERSION')) {
692 self._scriptsVersion = PMA_commonParams.get('PMA_VERSION');
694 self._scriptsCompleted = false;
695 self._scriptsToBeFired = [];
696 // We need to first complete list of files to load
697 // as next loop will directly fire requests to load them
698 // and that triggers removal of them from
699 // self._scriptsToBeLoaded
701 self._scriptsToBeLoaded.push(files[i].name);
703 self._scriptsToBeFired.push(files[i].name);
707 var script = files[i].name;
708 // Only for scripts that we don't already have
709 if ($.inArray(script, self._scripts) === -1) {
711 this.appendScript(script, callback);
713 self.done(script, callback);
716 // Trigger callback if there is nothing else to load
717 self.done(null, callback);
720 * Called whenever all files are loaded
724 done: function (script, callback) {
725 if (typeof ErrorReport !== 'undefined') {
726 ErrorReport.wrap_global_functions();
728 if ($.inArray(script, this._scriptsToBeFired)) {
729 AJAX.fireOnload(script);
731 if ($.inArray(script, this._scriptsToBeLoaded)) {
732 this._scriptsToBeLoaded.splice($.inArray(script, this._scriptsToBeLoaded), 1);
734 if (script === null) {
735 this._scriptsCompleted = true;
737 /* We need to wait for last signal (with null) or last script load */
738 AJAX.active = (this._scriptsToBeLoaded.length > 0) || ! this._scriptsCompleted;
739 /* Run callback on last script */
740 if (! AJAX.active && $.isFunction(callback)) {
745 * Appends a script element to the head to load the scripts
749 appendScript: function (name, callback) {
750 var head = document.head || document.getElementsByTagName('head')[0];
751 var script = document.createElement('script');
754 script.type = 'text/javascript';
755 script.src = 'js/' + name + '?' + 'v=' + encodeURIComponent(PMA_commonParams.get('PMA_VERSION'));
756 script.async = false;
757 script.onload = function () {
758 self.done(name, callback);
760 head.appendChild(script);
763 * Fires all the teardown event handlers for the current page
764 * and rebinds all forms and links to the request handler
766 * @param function callback The callback to call after resetting
770 reset: function (callback) {
771 for (var i in this._scriptsToBeFired) {
772 AJAX.fireTeardown(this._scriptsToBeFired[i]);
774 this._scriptsToBeFired = [];
776 * Re-attach a generic event handler to clicks
777 * on pages and submissions of forms
779 $(document).off('click', 'a').on('click', 'a', AJAX.requestHandler);
780 $(document).off('submit', 'form').on('submit', 'form', AJAX.requestHandler);
781 if (! (history && history.pushState)) {
782 PMA_MicroHistory.update();
790 * Here we register a function that will remove the onsubmit event from all
791 * forms that will be handled by the generic page loader. We then save this
792 * event handler in the "jQuery data", so that we can fire it up later in
793 * AJAX.requestHandler().
797 AJAX.registerOnload('functions.js', function () {
798 // Registering the onload event for functions.js
799 // ensures that it will be fired for all pages
800 $('form').not('.ajax').not('.disableAjax').each(function () {
801 if ($(this).attr('onsubmit')) {
802 $(this).data('onsubmit', this.onsubmit).attr('onsubmit', '');
806 var $page_content = $('#page_content');
808 * Workaround for passing submit button name,value on ajax form submit
809 * by appending hidden element with submit button name and value.
811 $page_content.on('click', 'form input[type=submit]', function () {
812 var buttonName = $(this).attr('name');
813 if (typeof buttonName === 'undefined') {
816 $(this).closest('form').append($('<input>', {
819 'value': $(this).val()
824 * Attach event listener to events when user modify visible
825 * Input,Textarea and select fields to make changes in forms
829 'form.lock-page textarea, ' +
830 'form.lock-page input[type="text"], ' +
831 'form.lock-page input[type="number"], ' +
832 'form.lock-page select',
838 'form.lock-page input[type="checkbox"], ' +
839 'form.lock-page input[type="radio"]',
844 * Reset lock when lock-page form reset event is fired
845 * Note: reset does not bubble in all browser so attach to
848 $('form.lock-page').on('reset', function (event) {
854 * Page load event handler
857 var menuContent = $('<div></div>')
858 .append($('#serverinfo').clone())
859 .append($('#topmenucontainer').clone())
861 if (history && history.pushState) {
862 // set initial state reload
863 var initState = ('state' in window.history && window.history.state !== null);
864 var initURL = $('#selflink').find('> a').attr('href') || location.href;
869 history.replaceState(state, null);
871 $(window).on('popstate', function (event) {
872 var initPop = (! initState && location.href === initURL);
874 // check if popstate fired on first page itself
878 var state = event.originalEvent.state;
879 if (state && state.menu) {
880 AJAX.$msgbox = PMA_ajaxShowMessage();
881 var params = 'ajax_request=true' + PMA_commonParams.get('arg_separator') + 'ajax_page_request=true';
882 var url = state.url || location.href;
883 $.get(url, params, AJAX.responseHandler);
884 // TODO: Check if sometimes menu is not retrieved from server,
885 // Not sure but it seems menu was missing only for printview which
886 // been removed lately, so if it's right some dead menu checks/fallbacks
887 // may need to be removed from this file and Header.php
888 // AJAX.handleMenu.replace(event.originalEvent.state.menu);
892 // Fallback to microhistory mechanism
894 .load([{ 'name' : 'microhistory.js', 'fire' : 1 }], function () {
895 // The cache primer is set by the footer class
896 if (PMA_MicroHistory.primer.url) {
897 PMA_MicroHistory.menus.add(
898 PMA_MicroHistory.primer.menuHash,
903 // Queue up this event twice to make sure that we get a copy
904 // of the page after all other onload events have been fired
905 if (PMA_MicroHistory.primer.url) {
906 PMA_MicroHistory.add(
907 PMA_MicroHistory.primer.url,
908 PMA_MicroHistory.primer.scripts,
909 PMA_MicroHistory.primer.menuHash
918 * Attach a generic event handler to clicks
919 * on pages and submissions of forms
921 $(document).on('click', 'a', AJAX.requestHandler);
922 $(document).on('submit', 'form', AJAX.requestHandler);
925 * Gracefully handle fatal server errors
926 * (e.g: 500 - Internal server error)
928 $(document).ajaxError(function (event, request, settings) {
930 console.log('AJAX error: status=' + request.status + ', text=' + request.statusText);
932 // Don't handle aborted requests
933 if (request.status !== 0 || request.statusText !== 'abort') {
935 var state = request.state();
937 if (request.status !== 0) {
938 details += '<div>' + escapeHtml(PMA_sprintf(PMA_messages.strErrorCode, request.status)) + '</div>';
940 details += '<div>' + escapeHtml(PMA_sprintf(PMA_messages.strErrorText, request.statusText + ' (' + state + ')')) + '</div>';
941 if (state === 'rejected' || state === 'timeout') {
942 details += '<div>' + escapeHtml(PMA_messages.strErrorConnection) + '</div>';
945 '<div class="error">' +
946 PMA_messages.strErrorProcessingRequest +