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 */
48 var len = key.length, hash = 0, i = 0;
49 for (; i < len; ++i) {
50 hash += key.charCodeAt(i);
57 return Math.abs(hash);
60 * Registers an onload event for a file
62 * @param file string file The filename for which to register the event
63 * @param func function func The function to execute when the page is ready
65 * @return self For chaining
67 registerOnload: function (file, func) {
68 var eventName = 'onload_' + AJAX.hash(file);
69 $(document).bind(eventName, func);
72 // no need to translate
73 "Registered event " + eventName + " for file " + file
79 * Registers a teardown event for a file. This is useful to execute functions
80 * that unbind events for page elements that are about to be removed.
82 * @param string file The filename for which to register the event
83 * @param function func The function to execute when
84 * the page is about to be torn down
86 * @return self For chaining
88 registerTeardown: function (file, func) {
89 var eventName = 'teardown_' + AJAX.hash(file);
90 $(document).bind(eventName, func);
93 // no need to translate
94 "Registered event " + eventName + " for file " + file
100 * Called when a page has finished loading, once for every
101 * file that registered to the onload event of that file.
103 * @param string file The filename for which to fire the event
107 fireOnload: function (file) {
108 var eventName = 'onload_' + AJAX.hash(file);
109 $(document).trigger(eventName);
112 // no need to translate
113 "Fired event " + eventName + " for file " + file
118 * Called just before a page is torn down, once for every
119 * file that registered to the teardown event of that file.
121 * @param string file The filename for which to fire the event
125 fireTeardown: function (file) {
126 var eventName = 'teardown_' + AJAX.hash(file);
127 $(document).triggerHandler(eventName);
130 // no need to translate
131 "Fired event " + eventName + " for file " + file
136 * function to handle lock page mechanism
138 * @param event the event object
142 lockPageHandler: function(event) {
143 //Don't lock on enter.
144 if (0 == event.charCode) {
148 var lockId = $(this).data('lock-id');
149 if (typeof lockId === 'undefined') {
153 * @todo Fix Code mirror does not give correct full value (query)
154 * in textarea, it returns only the change in content.
157 if (event.data.value == 1) {
158 newHash = AJAX.hash($(this).val());
160 newHash = AJAX.hash($(this).is(":checked"));
162 var oldHash = $(this).data('val-hash');
163 // Set lock if old value != new value
164 // otherwise release lock
165 if (oldHash !== newHash) {
166 AJAX.lockedTargets[lockId] = true;
168 delete AJAX.lockedTargets[lockId];
170 // Show lock icon if locked targets is not empty.
171 // otherwise remove lock icon
172 if (!jQuery.isEmptyObject(AJAX.lockedTargets)) {
173 $('#lock_page_icon').html(PMA_getImage('s_lock.png',PMA_messages.strLockToolTip).toString());
175 $('#lock_page_icon').html('');
183 resetLock: function() {
184 AJAX.lockedTargets = {};
185 $('#lock_page_icon').html('');
188 replace: function (content) {
189 $('#floating_menubar').html(content)
190 // Remove duplicate wrapper
191 // TODO: don't send it in the response
192 .children().first().remove();
193 $('#topmenu').menuResizer(PMA_mainMenuResizerCallback);
197 * Event handler for clicks on links and form submissions
199 * @param object e Event data
203 requestHandler: function (event) {
204 // In some cases we don't want to handle the request here and either
205 // leave the browser deal with it natively (e.g: file download)
206 // or leave an existing ajax event handler present elsewhere deal with it
207 var href = $(this).attr('href');
208 if (typeof event != 'undefined' && (event.shiftKey || event.ctrlKey)) {
210 } else if ($(this).attr('target')) {
212 } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
213 //reset the lockedTargets object, as specified AJAX operation has finished
216 } else if (href && href.match(/^#/)) {
218 } else if (href && href.match(/^mailto/)) {
220 } else if ($(this).hasClass('ui-datepicker-next') ||
221 $(this).hasClass('ui-datepicker-prev')
226 if (typeof event != 'undefined') {
227 event.preventDefault();
228 event.stopImmediatePropagation();
231 //triggers a confirm dialog if:
232 //the user has performed some operations on loaded page
233 //the user clicks on some link, (won't trigger for buttons)
234 //the click event is not triggered by script
235 if (typeof event !== 'undefined' && event.type === 'click' &&
236 event.isTrigger !== true &&
237 !jQuery.isEmptyObject(AJAX.lockedTargets) &&
238 confirm(PMA_messages.strConfirmNavigation) === false
243 var isLink = !! href || false;
244 var previousLinkAborted = false;
246 if (AJAX.active === true) {
247 // Cancel the old request if abortable, when the user requests
248 // something else. Otherwise silently bail out, as there is already
249 // a request well in progress.
251 //In case of a link request, attempt aborting
253 if(AJAX.xhr.status === 0 && AJAX.xhr.statusText === 'abort') {
255 AJAX.$msgbox = PMA_ajaxShowMessage(PMA_messages.strAbortedRequest);
258 previousLinkAborted = true;
264 //In case submitting a form, don't attempt aborting
269 AJAX.source = $(this);
271 $('html, body').animate({scrollTop: 0}, 'fast');
273 var url = isLink ? href : $(this).attr('action');
274 var params = 'ajax_request=true&ajax_page_request=true';
276 params += '&' + $(this).serialize();
278 if (! (history && history.pushState)) {
279 // Add a list of menu hashes that we have in the cache to the request
280 params += PMA_MicroHistory.menus.getRequestParam();
284 console.log("Loading: " + url); // no need to translate
289 AJAX.$msgbox = PMA_ajaxShowMessage();
290 //Save reference for the new link request
291 AJAX.xhr = $.get(url, params, AJAX.responseHandler);
292 if (history && history.pushState) {
296 if (previousLinkAborted) {
297 //hack: there is already an aborted entry on stack
298 //so just modify the aborted one
299 history.replaceState(state, null, href);
301 history.pushState(state, null, href);
306 * Manually fire the onsubmit event for the form, if any.
307 * The event was saved in the jQuery data object by an onload
308 * handler defined below. Workaround for bug #3583316
310 var onsubmit = $(this).data('onsubmit');
311 // Submit the request if there is no onsubmit handler
312 // or if it returns a value that evaluates to true
313 if (typeof onsubmit !== 'function' || onsubmit.apply(this, [event])) {
315 AJAX.$msgbox = PMA_ajaxShowMessage();
316 $.post(url, params, AJAX.responseHandler);
321 * Called after the request that was initiated by this.requestHandler()
322 * has completed successfully or with a caught error. For completely
323 * failed requests or requests with uncaught errors, see the .ajaxError
324 * handler at the bottom of this file.
326 * To refer to self use 'AJAX', instead of 'this' as this function
327 * is called in the jQuery context.
329 * @param object e Event data
333 responseHandler: function (data) {
334 if (typeof data === 'undefined' || data === null) {
337 if (typeof data.success != 'undefined' && data.success) {
338 $('html, body').animate({scrollTop: 0}, 'fast');
339 PMA_ajaxRemoveMessage(AJAX.$msgbox);
341 if (data._redirect) {
342 PMA_ajaxShowMessage(data._redirect, false);
348 AJAX.scriptHandler.reset(function () {
349 if (data._reloadNavigation) {
350 PMA_reloadNavigation();
353 $('title').replaceWith(data._title);
356 if (history && history.pushState) {
358 url : data._selflink,
361 history.replaceState(state, null);
362 AJAX.handleMenu.replace(data._menu);
364 PMA_MicroHistory.menus.replace(data._menu);
365 PMA_MicroHistory.menus.add(data._menuHash, data._menu);
367 } else if (data._menuHash) {
368 if (! (history && history.pushState)) {
369 PMA_MicroHistory.menus.replace(PMA_MicroHistory.menus.get(data._menuHash));
372 if (data._disableNaviSettings) {
373 PMA_disableNaviSettings();
376 PMA_ensureNaviSettings(data._selflink);
379 // Remove all containers that may have
380 // been added outside of #page_content
382 .not('#pma_navigation')
383 .not('#floating_menubar')
384 .not('#page_nav_icons')
385 .not('#page_content')
390 .not('#pma_console_container')
391 .not('#prefs_autoload')
393 // Replace #page_content with new content
394 if (data.message && data.message.length > 0) {
395 $('#page_content').replaceWith(
396 "<div id='page_content'>" + data.message + "</div>"
398 PMA_highlightSQL($('#page_content'));
399 checkNumberOfFields();
402 if (data._selflink) {
403 var source = data._selflink.split('?')[0];
404 //Check for faulty links
405 $selflink_replace = {
406 "import.php": "tbl_sql.php",
407 "tbl_chart.php": "sql.php",
408 "tbl_gis_visualization.php": "sql.php"
410 if ($selflink_replace[source]) {
411 var replacement = $selflink_replace[source];
412 data._selflink = data._selflink.replace(source, replacement);
414 $('#selflink').find('> a').attr('href', data._selflink);
417 PMA_commonParams.setAll(data._params);
420 AJAX.scriptHandler.load(data._scripts);
422 if (data._selflink && data._scripts && data._menuHash && data._params) {
423 if (! (history && history.pushState)) {
424 PMA_MicroHistory.add(
429 AJAX.source.attr('rel')
433 if (data._displayMessage) {
434 $('#page_content').prepend(data._displayMessage);
435 PMA_highlightSQL($('#page_content'));
438 $('#pma_errors').remove();
441 if(data._errSubmitMsg){
442 msg = data._errSubmitMsg;
445 $('<div/>', {id : 'pma_errors'})
446 .insertAfter('#selflink')
447 .append(data._errors);
448 // bind for php error reporting forms (bottom)
449 $("#pma_ignore_errors_bottom").bind("click", function(e) {
451 PMA_ignorePhpErrors();
453 $("#pma_ignore_all_errors_bottom").bind("click", function(e) {
455 PMA_ignorePhpErrors(false);
457 // In case of 'sendErrorReport'='always'
458 // submit the hidden error reporting form.
459 if (data._sendErrorAlways == '1' &&
460 data._stopErrorReportLoop != '1'
462 $("#pma_report_errors_form").submit();
463 PMA_ajaxShowMessage(PMA_messages.phpErrorsBeingSubmitted, false);
464 $('html, body').animate({scrollTop:$(document).height()}, 'slow');
465 } else if (data._promptPhpErrors) {
466 // otherwise just prompt user if it is set so.
467 msg = msg + PMA_messages.phpErrorsFound;
468 // scroll to bottom where all the errors are displayed.
469 $('html, body').animate({scrollTop:$(document).height()}, 'slow');
472 PMA_ajaxShowMessage(msg, false);
473 // bind for php error reporting forms (popup)
474 $("#pma_ignore_errors_popup").bind("click", function() {
475 PMA_ignorePhpErrors();
477 $("#pma_ignore_all_errors_popup").bind("click", function() {
478 PMA_ignorePhpErrors(false);
481 if (typeof AJAX._callback === 'function') {
482 AJAX._callback.call();
484 AJAX._callback = function () {};
488 PMA_ajaxShowMessage(data.error, false);
491 if (parseInt(data.redirect_flag) == 1) {
492 // add one more GET param to display session expiry msg
493 window.location.href += '&session_expired=1';
494 window.location.reload();
495 } else if (parseInt(data.reload_flag) == 1) {
496 // remove the token param and reload
497 window.location.href = window.location.href.replace(/&?token=[^&#]*/g, "");
498 window.location.reload();
500 if (data.fieldWithError) {
501 $(':input.error').removeClass("error");
502 $('#'+data.fieldWithError).addClass("error");
507 * This object is in charge of downloading scripts,
508 * keeping track of what's downloaded and firing
509 * the onload event for them when the page is ready.
513 * @var array _scripts The list of files already downloaded
517 * @var string _scriptsVersion version of phpMyAdmin from which the
518 * scripts have been loaded
520 _scriptsVersion: null,
522 * @var array _scriptsToBeLoaded The list of files that
523 * need to be downloaded
525 _scriptsToBeLoaded: [],
527 * @var array _scriptsToBeFired The list of files for which
528 * to fire the onload event
530 _scriptsToBeFired: [],
532 * Records that a file has been downloaded
534 * @param string file The filename
535 * @param string fire Whether this file will be registering
536 * onload/teardown events
538 * @return self For chaining
540 add: function (file, fire) {
541 this._scripts.push(file);
543 // Record whether to fire any events for the file
544 // This is necessary to correctly tear down the initial page
545 this._scriptsToBeFired.push(file);
550 * Download a list of js files in one request
552 * @param array files An array of filenames and flags
556 load: function (files, callback) {
558 // Clear loaded scripts if they are from another version of phpMyAdmin.
559 // Depends on common params being set before loading scripts in responseHandler
560 if (self._scriptsVersion == null) {
561 self._scriptsVersion = PMA_commonParams.get('PMA_VERSION');
562 } else if (self._scriptsVersion != PMA_commonParams.get('PMA_VERSION')) {
564 self._scriptsVersion = PMA_commonParams.get('PMA_VERSION');
566 self._scriptsToBeLoaded = [];
567 self._scriptsToBeFired = [];
568 for (var i in files) {
569 self._scriptsToBeLoaded.push(files[i].name);
571 self._scriptsToBeFired.push(files[i].name);
574 // Generate a request string
576 var needRequest = false;
577 for (var index in self._scriptsToBeLoaded) {
578 var script = self._scriptsToBeLoaded[index];
579 // Only for scripts that we don't already have
580 if ($.inArray(script, self._scripts) == -1) {
583 request.push("scripts%5B%5D=" + script);
586 request.push("call_done=1");
587 request.push("v=" + encodeURIComponent(PMA_commonParams.get('PMA_VERSION')));
588 // Download the composite js file, if necessary
590 this.appendScript("js/get_scripts.js.php?" + request.join("&"));
596 * Called whenever all files are loaded
600 done: function (callback) {
601 if($.isFunction(callback)) {
604 if (typeof ErrorReport !== 'undefined') {
605 ErrorReport.wrap_global_functions();
607 for (var i in this._scriptsToBeFired) {
608 AJAX.fireOnload(this._scriptsToBeFired[i]);
613 * Appends a script element to the head to load the scripts
617 appendScript: function (url) {
618 var head = document.head || document.getElementsByTagName('head')[0];
619 var script = document.createElement('script');
620 script.type = 'text/javascript';
622 script.async = false;
623 head.appendChild(script);
626 * Fires all the teardown event handlers for the current page
627 * and rebinds all forms and links to the request handler
629 * @param function callback The callback to call after resetting
633 reset: function (callback) {
634 for (var i in this._scriptsToBeFired) {
635 AJAX.fireTeardown(this._scriptsToBeFired[i]);
637 this._scriptsToBeFired = [];
639 * Re-attach a generic event handler to clicks
640 * on pages and submissions of forms
642 $(document).off('click', 'a').on('click', 'a', AJAX.requestHandler);
643 $(document).off('submit', 'form').on('submit', 'form', AJAX.requestHandler);
644 if (! (history && history.pushState)) {
645 PMA_MicroHistory.update();
653 * Here we register a function that will remove the onsubmit event from all
654 * forms that will be handled by the generic page loader. We then save this
655 * event handler in the "jQuery data", so that we can fire it up later in
656 * AJAX.requestHandler().
660 AJAX.registerOnload('functions.js', function () {
661 // Registering the onload event for functions.js
662 // ensures that it will be fired for all pages
663 $('form').not('.ajax').not('.disableAjax').each(function () {
664 if ($(this).attr('onsubmit')) {
665 $(this).data('onsubmit', this.onsubmit).attr('onsubmit', '');
669 var $page_content = $('#page_content');
671 * Workaround for passing submit button name,value on ajax form submit
672 * by appending hidden element with submit button name and value.
674 $page_content.on('click', 'form input[type=submit]', function() {
675 var buttonName = $(this).attr('name');
676 if (typeof buttonName === 'undefined') {
679 $(this).closest('form').append($('<input/>', {
682 'value': $(this).val()
687 * Attach event listener to events when user modify visible
688 * Input,Textarea and select fields to make changes in forms
692 'form.lock-page textarea, ' +
693 'form.lock-page input[type="text"], ' +
694 'form.lock-page input[type="number"], ' +
695 'form.lock-page select',
701 'form.lock-page input[type="checkbox"], ' +
702 'form.lock-page input[type="radio"]',
707 * Reset lock when lock-page form reset event is fired
708 * Note: reset does not bubble in all browser so attach to
711 $('form.lock-page').on('reset', function(event){
717 * Page load event handler
720 var menuContent = $('<div></div>')
721 .append($('#serverinfo').clone())
722 .append($('#topmenucontainer').clone())
724 if (history && history.pushState) {
725 //set initial state reload
726 var initState = ('state' in window.history && window.history.state !== null);
727 var initURL = $('#selflink').find('> a').attr('href') || location.href;
732 history.replaceState(state, null);
734 $(window).on('popstate', function(event) {
735 var initPop = (! initState && location.href == initURL);
737 //check if popstate fired on first page itself
741 var state = event.originalEvent.state;
742 if (state && state.menu) {
743 AJAX.$msgbox = PMA_ajaxShowMessage();
744 var params = 'ajax_request=true&ajax_page_request=true';
745 var url = state.url || location.href;
746 $.get(url, params, AJAX.responseHandler);
747 //TODO: Check if sometimes menu is not retrieved from server,
748 // Not sure but it seems menu was missing only for printview which
749 // been removed lately, so if it's right some dead menu checks/fallbacks
750 // may need to be removed from this file and Header.class.php
751 //AJAX.handleMenu.replace(event.originalEvent.state.menu);
755 // Fallback to microhistory mechanism
757 .load([{'name' : 'microhistory.js', 'fire' : 1}], function () {
758 // The cache primer is set by the footer class
759 if (PMA_MicroHistory.primer.url) {
760 PMA_MicroHistory.menus.add(
761 PMA_MicroHistory.primer.menuHash,
766 // Queue up this event twice to make sure that we get a copy
767 // of the page after all other onload events have been fired
768 if (PMA_MicroHistory.primer.url) {
769 PMA_MicroHistory.add(
770 PMA_MicroHistory.primer.url,
771 PMA_MicroHistory.primer.scripts,
772 PMA_MicroHistory.primer.menuHash
781 * Attach a generic event handler to clicks
782 * on pages and submissions of forms
784 $(document).on('click', 'a', AJAX.requestHandler);
785 $(document).on('submit', 'form', AJAX.requestHandler);
788 * Gracefully handle fatal server errors
789 * (e.g: 500 - Internal server error)
791 $(document).ajaxError(function (event, request, settings) {
792 if (request.status !== 0) { // Don't handle aborted requests
793 var errorCode = PMA_sprintf(PMA_messages.strErrorCode, request.status);
794 var errorText = PMA_sprintf(PMA_messages.strErrorText, request.statusText);
796 '<div class="error">' +
797 PMA_messages.strErrorProcessingRequest +
798 '<div>' + errorCode + '</div>' +
799 '<div>' + errorText + '</div>' +