Translated using Weblate (Slovenian)
[phpmyadmin.git] / js / ajax.js
blob8e9af8940d3d0adb84daa357d1e5e7354f812d81
1 /* vim: set expandtab sw=4 ts=4 sts=4: */
2 /**
3  * This object handles ajax requests for pages. It also
4  * handles the reloading of the main menu and scripts.
5  */
6 var AJAX = {
7     /**
8      * @var bool active Whether we are busy
9      */
10     active: false,
11     /**
12      * @var object source The object whose event initialized the request
13      */
14     source: null,
15     /**
16      * @var object xhr A reference to the ajax request that is currently running
17      */
18     xhr: null,
19     /**
20      * @var object lockedTargets, list of locked targets
21      */
22     lockedTargets: {},
23     /**
24      * @var function Callback to execute after a successful request
25      *               Used by PMA_commonFunctions from common.js
26      */
27     _callback: function () {},
28     /**
29      * @var bool _debug Makes noise in your Firebug console
30      */
31     _debug: false,
32     /**
33      * @var object $msgbox A reference to a jQuery object that links to a message
34      *                     box that is generated by PMA_ajaxShowMessage()
35      */
36     $msgbox: null,
37     /**
38      * Given the filename of a script, returns a hash to be
39      * used to refer to all the events registered for the file
40      *
41      * @param key string key The filename for which to get the event name
42      *
43      * @return int
44      */
45     hash: function (key) {
46         /* http://burtleburtle.net/bob/hash/doobs.html#one */
47         key += "";
48         var len = key.length, hash = 0, i = 0;
49         for (; i < len; ++i) {
50             hash += key.charCodeAt(i);
51             hash += (hash << 10);
52             hash ^= (hash >> 6);
53         }
54         hash += (hash << 3);
55         hash ^= (hash >> 11);
56         hash += (hash << 15);
57         return Math.abs(hash);
58     },
59     /**
60      * Registers an onload event for a file
61      *
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
64      *
65      * @return self For chaining
66      */
67     registerOnload: function (file, func) {
68         var eventName = 'onload_' + AJAX.hash(file);
69         $(document).bind(eventName, func);
70         if (this._debug) {
71             console.log(
72                 // no need to translate
73                 "Registered event " + eventName + " for file " + file
74             );
75         }
76         return this;
77     },
78     /**
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.
81      *
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
85      *
86      * @return self For chaining
87      */
88     registerTeardown: function (file, func) {
89         var eventName = 'teardown_' + AJAX.hash(file);
90         $(document).bind(eventName, func);
91         if (this._debug) {
92             console.log(
93                 // no need to translate
94                 "Registered event " + eventName + " for file " + file
95             );
96         }
97         return this;
98     },
99     /**
100      * Called when a page has finished loading, once for every
101      * file that registered to the onload event of that file.
102      *
103      * @param string file The filename for which to fire the event
104      *
105      * @return void
106      */
107     fireOnload: function (file) {
108         var eventName = 'onload_' + AJAX.hash(file);
109         $(document).trigger(eventName);
110         if (this._debug) {
111             console.log(
112                 // no need to translate
113                 "Fired event " + eventName + " for file " + file
114             );
115         }
116     },
117     /**
118      * Called just before a page is torn down, once for every
119      * file that registered to the teardown event of that file.
120      *
121      * @param string file The filename for which to fire the event
122      *
123      * @return void
124      */
125     fireTeardown: function (file) {
126         var eventName = 'teardown_' + AJAX.hash(file);
127         $(document).triggerHandler(eventName);
128         if (this._debug) {
129             console.log(
130                 // no need to translate
131                 "Fired event " + eventName + " for file " + file
132             );
133         }
134     },
135     /**
136      * function to handle lock page mechanism
137      *
138      * @param event the event object
139      *
140      * @return void
141      */
142     lockPageHandler: function(event) {
143         //Don't lock on enter.
144         if (0 == event.charCode) {
145             return;
146         }
148         var lockId = $(this).data('lock-id');
149         if (typeof lockId === 'undefined') {
150             return;
151         }
152         /*
153          * @todo Fix Code mirror does not give correct full value (query)
154          * in textarea, it returns only the change in content.
155          */
156         var newHash = null;
157         if (event.data.value == 1) {
158             newHash = AJAX.hash($(this).val());
159         } else {
160             newHash = AJAX.hash($(this).is(":checked"));
161         }
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;
167         } else {
168             delete AJAX.lockedTargets[lockId];
169         }
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());
174         } else {
175             $('#lock_page_icon').html('');
176         }
177     },
178     /**
179      * resets the lock
180      *
181      * @return void
182      */
183     resetLock: function() {
184         AJAX.lockedTargets = {};
185         $('#lock_page_icon').html('');
186     },
187     /**
188      * Event handler for clicks on links and form submissions
189      *
190      * @param object e Event data
191      *
192      * @return void
193      */
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)) {
200             return true;
201         } else if ($(this).attr('target')) {
202             return true;
203         } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
204             //reset the lockedTargets object, as specified AJAX operation has finished
205             AJAX.resetLock();
206             return true;
207         } else if (href && href.match(/^#/)) {
208             return true;
209         } else if (href && href.match(/^mailto/)) {
210             return true;
211         } else if ($(this).hasClass('ui-datepicker-next') ||
212             $(this).hasClass('ui-datepicker-prev')
213         ) {
214             return true;
215         }
217         if (typeof event != 'undefined') {
218             event.preventDefault();
219             event.stopImmediatePropagation();
220         }
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
230         ) {
231             return false;
232         }
233         AJAX.resetLock();
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.
239             if (AJAX.xhr) {
240                 //In case of a link request, attempt aborting
241                 AJAX.xhr.abort();
242                 if(AJAX.xhr.status === 0 && AJAX.xhr.statusText === 'abort') {
243                     //If aborted
244                     AJAX.$msgbox = PMA_ajaxShowMessage(PMA_messages.strAbortedRequest);
245                     AJAX.active = false;
246                     AJAX.xhr = null;
247                 } else {
248                     //If can't abort
249                     return false;
250                 }
251             } else {
252                 //In case submitting a form, don't attempt aborting
253                 return false;
254             }
255         }
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';
264         if (! isLink) {
265             params += '&' + $(this).serialize();
266         }
267         // Add a list of menu hashes that we have in the cache to the request
268         params += AJAX.cache.menus.getRequestParam();
270         if (AJAX._debug) {
271             console.log("Loading: " + url); // no need to translate
272         }
274         if (isLink) {
275             AJAX.active = true;
276             AJAX.$msgbox = PMA_ajaxShowMessage();
277             //Save reference for the new link request
278             AJAX.xhr = $.get(url, params, AJAX.responseHandler);
279         } else {
280             /**
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
284              */
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])) {
289                 AJAX.active = true;
290                 AJAX.$msgbox = PMA_ajaxShowMessage();
291                 $.post(url, params, AJAX.responseHandler);
292             }
293         }
294     },
295     /**
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.
300      *
301      * To refer to self use 'AJAX', instead of 'this' as this function
302      * is called in the jQuery context.
303      *
304      * @param object e Event data
305      *
306      * @return void
307      */
308     responseHandler: function (data) {
309         if (typeof data === 'undefined' || data === null) {
310             return;
311         }
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);
318                 AJAX.active = false;
319                 AJAX.xhr = null;
320                 return;
321             }
323             AJAX.scriptHandler.reset(function () {
324                 if (data._reloadNavigation) {
325                     PMA_reloadNavigation();
326                 }
327                 if (data._title) {
328                     $('title').replaceWith(data._title);
329                 }
330                 if (data._menu) {
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));
335                 }
336                 if (data._disableNaviSettings) {
337                     PMA_disableNaviSettings();
338                 }
339                 else {
340                     PMA_ensureNaviSettings(data._selflink);
341                 }
343                 // Remove all containers that may have
344                 // been added outside of #page_content
345                 $('body').children()
346                     .not('#pma_navigation')
347                     .not('#floating_menubar')
348                     .not('#page_nav_icons')
349                     .not('#page_content')
350                     .not('#selflink')
351                     .not('#pma_header')
352                     .not('#pma_footer')
353                     .not('#pma_demo')
354                     .not('#pma_console_container')
355                     .not('#prefs_autoload')
356                     .remove();
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>"
361                     );
362                     PMA_highlightSQL($('#page_content'));
363                     checkNumberOfFields();
364                 }
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"
374                     };
375                     if ($selflink_replace[source]) {
376                         var replacement = $selflink_replace[source];
377                         data._selflink = data._selflink.replace(source, replacement);
378                     }
379                     $('#selflink > a').attr('href', data._selflink);
380                 }
381                 if (data._scripts) {
382                     AJAX.scriptHandler.load(data._scripts);
383                 }
384                 if (data._selflink && data._scripts && data._menuHash && data._params) {
385                     AJAX.cache.add(
386                         data._selflink,
387                         data._scripts,
388                         data._menuHash,
389                         data._params,
390                         AJAX.source.attr('rel')
391                     );
392                 }
393                 if (data._params) {
394                     PMA_commonParams.setAll(data._params);
395                 }
396                 if (data._displayMessage) {
397                     $('#page_content').prepend(data._displayMessage);
398                     PMA_highlightSQL($('#page_content'));
399                 }
401                 $('#pma_errors').remove();
403                 var msg = '';
404                 if(data._errSubmitMsg){
405                     msg = data._errSubmitMsg;
406                 }
407                 if (data._errors) {
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();
414                     });
415                     $("#pma_ignore_all_errors_bottom").bind("click", function() {
416                         PMA_ignorePhpErrors(false);
417                     });
418                     // In case of 'sendErrorReport'='always'
419                     // submit the hidden error reporting form.
420                     if (data._sendErrorAlways == '1' &&
421                         data._stopErrorReportLoop != '1'
422                     ) {
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');
431                     }
432                 }
433                 PMA_ajaxShowMessage(msg, false);
434                 // bind for php error reporting forms (popup)
435                 $("#pma_ignore_errors_popup").bind("click", function() {
436                     PMA_ignorePhpErrors();
437                 });
438                 $("#pma_ignore_all_errors_popup").bind("click", function() {
439                     PMA_ignorePhpErrors(false);
440                 });
442                 if (typeof AJAX._callback === 'function') {
443                     AJAX._callback.call();
444                 }
445                 AJAX._callback = function () {};
446             });
448         } else {
449             PMA_ajaxShowMessage(data.error, false);
450             AJAX.active = false;
451             AJAX.xhr = null;
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();
460             }
461             if (data.fieldWithError) {
462                 $(':input.error').removeClass("error");
463                 $('#'+data.fieldWithError).addClass("error");
464             }
465         }
466     },
467     /**
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.
471      */
472     scriptHandler: {
473         /**
474          * @var array _scripts The list of files already downloaded
475          */
476         _scripts: [],
477         /**
478          * @var array _scriptsToBeLoaded The list of files that
479          *                               need to be downloaded
480          */
481         _scriptsToBeLoaded: [],
482         /**
483          * @var array _scriptsToBeFired The list of files for which
484          *                              to fire the onload event
485          */
486         _scriptsToBeFired: [],
487         /**
488          * Records that a file has been downloaded
489          *
490          * @param string file The filename
491          * @param string fire Whether this file will be registering
492          *                    onload/teardown events
493          *
494          * @return self For chaining
495          */
496         add: function (file, fire) {
497             this._scripts.push(file);
498             if (fire) {
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);
502             }
503             return this;
504         },
505         /**
506          * Download a list of js files in one request
507          *
508          * @param array files An array of filenames and flags
509          *
510          * @return void
511          */
512         load: function (files) {
513             var self = this;
514             self._scriptsToBeLoaded = [];
515             self._scriptsToBeFired = [];
516             for (var i in files) {
517                 self._scriptsToBeLoaded.push(files[i].name);
518                 if (files[i].fire) {
519                     self._scriptsToBeFired.push(files[i].name);
520                 }
521             }
522             // Generate a request string
523             var request = [];
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) {
529                     needRequest = true;
530                     this.add(script);
531                     request.push("scripts%5B%5D=" + script);
532                 }
533             }
534             request.push("call_done=1");
535             // Download the composite js file, if necessary
536             if (needRequest) {
537                 this.appendScript("js/get_scripts.js.php?" + request.join("&"));
538             } else {
539                 self.done();
540             }
541         },
542         /**
543          * Called whenever all files are loaded
544          *
545          * @return void
546          */
547         done: function () {
548             if (typeof ErrorReport !== 'undefined') {
549                 ErrorReport.wrap_global_functions();
550             }
551             for (var i in this._scriptsToBeFired) {
552                 AJAX.fireOnload(this._scriptsToBeFired[i]);
553             }
554             AJAX.active = false;
555         },
556         /**
557          * Appends a script element to the head to load the scripts
558          *
559          * @return void
560          */
561         appendScript: function (url) {
562             var head = document.head || document.getElementsByTagName('head')[0];
563             var script = document.createElement('script');
564             script.type = 'text/javascript';
565             script.src = url;
566             script.async = false;
567             head.appendChild(script);
568         },
569         /**
570          * Fires all the teardown event handlers for the current page
571          * and rebinds all forms and links to the request handler
572          *
573          * @param function callback The callback to call after resetting
574          *
575          * @return void
576          */
577         reset: function (callback) {
578             for (var i in this._scriptsToBeFired) {
579                 AJAX.fireTeardown(this._scriptsToBeFired[i]);
580             }
581             this._scriptsToBeFired = [];
582             /**
583              * Re-attach a generic event handler to clicks
584              * on pages and submissions of forms
585              */
586             $(document).off('click', 'a').on('click', 'a', AJAX.requestHandler);
587             $(document).off('submit', 'form').on('submit', 'form', AJAX.requestHandler);
588             AJAX.cache.update();
589             callback();
590         }
591     }
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().
600  * See bug #3583316
601  */
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', '');
608         }
609     });
611     /**
612      * Workaround for passing submit button name,value on ajax form submit
613      * by appending hidden element with submit button name and value.
614      */
615     $("#page_content").on('click', 'form input[type=submit]', function() {
616         var buttonName = $(this).attr('name');
617         if (typeof buttonName === 'undefined') {
618             return;
619         }
620         $(this).closest('form').append($('<input/>', {
621             'type' : 'hidden',
622             'name' : buttonName,
623             'value': $(this).val()
624         }));
625     });
627     /**
628      * Attach event listener to events when user modify visible
629      * Input,Textarea and select fields to make changes in forms
630      */
631     $('#page_content').on(
632         'keyup change',
633         'form.lock-page textarea, ' +
634         'form.lock-page input[type="text"], ' +
635         'form.lock-page input[type="number"], ' +
636         'form.lock-page select',
637         {value:1},
638         AJAX.lockPageHandler
639     );
640     $('#page_content').on(
641         'change',
642         'form.lock-page input[type="checkbox"], ' +
643         'form.lock-page input[type="radio"]',
644         {value:2},
645         AJAX.lockPageHandler
646     );
647     /**
648      * Reset lock when lock-page form reset event is fired
649      * Note: reset does not bubble in all browser so attach to
650      * form directly.
651      */
652     $('form.lock-page').on('reset', function(event){
653         AJAX.resetLock();
654     });
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
661  */
662 AJAX.cache = {
663     /**
664      * @var int The maximum number of pages to keep in the cache
665      */
666     MAX: 6,
667     /**
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.
671      */
672     primer: {},
673     /**
674      * @var array Stores the content of the cached pages
675      */
676     pages: [],
677     /**
678      * @var int The index of the currently loaded page
679      *          This is used to know at which point in the history we are
680      */
681     current: 0,
682     /**
683      * Saves a new page in the cache
684      *
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
694      *                                   a new page
695      *                       undefined:  Default behaviour, 'samepage' if the
696      *                                   selflinks of the two pages are the same.
697      *                                   'newpage' otherwise
698      *
699      * @return void
700      */
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];
707             }
708         }
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
712             this.pages.pop();
713         }
714         if (rel === 'newpage' ||
715             (
716                 typeof rel === 'undefined' && (
717                     typeof this.pages[this.current - 1] === 'undefined' ||
718                     this.pages[this.current - 1].hash !== hash
719                 )
720             )
721         ) {
722             this.pages.push({
723                 hash: hash,
724                 content: $('#page_content').html(),
725                 scripts: scripts,
726                 selflink: $('#selflink').html(),
727                 menu: menu,
728                 params: params
729             });
730             AJAX.setUrlHash(this.current, hash);
731             this.current++;
732         }
733     },
734     /**
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
737      *
738      * @param string index Which page from the history to load
739      *
740      * @return void
741      */
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)
747         ) {
748             PMA_ajaxShowMessage(
749                 '<div class="error">' + PMA_messages.strInvalidPage + '</div>',
750                 false
751             );
752         } else {
753             AJAX.active = true;
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;
762             });
763         }
764     },
765     /**
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
768      *
769      * @return void
770      */
771     update: function () {
772         var page = this.pages[this.current - 1];
773         if (page) {
774             page.content = $('#page_content').html();
775         }
776     },
777     /**
778      * @var object Dedicated menu cache
779      */
780     menus: {
781         /**
782          * Returns the number of items in an associative array
783          *
784          * @return int
785          */
786         size: function (obj) {
787             var size = 0, key;
788             for (key in obj) {
789                 if (obj.hasOwnProperty(key)) {
790                     size++;
791                 }
792             }
793             return size;
794         },
795         /**
796          * @var hash Stores the content of the cached menus
797          */
798         data: {},
799         /**
800          * Saves a new menu in the cache
801          *
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
804          *
805          * @return void
806          */
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) {
812                     if (this.data[i]) {
813                         if (! init || this.data[i].timestamp.getTime() < oldest.getTime()) {
814                             oldest = this.data[i].timestamp;
815                             key = i;
816                             init = 1;
817                         }
818                     }
819                 }
820                 delete this.data[key];
821             }
822             this.data[hash] = {
823                 content: content,
824                 timestamp: new Date()
825             };
826         },
827         /**
828          * Retrieves a menu given its hash
829          *
830          * @param string hash The hash of the menu to be retrieved
831          *
832          * @return string
833          */
834         get: function (hash) {
835             if (this.data[hash]) {
836                 return this.data[hash].content;
837             } else {
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
840                 return '';
841             }
842         },
843         /**
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
846          *
847          * @return string
848          */
849         getRequestParam: function () {
850             var param = '';
851             var menuHashes = [];
852             for (var i in this.data) {
853                 menuHashes.push(i);
854             }
855             var menuHashesParam = menuHashes.join('-');
856             if (menuHashesParam) {
857                 param = '&menuHashes=' + menuHashesParam;
858             }
859             return param;
860         },
861         /**
862          * Replaces the menu with new content
863          *
864          * @return void
865          */
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);
872         }
873     }
877  * URL hash management module.
878  * Allows direct bookmarking and microhistory.
879  */
880 AJAX.setUrlHash = (function (jQuery, window) {
881     "use strict";
882     /**
883      * Indictaes whether we have already completed
884      * the initialisation of the hash
885      *
886      * @access private
887      */
888     var ready = false;
889     /**
890      * Stores a hash that needed to be set when we were not ready
891      *
892      * @access private
893      */
894     var savedHash = "";
895     /**
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
899      *
900      * @access private
901      */
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');
910         }
911     }
913     /**
914      * Sets the hash part of the URL
915      *
916      * @access public
917      */
918     function setUrlHash(index, hash) {
919         /*
920          * Known problem:
921          * Setting hash leads to reload in webkit:
922          * http://www.quirksmode.org/bugreports/archives/2005/05/Safari_13_visual_anomaly_with_windowlocationhref.html
923          *
924          * so we expect that users are not running an ancient Safari version
925          */
927         userChange = false;
928         if (ready) {
929             window.location.hash = "PMAURL-" + index + ":" + hash;
930             resetFavicon();
931         } else {
932             savedHash = "PMAURL-" + index + ":" + hash;
933         }
934     }
935     /**
936      * Start initialisation
937      */
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(
947                     colon_position + 1
948                 );
949             }
950         }
951     } else {
952         // We don't have a valid hash, so we'll set it up
953         // when the page finishes loading
954         jQuery(function () {
955             /* Check if we should set URL */
956             if (savedHash !== "") {
957                 window.location.hash = savedHash;
958                 savedHash = "";
959                 resetFavicon();
960             }
961             // Indicate that we're done initialising
962             ready = true;
963         });
964     }
965     /**
966      * Register an event handler for when the url hash changes
967      */
968     jQuery(function () {
969         jQuery(window).hashchange(function () {
970             if (userChange === false) {
971                 // Ignore internally triggered hash changes
972                 userChange = true;
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(':')
977                 );
978                 AJAX.cache.navigate(index);
979             }
980         });
981     });
982     /**
983      * Publicly exposes a reference to the otherwise private setUrlHash function
984      */
985     return setUrlHash;
986 })(jQuery, window);
989  * Page load event handler
990  */
991 $(function () {
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,
997             $('<div></div>')
998                 .append('<div></div>')
999                 .append($('#serverinfo').clone())
1000                 .append($('#topmenucontainer').clone())
1001                 .html()
1002         );
1003     }
1004     $(function () {
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) {
1008             AJAX.cache.add(
1009                 AJAX.cache.primer.url,
1010                 AJAX.cache.primer.scripts,
1011                 AJAX.cache.primer.menuHash
1012             );
1013         }
1014     });
1018  * Attach a generic event handler to clicks
1019  * on pages and submissions of forms
1020  */
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)
1027  */
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>' +
1037             '</div>',
1038             false
1039         );
1040         AJAX.active = false;
1041     }