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 * Event handler for clicks on links and form submissions
190 * @param object e Event data
194 requestHandler: function (event) {
195 // In some cases we don't want to handle the request here and either
196 // leave the browser deal with it natively (e.g: file download)
197 // or leave an existing ajax event handler present elsewhere deal with it
198 var href = $(this).attr('href');
199 if (typeof event != 'undefined' && (event.shiftKey || event.ctrlKey)) {
201 } else if ($(this).attr('target')) {
203 } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
204 //reset the lockedTargets object, as specified AJAX operation has finished
207 } else if (href && href.match(/^#/)) {
209 } else if (href && href.match(/^mailto/)) {
211 } else if ($(this).hasClass('ui-datepicker-next') ||
212 $(this).hasClass('ui-datepicker-prev')
217 if (typeof event != 'undefined') {
218 event.preventDefault();
219 event.stopImmediatePropagation();
222 //triggers a confirm dialog if:
223 //the user has performed some operations on loaded page
224 //the user clicks on some link, (won't trigger for buttons)
225 //the click event is not triggered by script
226 if (typeof event !== 'undefined' && event.type === 'click' &&
227 event.isTrigger !== true &&
228 !jQuery.isEmptyObject(AJAX.lockedTargets) &&
229 confirm(PMA_messages.strConfirmNavigation) === false
235 if (AJAX.active === true) {
236 // Cancel the old request if abortable, when the user requests
237 // something else. Otherwise silently bail out, as there is already
238 // a request well in progress.
240 //In case of a link request, attempt aborting
242 if(AJAX.xhr.status === 0 && AJAX.xhr.statusText === 'abort') {
244 AJAX.$msgbox = PMA_ajaxShowMessage(PMA_messages.strAbortedRequest);
252 //In case submitting a form, don't attempt aborting
257 AJAX.source = $(this);
259 $('html, body').animate({scrollTop: 0}, 'fast');
261 var isLink = !! href || false;
262 var url = isLink ? href : $(this).attr('action');
263 var params = 'ajax_request=true&ajax_page_request=true';
265 params += '&' + $(this).serialize();
267 // Add a list of menu hashes that we have in the cache to the request
268 params += AJAX.cache.menus.getRequestParam();
271 console.log("Loading: " + url); // no need to translate
276 AJAX.$msgbox = PMA_ajaxShowMessage();
277 //Save reference for the new link request
278 AJAX.xhr = $.get(url, params, AJAX.responseHandler);
281 * Manually fire the onsubmit event for the form, if any.
282 * The event was saved in the jQuery data object by an onload
283 * handler defined below. Workaround for bug #3583316
285 var onsubmit = $(this).data('onsubmit');
286 // Submit the request if there is no onsubmit handler
287 // or if it returns a value that evaluates to true
288 if (typeof onsubmit !== 'function' || onsubmit.apply(this, [event])) {
290 AJAX.$msgbox = PMA_ajaxShowMessage();
291 $.post(url, params, AJAX.responseHandler);
296 * Called after the request that was initiated by this.requestHandler()
297 * has completed successfully or with a caught error. For completely
298 * failed requests or requests with uncaught errors, see the .ajaxError
299 * handler at the bottom of this file.
301 * To refer to self use 'AJAX', instead of 'this' as this function
302 * is called in the jQuery context.
304 * @param object e Event data
308 responseHandler: function (data) {
309 if (typeof data === 'undefined' || data === null) {
312 if (typeof data.success != 'undefined' && data.success) {
313 $('html, body').animate({scrollTop: 0}, 'fast');
314 PMA_ajaxRemoveMessage(AJAX.$msgbox);
316 if (data._redirect) {
317 PMA_ajaxShowMessage(data._redirect, false);
323 AJAX.scriptHandler.reset(function () {
324 if (data._reloadNavigation) {
325 PMA_reloadNavigation();
328 $('title').replaceWith(data._title);
331 AJAX.cache.menus.replace(data._menu);
332 AJAX.cache.menus.add(data._menuHash, data._menu);
333 } else if (data._menuHash) {
334 AJAX.cache.menus.replace(AJAX.cache.menus.get(data._menuHash));
336 if (data._disableNaviSettings) {
337 PMA_disableNaviSettings();
340 PMA_ensureNaviSettings(data._selflink);
343 // Remove all containers that may have
344 // been added outside of #page_content
346 .not('#pma_navigation')
347 .not('#floating_menubar')
348 .not('#page_nav_icons')
349 .not('#page_content')
354 .not('#pma_console_container')
355 .not('#prefs_autoload')
357 // Replace #page_content with new content
358 if (data.message && data.message.length > 0) {
359 $('#page_content').replaceWith(
360 "<div id='page_content'>" + data.message + "</div>"
362 PMA_highlightSQL($('#page_content'));
363 checkNumberOfFields();
366 if (data._selflink) {
368 var source = data._selflink.split('?')[0];
369 //Check for faulty links
370 $selflink_replace = {
371 "import.php": "tbl_sql.php",
372 "tbl_chart.php": "sql.php",
373 "tbl_gis_visualization.php": "sql.php"
375 if ($selflink_replace[source]) {
376 var replacement = $selflink_replace[source];
377 data._selflink = data._selflink.replace(source, replacement);
379 $('#selflink > a').attr('href', data._selflink);
382 AJAX.scriptHandler.load(data._scripts);
384 if (data._selflink && data._scripts && data._menuHash && data._params) {
390 AJAX.source.attr('rel')
394 PMA_commonParams.setAll(data._params);
396 if (data._displayMessage) {
397 $('#page_content').prepend(data._displayMessage);
398 PMA_highlightSQL($('#page_content'));
401 $('#pma_errors').remove();
404 if(data._errSubmitMsg){
405 msg = data._errSubmitMsg;
408 $('<div/>', {id : 'pma_errors'})
409 .insertAfter('#selflink')
410 .append(data._errors);
411 // bind for php error reporting forms (bottom)
412 $("#pma_ignore_errors_bottom").bind("click", function() {
413 PMA_ignorePhpErrors();
415 $("#pma_ignore_all_errors_bottom").bind("click", function() {
416 PMA_ignorePhpErrors(false);
418 // In case of 'sendErrorReport'='always'
419 // submit the hidden error reporting form.
420 if (data._sendErrorAlways == '1' &&
421 data._stopErrorReportLoop != '1'
423 $("#pma_report_errors_form").submit();
424 PMA_ajaxShowMessage(PMA_messages.phpErrorsBeingSubmitted, false);
425 $('html, body').animate({scrollTop:$(document).height()}, 'slow');
426 } else if (data._promptPhpErrors) {
427 // otherwise just prompt user if it is set so.
428 msg = msg + PMA_messages.phpErrorsFound;
429 // scroll to bottom where all the errors are displayed.
430 $('html, body').animate({scrollTop:$(document).height()}, 'slow');
433 PMA_ajaxShowMessage(msg, false);
434 // bind for php error reporting forms (popup)
435 $("#pma_ignore_errors_popup").bind("click", function() {
436 PMA_ignorePhpErrors();
438 $("#pma_ignore_all_errors_popup").bind("click", function() {
439 PMA_ignorePhpErrors(false);
442 if (typeof AJAX._callback === 'function') {
443 AJAX._callback.call();
445 AJAX._callback = function () {};
449 PMA_ajaxShowMessage(data.error, false);
452 if (parseInt(data.redirect_flag) == 1) {
453 // add one more GET param to display session expiry msg
454 window.location.href += '&session_expired=1';
455 window.location.reload();
456 } else if (parseInt(data.reload_flag) == 1) {
457 // remove the token param and reload
458 window.location.href = window.location.href.replace(/&?token=[^&#]*/g, "");
459 window.location.reload();
461 if (data.fieldWithError) {
462 $(':input.error').removeClass("error");
463 $('#'+data.fieldWithError).addClass("error");
468 * This object is in charge of downloading scripts,
469 * keeping track of what's downloaded and firing
470 * the onload event for them when the page is ready.
474 * @var array _scripts The list of files already downloaded
478 * @var array _scriptsToBeLoaded The list of files that
479 * need to be downloaded
481 _scriptsToBeLoaded: [],
483 * @var array _scriptsToBeFired The list of files for which
484 * to fire the onload event
486 _scriptsToBeFired: [],
488 * Records that a file has been downloaded
490 * @param string file The filename
491 * @param string fire Whether this file will be registering
492 * onload/teardown events
494 * @return self For chaining
496 add: function (file, fire) {
497 this._scripts.push(file);
499 // Record whether to fire any events for the file
500 // This is necessary to correctly tear down the initial page
501 this._scriptsToBeFired.push(file);
506 * Download a list of js files in one request
508 * @param array files An array of filenames and flags
512 load: function (files) {
514 self._scriptsToBeLoaded = [];
515 self._scriptsToBeFired = [];
516 for (var i in files) {
517 self._scriptsToBeLoaded.push(files[i].name);
519 self._scriptsToBeFired.push(files[i].name);
522 // Generate a request string
524 var needRequest = false;
525 for (var index in self._scriptsToBeLoaded) {
526 var script = self._scriptsToBeLoaded[index];
527 // Only for scripts that we don't already have
528 if ($.inArray(script, self._scripts) == -1) {
531 request.push("scripts%5B%5D=" + script);
534 request.push("call_done=1");
535 // Download the composite js file, if necessary
537 this.appendScript("js/get_scripts.js.php?" + request.join("&"));
543 * Called whenever all files are loaded
548 if (typeof ErrorReport !== 'undefined') {
549 ErrorReport.wrap_global_functions();
551 for (var i in this._scriptsToBeFired) {
552 AJAX.fireOnload(this._scriptsToBeFired[i]);
557 * Appends a script element to the head to load the scripts
561 appendScript: function (url) {
562 var head = document.head || document.getElementsByTagName('head')[0];
563 var script = document.createElement('script');
564 script.type = 'text/javascript';
566 script.async = false;
567 head.appendChild(script);
570 * Fires all the teardown event handlers for the current page
571 * and rebinds all forms and links to the request handler
573 * @param function callback The callback to call after resetting
577 reset: function (callback) {
578 for (var i in this._scriptsToBeFired) {
579 AJAX.fireTeardown(this._scriptsToBeFired[i]);
581 this._scriptsToBeFired = [];
583 * Re-attach a generic event handler to clicks
584 * on pages and submissions of forms
586 $(document).off('click', 'a').on('click', 'a', AJAX.requestHandler);
587 $(document).off('submit', 'form').on('submit', 'form', AJAX.requestHandler);
595 * Here we register a function that will remove the onsubmit event from all
596 * forms that will be handled by the generic page loader. We then save this
597 * event handler in the "jQuery data", so that we can fire it up later in
598 * AJAX.requestHandler().
602 AJAX.registerOnload('functions.js', function () {
603 // Registering the onload event for functions.js
604 // ensures that it will be fired for all pages
605 $('form').not('.ajax').not('.disableAjax').each(function () {
606 if ($(this).attr('onsubmit')) {
607 $(this).data('onsubmit', this.onsubmit).attr('onsubmit', '');
612 * Workaround for passing submit button name,value on ajax form submit
613 * by appending hidden element with submit button name and value.
615 $("#page_content").on('click', 'form input[type=submit]', function() {
616 var buttonName = $(this).attr('name');
617 if (typeof buttonName === 'undefined') {
620 $(this).closest('form').append($('<input/>', {
623 'value': $(this).val()
628 * Attach event listener to events when user modify visible
629 * Input,Textarea and select fields to make changes in forms
631 $('#page_content').on(
633 'form.lock-page textarea, ' +
634 'form.lock-page input[type="text"], ' +
635 'form.lock-page input[type="number"], ' +
636 'form.lock-page select',
640 $('#page_content').on(
642 'form.lock-page input[type="checkbox"], ' +
643 'form.lock-page input[type="radio"]',
648 * Reset lock when lock-page form reset event is fired
649 * Note: reset does not bubble in all browser so attach to
652 $('form.lock-page').on('reset', function(event){
658 * An implementation of a client-side page cache.
659 * This object also uses the cache to provide a simple microhistory,
660 * that is the ability to use the back and forward buttons in the browser
664 * @var int The maximum number of pages to keep in the cache
668 * @var object A hash used to prime the cache with data about the initially
669 * loaded page. This is set in the footer, and then loaded
670 * by a double-queued event further down this file.
674 * @var array Stores the content of the cached pages
678 * @var int The index of the currently loaded page
679 * This is used to know at which point in the history we are
683 * Saves a new page in the cache
685 * @param string hash The hash part of the url that is being loaded
686 * @param array scripts A list of scripts that is required for the page
687 * @param string menu A hash that links to a menu stored
688 * in a dedicated menu cache
689 * @param array params A list of parameters used by PMA_commonParams()
690 * @param string rel A relationship to the current page:
691 * 'samepage': Forces the response to be treated as
692 * the same page as the current one
693 * 'newpage': Forces the response to be treated as
695 * undefined: Default behaviour, 'samepage' if the
696 * selflinks of the two pages are the same.
697 * 'newpage' otherwise
701 add: function (hash, scripts, menu, params, rel) {
702 if (this.pages.length > AJAX.cache.MAX) {
703 // Trim the cache, to the maximum number of allowed entries
704 // This way we will have a cached menu for every page
705 for (var i = 0; i < this.pages.length - this.MAX; i++) {
706 delete this.pages[i];
709 while (this.current < this.pages.length) {
710 // trim the cache if we went back in the history
711 // and are now going forward again
714 if (rel === 'newpage' ||
716 typeof rel === 'undefined' && (
717 typeof this.pages[this.current - 1] === 'undefined' ||
718 this.pages[this.current - 1].hash !== hash
724 content: $('#page_content').html(),
726 selflink: $('#selflink').html(),
730 AJAX.setUrlHash(this.current, hash);
735 * Restores a page from the cache. This is called when the hash
736 * part of the url changes and it's structure appears to be valid
738 * @param string index Which page from the history to load
742 navigate: function (index) {
743 if (typeof this.pages[index] === 'undefined' ||
744 typeof this.pages[index].content === 'undefined' ||
745 typeof this.pages[index].menu === 'undefined' ||
746 ! AJAX.cache.menus.get(this.pages[index].menu)
749 '<div class="error">' + PMA_messages.strInvalidPage + '</div>',
754 var record = this.pages[index];
755 AJAX.scriptHandler.reset(function () {
756 $('#page_content').html(record.content);
757 $('#selflink').html(record.selflink);
758 AJAX.cache.menus.replace(AJAX.cache.menus.get(record.menu));
759 PMA_commonParams.setAll(record.params);
760 AJAX.scriptHandler.load(record.scripts);
761 AJAX.cache.current = ++index;
766 * Resaves the content of the current page in the cache.
767 * Necessary in order not to show the user some outdated version of the page
771 update: function () {
772 var page = this.pages[this.current - 1];
774 page.content = $('#page_content').html();
778 * @var object Dedicated menu cache
782 * Returns the number of items in an associative array
786 size: function (obj) {
789 if (obj.hasOwnProperty(key)) {
796 * @var hash Stores the content of the cached menus
800 * Saves a new menu in the cache
802 * @param string hash The hash (trimmed md5) of the menu to be saved
803 * @param string content The HTML code of the menu to be saved
807 add: function (hash, content) {
808 if (this.size(this.data) > AJAX.cache.MAX) {
809 // when the cache grows, we remove the oldest entry
810 var oldest, key, init = 0;
811 for (var i in this.data) {
813 if (! init || this.data[i].timestamp.getTime() < oldest.getTime()) {
814 oldest = this.data[i].timestamp;
820 delete this.data[key];
824 timestamp: new Date()
828 * Retrieves a menu given its hash
830 * @param string hash The hash of the menu to be retrieved
834 get: function (hash) {
835 if (this.data[hash]) {
836 return this.data[hash].content;
838 // This should never happen as long as the number of stored menus
839 // is larger or equal to the number of pages in the page cache
844 * Prepares part of the parameter string used during page requests,
845 * this is necessary to tell the server which menus we have in the cache
849 getRequestParam: function () {
852 for (var i in this.data) {
855 var menuHashesParam = menuHashes.join('-');
856 if (menuHashesParam) {
857 param = '&menuHashes=' + menuHashesParam;
862 * Replaces the menu with new content
866 replace: function (content) {
867 $('#floating_menubar').html(content)
868 // Remove duplicate wrapper
869 // TODO: don't send it in the response
870 .children().first().remove();
871 $('#topmenu').menuResizer(PMA_mainMenuResizerCallback);
877 * URL hash management module.
878 * Allows direct bookmarking and microhistory.
880 AJAX.setUrlHash = (function (jQuery, window) {
883 * Indictaes whether we have already completed
884 * the initialisation of the hash
890 * Stores a hash that needed to be set when we were not ready
896 * Flag to indicate if the change of hash was triggered
897 * by a user pressing the back/forward button or if
898 * the change was triggered internally
902 var userChange = true;
904 // Fix favicon disappearing in Firefox when setting location.hash
905 function resetFavicon() {
906 if (navigator.userAgent.indexOf('Firefox') > -1) {
907 // Move the link tags for the favicon to the bottom
908 // of the head element to force a reload of the favicon
909 $('head > link[href=favicon\\.ico]').appendTo('head');
914 * Sets the hash part of the URL
918 function setUrlHash(index, hash) {
921 * Setting hash leads to reload in webkit:
922 * http://www.quirksmode.org/bugreports/archives/2005/05/Safari_13_visual_anomaly_with_windowlocationhref.html
924 * so we expect that users are not running an ancient Safari version
929 window.location.hash = "PMAURL-" + index + ":" + hash;
932 savedHash = "PMAURL-" + index + ":" + hash;
936 * Start initialisation
938 if (window.location.hash.substring(0, 8) == '#PMAURL-') {
939 // We have a valid hash, let's redirect the user
940 // to the page that it's pointing to
941 var colon_position = window.location.hash.indexOf(':');
942 var questionmark_position = window.location.hash.indexOf('?');
943 if (colon_position != -1 && questionmark_position != -1 && colon_position < questionmark_position) {
944 var hash_url = window.location.hash.substring(colon_position + 1, questionmark_position);
945 if (PMA_gotoWhitelist.indexOf(hash_url) != -1) {
946 window.location = window.location.hash.substring(
952 // We don't have a valid hash, so we'll set it up
953 // when the page finishes loading
955 /* Check if we should set URL */
956 if (savedHash !== "") {
957 window.location.hash = savedHash;
961 // Indicate that we're done initialising
966 * Register an event handler for when the url hash changes
969 jQuery(window).hashchange(function () {
970 if (userChange === false) {
971 // Ignore internally triggered hash changes
973 } else if (/^#PMAURL-\d+:/.test(window.location.hash)) {
974 // Change page if the hash changed was triggered by a user action
975 var index = window.location.hash.substring(
976 8, window.location.hash.indexOf(':')
978 AJAX.cache.navigate(index);
983 * Publicly exposes a reference to the otherwise private setUrlHash function
989 * Page load event handler
992 // Add the menu from the initial page into the cache
993 // The cache primer is set by the footer class
994 if (AJAX.cache.primer.url) {
995 AJAX.cache.menus.add(
996 AJAX.cache.primer.menuHash,
998 .append('<div></div>')
999 .append($('#serverinfo').clone())
1000 .append($('#topmenucontainer').clone())
1005 // Queue up this event twice to make sure that we get a copy
1006 // of the page after all other onload events have been fired
1007 if (AJAX.cache.primer.url) {
1009 AJAX.cache.primer.url,
1010 AJAX.cache.primer.scripts,
1011 AJAX.cache.primer.menuHash
1018 * Attach a generic event handler to clicks
1019 * on pages and submissions of forms
1021 $(document).on('click', 'a', AJAX.requestHandler);
1022 $(document).on('submit', 'form', AJAX.requestHandler);
1025 * Gracefully handle fatal server errors
1026 * (e.g: 500 - Internal server error)
1028 $(document).ajaxError(function (event, request, settings) {
1029 if (request.status !== 0) { // Don't handle aborted requests
1030 var errorCode = PMA_sprintf(PMA_messages.strErrorCode, request.status);
1031 var errorText = PMA_sprintf(PMA_messages.strErrorText, request.statusText);
1032 PMA_ajaxShowMessage(
1033 '<div class="error">' +
1034 PMA_messages.strErrorProcessingRequest +
1035 '<div>' + errorCode + '</div>' +
1036 '<div>' + errorText + '</div>' +
1040 AJAX.active = false;