Translated using Weblate (Swedish)
[phpmyadmin.git] / js / functions.js
blob7dc99619575ba68f44ad5f1a92e3c6946ca18083
1 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 /* global isStorageSupported */ // js/config.js
4 /* global ChartType, ColumnType, DataTable, JQPlotChartFactory */ // js/chart.js
5 /* global DatabaseStructure */ // js/database/structure.js
6 /* global mysqlDocBuiltin, mysqlDocKeyword */ // js/doclinks.js
7 /* global Indexes */ // js/indexes.js
8 /* global maxInputVars, mysqlDocTemplate, pmaThemeImage */ // js/messages.php
9 /* global MicroHistory */ // js/microhistory.js
10 /* global checkPasswordStrength */ // js/server/privileges.js
11 /* global sprintf */ // js/vendor/sprintf.js
12 /* global Int32Array */ // ES6
14 /**
15  * general function, usually for data manipulation pages
16  *
17  */
18 var Functions = {};
20 /**
21  * @var sqlBoxLocked lock for the sqlbox textarea in the querybox
22  */
23 // eslint-disable-next-line no-unused-vars
24 var sqlBoxLocked = false;
26 /**
27  * @var {array} holds elements which content should only selected once
28  */
29 var onlyOnceElements = [];
31 /**
32  * @var {int} ajaxMessageCount Number of AJAX messages shown since page load
33  */
34 var ajaxMessageCount = 0;
36 /**
37  * @var codeMirrorEditor object containing CodeMirror editor of the query editor in SQL tab
38  */
39 var codeMirrorEditor = false;
41 /**
42  * @var codeMirrorInlineEditor object containing CodeMirror editor of the inline query editor
43  */
44 var codeMirrorInlineEditor = false;
46 /**
47  * @var {boolean} sqlAutoCompleteInProgress shows if Table/Column name autocomplete AJAX is in progress
48  */
49 var sqlAutoCompleteInProgress = false;
51 /**
52  * @var sqlAutoComplete object containing list of columns in each table
53  */
54 var sqlAutoComplete = false;
56 /**
57  * @var {string} sqlAutoCompleteDefaultTable string containing default table to autocomplete columns
58  */
59 var sqlAutoCompleteDefaultTable = '';
61 /**
62  * @var {array} centralColumnList array to hold the columns in central list per db.
63  */
64 var centralColumnList = [];
66 /**
67  * @var {array} primaryIndexes array to hold 'Primary' index columns.
68  */
69 // eslint-disable-next-line no-unused-vars
70 var primaryIndexes = [];
72 /**
73  * @var {array} uniqueIndexes array to hold 'Unique' index columns.
74  */
75 // eslint-disable-next-line no-unused-vars
76 var uniqueIndexes = [];
78 /**
79  * @var {array} indexes array to hold 'Index' columns.
80  */
81 // eslint-disable-next-line no-unused-vars
82 var indexes = [];
84 /**
85  * @var {array} fulltextIndexes array to hold 'Fulltext' columns.
86  */
87 // eslint-disable-next-line no-unused-vars
88 var fulltextIndexes = [];
90 /**
91  * @var {array} spatialIndexes array to hold 'Spatial' columns.
92  */
93 // eslint-disable-next-line no-unused-vars
94 var spatialIndexes = [];
96 /**
97  * Make sure that ajax requests will not be cached
98  * by appending a random variable to their parameters
99  */
100 $.ajaxPrefilter(function (options, originalOptions) {
101     var nocache = new Date().getTime() + '' + Math.floor(Math.random() * 1000000);
102     if (typeof options.data === 'string') {
103         options.data += '&_nocache=' + nocache + '&token=' + encodeURIComponent(CommonParams.get('token'));
104     } else if (typeof options.data === 'object') {
105         options.data = $.extend(originalOptions.data, { '_nocache' : nocache, 'token': CommonParams.get('token') });
106     }
110  * Adds a date/time picker to an element
112  * @param {object} $thisElement a jQuery object pointing to the element
113  */
114 Functions.addDatepicker = function ($thisElement, type, options) {
115     if (type !== 'date' && type !== 'time' && type !== 'datetime' && type !== 'timestamp') {
116         return;
117     }
119     var showTimepicker = true;
120     if (type === 'date') {
121         showTimepicker = false;
122     }
124     // Getting the current Date and time
125     var currentDateTime = new Date();
127     var defaultOptions = {
128         timeInput : true,
129         hour: currentDateTime.getHours(),
130         minute: currentDateTime.getMinutes(),
131         second: currentDateTime.getSeconds(),
132         showOn: 'button',
133         buttonImage: pmaThemeImage + 'b_calendar.png',
134         buttonImageOnly: true,
135         stepMinutes: 1,
136         stepHours: 1,
137         showSecond: true,
138         showMillisec: true,
139         showMicrosec: true,
140         showTimepicker: showTimepicker,
141         showButtonPanel: false,
142         dateFormat: 'yy-mm-dd', // yy means year with four digits
143         timeFormat: 'HH:mm:ss.lc',
144         constrainInput: false,
145         altFieldTimeOnly: false,
146         showAnim: '',
147         beforeShow: function (input, inst) {
148             // Remember that we came from the datepicker; this is used
149             // in table/change.js by verificationsAfterFieldChange()
150             $thisElement.data('comes_from', 'datepicker');
151             if ($(input).closest('.cEdit').length > 0) {
152                 setTimeout(function () {
153                     inst.dpDiv.css({
154                         top: 0,
155                         left: 0,
156                         position: 'relative'
157                     });
158                 }, 0);
159             }
160             setTimeout(function () {
161                 // Fix wrong timepicker z-index, doesn't work without timeout
162                 $('#ui-timepicker-div').css('z-index', $('#ui-datepicker-div').css('z-index'));
163                 // Integrate tooltip text into dialog
164                 var tooltip = $thisElement.tooltip('instance');
165                 if (typeof tooltip !== 'undefined') {
166                     tooltip.disable();
167                     var $note = $('<p class="note"></div>');
168                     $note.text(tooltip.option('content'));
169                     $('div.ui-datepicker').append($note);
170                 }
171             }, 0);
172         },
173         onSelect: function () {
174             $thisElement.data('datepicker').inline = true;
175         },
176         onClose: function () {
177             // The value is no more from the date picker
178             $thisElement.data('comes_from', '');
179             if (typeof $thisElement.data('datepicker') !== 'undefined') {
180                 $thisElement.data('datepicker').inline = false;
181             }
182             var tooltip = $thisElement.tooltip('instance');
183             if (typeof tooltip !== 'undefined') {
184                 tooltip.enable();
185             }
186         }
187     };
188     if (type === 'time') {
189         $thisElement.timepicker($.extend(defaultOptions, options));
190         // Add a tip regarding entering MySQL allowed-values for TIME data-type
191         Functions.tooltip($thisElement, 'input', Messages.strMysqlAllowedValuesTipTime);
192     } else {
193         $thisElement.datetimepicker($.extend(defaultOptions, options));
194     }
198  * Add a date/time picker to each element that needs it
199  * (only when jquery-ui-timepicker-addon.js is loaded)
200  */
201 Functions.addDateTimePicker = function () {
202     if ($.timepicker !== undefined) {
203         $('input.timefield, input.datefield, input.datetimefield').each(function () {
204             var decimals = $(this).parent().attr('data-decimals');
205             var type = $(this).parent().attr('data-type');
207             var showMillisec = false;
208             var showMicrosec = false;
209             var timeFormat = 'HH:mm:ss';
210             var hourMax = 23;
211             // check for decimal places of seconds
212             if (decimals > 0 && type.indexOf('time') !== -1) {
213                 if (decimals > 3) {
214                     showMillisec = true;
215                     showMicrosec = true;
216                     timeFormat = 'HH:mm:ss.lc';
217                 } else {
218                     showMillisec = true;
219                     timeFormat = 'HH:mm:ss.l';
220                 }
221             }
222             if (type === 'time') {
223                 hourMax = 99;
224             }
225             Functions.addDatepicker($(this), type, {
226                 showMillisec: showMillisec,
227                 showMicrosec: showMicrosec,
228                 timeFormat: timeFormat,
229                 hourMax: hourMax
230             });
231             // Add a tip regarding entering MySQL allowed-values
232             // for TIME and DATE data-type
233             if ($(this).hasClass('timefield')) {
234                 Functions.tooltip($(this), 'input', Messages.strMysqlAllowedValuesTipTime);
235             } else if ($(this).hasClass('datefield')) {
236                 Functions.tooltip($(this), 'input', Messages.strMysqlAllowedValuesTipDate);
237             }
238         });
239     }
243  * Handle redirect and reload flags sent as part of AJAX requests
245  * @param data ajax response data
246  */
247 Functions.handleRedirectAndReload = function (data) {
248     if (parseInt(data.redirect_flag) === 1) {
249         // add one more GET param to display session expiry msg
250         if (window.location.href.indexOf('?') === -1) {
251             window.location.href += '?session_expired=1';
252         } else {
253             window.location.href += CommonParams.get('arg_separator') + 'session_expired=1';
254         }
255         window.location.reload();
256     } else if (parseInt(data.reload_flag) === 1) {
257         window.location.reload();
258     }
262  * Creates an SQL editor which supports auto completing etc.
264  * @param $textarea   jQuery object wrapping the textarea to be made the editor
265  * @param options     optional options for CodeMirror
266  * @param resize      optional resizing ('vertical', 'horizontal', 'both')
267  * @param lintOptions additional options for lint
268  */
269 Functions.getSqlEditor = function ($textarea, options, resize, lintOptions) {
270     var resizeType = resize;
271     if ($textarea.length > 0 && typeof CodeMirror !== 'undefined') {
272         // merge options for CodeMirror
273         var defaults = {
274             lineNumbers: true,
275             matchBrackets: true,
276             extraKeys: { 'Ctrl-Space': 'autocomplete' },
277             hintOptions: { 'completeSingle': false, 'completeOnSingleClick': true },
278             indentUnit: 4,
279             mode: 'text/x-mysql',
280             lineWrapping: true
281         };
283         if (CodeMirror.sqlLint) {
284             $.extend(defaults, {
285                 gutters: ['CodeMirror-lint-markers'],
286                 lint: {
287                     'getAnnotations': CodeMirror.sqlLint,
288                     'async': true,
289                     'lintOptions': lintOptions
290                 }
291             });
292         }
294         $.extend(true, defaults, options);
296         // create CodeMirror editor
297         var codemirrorEditor = CodeMirror.fromTextArea($textarea[0], defaults);
298         // allow resizing
299         if (! resizeType) {
300             resizeType = 'vertical';
301         }
302         var handles = '';
303         if (resizeType === 'vertical') {
304             handles = 's';
305         }
306         if (resizeType === 'both') {
307             handles = 'all';
308         }
309         if (resizeType === 'horizontal') {
310             handles = 'e, w';
311         }
312         $(codemirrorEditor.getWrapperElement())
313             .css('resize', resizeType)
314             .resizable({
315                 handles: handles,
316                 resize: function () {
317                     codemirrorEditor.setSize($(this).width(), $(this).height());
318                 }
319             });
320         // enable autocomplete
321         codemirrorEditor.on('inputRead', Functions.codeMirrorAutoCompleteOnInputRead);
323         // page locking
324         codemirrorEditor.on('change', function (e) {
325             e.data = {
326                 value: 3,
327                 content: codemirrorEditor.isClean(),
328             };
329             AJAX.lockPageHandler(e);
330         });
332         return codemirrorEditor;
333     }
334     return null;
338  * Clear text selection
339  */
340 Functions.clearSelection = function () {
341     if (document.selection && document.selection.empty) {
342         document.selection.empty();
343     } else if (window.getSelection) {
344         var sel = window.getSelection();
345         if (sel.empty) {
346             sel.empty();
347         }
348         if (sel.removeAllRanges) {
349             sel.removeAllRanges();
350         }
351     }
355  * Create a jQuery UI tooltip
357  * @param $elements     jQuery object representing the elements
358  * @param item          the item
359  *                      (see https://api.jqueryui.com/tooltip/#option-items)
360  * @param myContent     content of the tooltip
361  * @param additionalOptions to override the default options
363  */
364 Functions.tooltip = function ($elements, item, myContent, additionalOptions) {
365     if ($('#no_hint').length > 0) {
366         return;
367     }
369     var defaultOptions = {
370         content: myContent,
371         items:  item,
372         tooltipClass: 'tooltip',
373         track: true,
374         show: false,
375         hide: false
376     };
378     $elements.tooltip($.extend(true, defaultOptions, additionalOptions));
382  * HTML escaping
383  */
384 Functions.escapeHtml = function (unsafe) {
385     if (typeof(unsafe) !== 'undefined') {
386         return unsafe
387             .toString()
388             .replace(/&/g, '&amp;')
389             .replace(/</g, '&lt;')
390             .replace(/>/g, '&gt;')
391             .replace(/"/g, '&quot;')
392             .replace(/'/g, '&#039;');
393     } else {
394         return false;
395     }
398 Functions.escapeJsString = function (unsafe) {
399     if (typeof(unsafe) !== 'undefined') {
400         return unsafe
401             .toString()
402             .replace('\x00', '')
403             .replace('\\', '\\\\')
404             .replace('\'', '\\\'')
405             .replace('&#039;', '\\&#039;')
406             .replace('"', '\\"')
407             .replace('&quot;', '\\&quot;')
408             .replace('\n', '\n')
409             .replace('\r', '\r')
410             .replace(/<\/script/gi, '</\' + \'script');
411     } else {
412         return false;
413     }
416 Functions.escapeBacktick = function (s) {
417     return s.replace('`', '``');
420 Functions.escapeSingleQuote = function (s) {
421     return s.replace('\\', '\\\\').replace('\'', '\\\'');
424 Functions.sprintf = function () {
425     return sprintf.apply(this, arguments);
429  * Hides/shows the default value input field, depending on the default type
430  * Ticks the NULL checkbox if NULL is chosen as default value.
431  */
432 Functions.hideShowDefaultValue = function ($defaultType) {
433     if ($defaultType.val() === 'USER_DEFINED') {
434         $defaultType.siblings('.default_value').show().focus();
435     } else {
436         $defaultType.siblings('.default_value').hide();
437         if ($defaultType.val() === 'NULL') {
438             var $nullCheckbox = $defaultType.closest('tr').find('.allow_null');
439             $nullCheckbox.prop('checked', true);
440         }
441     }
445  * Hides/shows the input field for column expression based on whether
446  * VIRTUAL/PERSISTENT is selected
448  * @param $virtuality virtuality dropdown
449  */
450 Functions.hideShowExpression = function ($virtuality) {
451     if ($virtuality.val() === '') {
452         $virtuality.siblings('.expression').hide();
453     } else {
454         $virtuality.siblings('.expression').show();
455     }
459  * Show notices for ENUM columns; add/hide the default value
461  */
462 Functions.verifyColumnsProperties = function () {
463     $('select.column_type').each(function () {
464         Functions.showNoticeForEnum($(this));
465     });
466     $('select.default_type').each(function () {
467         Functions.hideShowDefaultValue($(this));
468     });
469     $('select.virtuality').each(function () {
470         Functions.hideShowExpression($(this));
471     });
475  * Add a hidden field to the form to indicate that this will be an
476  * Ajax request (only if this hidden field does not exist)
478  * @param $form object   the form
479  */
480 Functions.prepareForAjaxRequest = function ($form) {
481     if (! $form.find('input:hidden').is('#ajax_request_hidden')) {
482         $form.append('<input type="hidden" id="ajax_request_hidden" name="ajax_request" value="true">');
483     }
487  * Generate a new password and copy it to the password input areas
489  * @param {object} passwordForm the form that holds the password fields
491  * @return {boolean} always true
492  */
493 Functions.suggestPassword = function (passwordForm) {
494     // restrict the password to just letters and numbers to avoid problems:
495     // "editors and viewers regard the password as multiple words and
496     // things like double click no longer work"
497     var pwchars = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWYXZ';
498     var passwordlength = 16;    // do we want that to be dynamic?  no, keep it simple :)
499     var passwd = passwordForm.generated_pw;
500     var randomWords = new Int32Array(passwordlength);
502     passwd.value = '';
504     var i;
506     // First we're going to try to use a built-in CSPRNG
507     if (window.crypto && window.crypto.getRandomValues) {
508         window.crypto.getRandomValues(randomWords);
509     } else if (window.msCrypto && window.msCrypto.getRandomValues) {
510         // Because of course IE calls it msCrypto instead of being standard
511         window.msCrypto.getRandomValues(randomWords);
512     } else {
513         // Fallback to Math.random
514         for (i = 0; i < passwordlength; i++) {
515             randomWords[i] = Math.floor(Math.random() * pwchars.length);
516         }
517     }
519     for (i = 0; i < passwordlength; i++) {
520         passwd.value += pwchars.charAt(Math.abs(randomWords[i]) % pwchars.length);
521     }
523     var $jQueryPasswordForm = $(passwordForm);
525     passwordForm.elements.pma_pw.value = passwd.value;
526     passwordForm.elements.pma_pw2.value = passwd.value;
527     var meterObj = $jQueryPasswordForm.find('meter[name="pw_meter"]').first();
528     var meterObjLabel = $jQueryPasswordForm.find('span[name="pw_strength"]').first();
529     checkPasswordStrength(passwd.value, meterObj, meterObjLabel);
530     return true;
534  * Version string to integer conversion.
535  */
536 Functions.parseVersionString = function (str) {
537     if (typeof(str) !== 'string') {
538         return false;
539     }
540     var add = 0;
541     // Parse possible alpha/beta/rc/
542     var state = str.split('-');
543     if (state.length >= 2) {
544         if (state[1].substr(0, 2) === 'rc') {
545             add = - 20 - parseInt(state[1].substr(2), 10);
546         } else if (state[1].substr(0, 4) === 'beta') {
547             add =  - 40 - parseInt(state[1].substr(4), 10);
548         } else if (state[1].substr(0, 5) === 'alpha') {
549             add =  - 60 - parseInt(state[1].substr(5), 10);
550         } else if (state[1].substr(0, 3) === 'dev') {
551             /* We don't handle dev, it's git snapshot */
552             add = 0;
553         }
554     }
555     // Parse version
556     var x = str.split('.');
557     // Use 0 for non existing parts
558     var maj = parseInt(x[0], 10) || 0;
559     var min = parseInt(x[1], 10) || 0;
560     var pat = parseInt(x[2], 10) || 0;
561     var hotfix = parseInt(x[3], 10) || 0;
562     return  maj * 100000000 + min * 1000000 + pat * 10000 + hotfix * 100 + add;
566  * Indicates current available version on main page.
567  */
568 Functions.currentVersion = function (data) {
569     if (data && data.version && data.date) {
570         var current = Functions.parseVersionString($('span.version').text());
571         var latest = Functions.parseVersionString(data.version);
572         var url = 'https://www.phpmyadmin.net/files/' + Functions.escapeHtml(encodeURIComponent(data.version)) + '/';
573         var versionInformationMessage = document.createElement('span');
574         versionInformationMessage.className = 'latest';
575         var versionInformationMessageLink = document.createElement('a');
576         versionInformationMessageLink.href = url;
577         versionInformationMessageLink.className = 'disableAjax';
578         var versionInformationMessageLinkText = document.createTextNode(data.version);
579         versionInformationMessageLink.appendChild(versionInformationMessageLinkText);
580         var prefixMessage = document.createTextNode(Messages.strLatestAvailable + ' ');
581         versionInformationMessage.appendChild(prefixMessage);
582         versionInformationMessage.appendChild(versionInformationMessageLink);
583         if (latest > current) {
584             var message = Functions.sprintf(
585                 Messages.strNewerVersion,
586                 Functions.escapeHtml(data.version),
587                 Functions.escapeHtml(data.date)
588             );
589             var htmlClass = 'notice';
590             if (Math.floor(latest / 10000) === Math.floor(current / 10000)) {
591                 /* Security update */
592                 htmlClass = 'error';
593             }
594             $('#newer_version_notice').remove();
595             var mainContainerDiv = document.createElement('div');
596             mainContainerDiv.id = 'newer_version_notice';
597             mainContainerDiv.className = htmlClass;
598             var mainContainerDivLink = document.createElement('a');
599             mainContainerDivLink.href = url;
600             mainContainerDivLink.className = 'disableAjax';
601             var mainContainerDivLinkText = document.createTextNode(message);
602             mainContainerDivLink.appendChild(mainContainerDivLinkText);
603             mainContainerDiv.appendChild(mainContainerDivLink);
604             $('#maincontainer').append($(mainContainerDiv));
605         }
606         if (latest === current) {
607             versionInformationMessage = document.createTextNode(' (' + Messages.strUpToDate + ')');
608         }
609         /* Remove extra whitespace */
610         var versionInfo = $('#li_pma_version').contents().get(2);
611         if (typeof versionInfo !== 'undefined') {
612             versionInfo.textContent = $.trim(versionInfo.textContent);
613         }
614         var $liPmaVersion = $('#li_pma_version');
615         $liPmaVersion.find('span.latest').remove();
616         $liPmaVersion.append($(versionInformationMessage));
617     }
621  * Loads Git revision data from ajax for index.php
622  */
623 Functions.displayGitRevision = function () {
624     $('#is_git_revision').remove();
625     $('#li_pma_version_git').remove();
626     $.get(
627         'index.php',
628         {
629             'server': CommonParams.get('server'),
630             'git_revision': true,
631             'ajax_request': true,
632             'no_debug': true
633         },
634         function (data) {
635             if (typeof data !== 'undefined' && data.success === true) {
636                 $(data.message).insertAfter('#li_pma_version');
637             }
638         }
639     );
643  * for PhpMyAdmin\Display\ChangePassword
644  *     libraries/user_password.php
646  */
647 Functions.displayPasswordGenerateButton = function () {
648     var generatePwdRow = $('<tr></tr>').addClass('vmiddle');
649     $('<td></td>').html(Messages.strGeneratePassword).appendTo(generatePwdRow);
650     var pwdCell = $('<td></td>').appendTo(generatePwdRow);
651     var pwdButton = $('<input>')
652         .attr({ type: 'button', id: 'button_generate_password', value: Messages.strGenerate })
653         .addClass('btn btn-secondary button')
654         .on('click', function () {
655             Functions.suggestPassword(this.form);
656         });
657     var pwdTextbox = $('<input>')
658         .attr({ type: 'text', name: 'generated_pw', id: 'generated_pw' });
659     pwdCell.append(pwdButton).append(pwdTextbox);
661     if (document.getElementById('button_generate_password') === null) {
662         $('#tr_element_before_generate_password').parent().append(generatePwdRow);
663     }
665     var generatePwdDiv = $('<div></div>').addClass('item');
666     $('<label></label>').attr({ for: 'button_generate_password' })
667         .html(Messages.strGeneratePassword + ':')
668         .appendTo(generatePwdDiv);
669     var optionsSpan = $('<span></span>').addClass('options')
670         .appendTo(generatePwdDiv);
671     pwdButton.clone(true).appendTo(optionsSpan);
672     pwdTextbox.clone(true).appendTo(generatePwdDiv);
674     if (document.getElementById('button_generate_password') === null) {
675         $('#div_element_before_generate_password').parent().append(generatePwdDiv);
676     }
680  * selects the content of a given object, f.e. a textarea
682  * @param {object}  element  element of which the content will be selected
683  * @param {var}     lock     variable which holds the lock for this element or true, if no lock exists
684  * @param {boolean} onlyOnce boolean if true this is only done once f.e. only on first focus
685  */
686 Functions.selectContent = function (element, lock, onlyOnce) {
687     if (onlyOnce && onlyOnceElements[element.name]) {
688         return;
689     }
691     onlyOnceElements[element.name] = true;
693     if (lock) {
694         return;
695     }
697     element.select();
701  * Displays a confirmation box before submitting a "DROP/DELETE/ALTER" query.
702  * This function is called while clicking links
704  * @param theLink     object the link
705  * @param theSqlQuery object the sql query to submit
707  * @return boolean  whether to run the query or not
708  */
709 Functions.confirmLink = function (theLink, theSqlQuery) {
710     // Confirmation is not required in the configuration file
711     // or browser is Opera (crappy js implementation)
712     if (Messages.strDoYouReally === '' || typeof(window.opera) !== 'undefined') {
713         return true;
714     }
716     var isConfirmed = confirm(Functions.sprintf(Messages.strDoYouReally, theSqlQuery));
717     if (isConfirmed) {
718         if (typeof(theLink.href) !== 'undefined') {
719             theLink.href += CommonParams.get('arg_separator') + 'is_js_confirmed=1';
720         } else if (typeof(theLink.form) !== 'undefined') {
721             theLink.form.action += '?is_js_confirmed=1';
722         }
723     }
725     return isConfirmed;
729  * Confirms a "DROP/DELETE/ALTER" query before
730  * submitting it if required.
731  * This function is called by the 'Functions.checkSqlQuery()' js function.
733  * @param theForm1 object   the form
734  * @param sqlQuery1 string  the sql query string
736  * @return boolean  whether to run the query or not
738  * @see     Functions.checkSqlQuery()
739  */
740 Functions.confirmQuery = function (theForm1, sqlQuery1) {
741     // Confirmation is not required in the configuration file
742     if (Messages.strDoYouReally === '') {
743         return true;
744     }
746     // Confirms a "DROP/DELETE/ALTER/TRUNCATE" statement
747     //
748     // TODO: find a way (if possible) to use the parser-analyser
749     // for this kind of verification
750     // For now, I just added a ^ to check for the statement at
751     // beginning of expression
753     var doConfirmRegExp0 = new RegExp('^\\s*DROP\\s+(IF EXISTS\\s+)?(TABLE|PROCEDURE)\\s', 'i');
754     var doConfirmRegExp1 = new RegExp('^\\s*ALTER\\s+TABLE\\s+((`[^`]+`)|([A-Za-z0-9_$]+))\\s+DROP\\s', 'i');
755     var doConfirmRegExp2 = new RegExp('^\\s*DELETE\\s+FROM\\s', 'i');
756     var doConfirmRegExp3 = new RegExp('^\\s*TRUNCATE\\s', 'i');
757     var doConfirmRegExp4 = new RegExp('^(?=.*UPDATE\\b)^((?!WHERE).)*$', 'i');
759     if (doConfirmRegExp0.test(sqlQuery1) ||
760         doConfirmRegExp1.test(sqlQuery1) ||
761         doConfirmRegExp2.test(sqlQuery1) ||
762         doConfirmRegExp3.test(sqlQuery1) ||
763         doConfirmRegExp4.test(sqlQuery1)) {
764         var message;
765         if (sqlQuery1.length > 100) {
766             message = sqlQuery1.substr(0, 100) + '\n    ...';
767         } else {
768             message = sqlQuery1;
769         }
770         var isConfirmed = confirm(Functions.sprintf(Messages.strDoYouReally, message));
771         // statement is confirmed -> update the
772         // "is_js_confirmed" form field so the confirm test won't be
773         // run on the server side and allows to submit the form
774         if (isConfirmed) {
775             theForm1.elements.is_js_confirmed.value = 1;
776             return true;
777         } else {
778             // statement is rejected -> do not submit the form
779             window.focus();
780             return false;
781         } // end if (handle confirm box result)
782     } // end if (display confirm box)
784     return true;
788  * Displays an error message if the user submitted the sql query form with no
789  * sql query, else checks for "DROP/DELETE/ALTER" statements
791  * @param theForm object the form
793  * @return boolean  always false
795  * @see     Functions.confirmQuery()
796  */
797 Functions.checkSqlQuery = function (theForm) {
798     // get the textarea element containing the query
799     var sqlQuery;
800     if (codeMirrorEditor) {
801         codeMirrorEditor.save();
802         sqlQuery = codeMirrorEditor.getValue();
803     } else {
804         sqlQuery = theForm.elements.sql_query.value;
805     }
806     var spaceRegExp = new RegExp('\\s+');
807     if (typeof(theForm.elements.sql_file) !== 'undefined' &&
808             theForm.elements.sql_file.value.replace(spaceRegExp, '') !== '') {
809         return true;
810     }
811     if (typeof(theForm.elements.id_bookmark) !== 'undefined' &&
812             (theForm.elements.id_bookmark.value !== null || theForm.elements.id_bookmark.value !== '') &&
813             theForm.elements.id_bookmark.selectedIndex !== 0) {
814         return true;
815     }
816     var result = false;
817     // Checks for "DROP/DELETE/ALTER" statements
818     if (sqlQuery.replace(spaceRegExp, '') !== '') {
819         result = Functions.confirmQuery(theForm, sqlQuery);
820     } else {
821         alert(Messages.strFormEmpty);
822     }
824     if (codeMirrorEditor) {
825         codeMirrorEditor.focus();
826     } else if (codeMirrorInlineEditor) {
827         codeMirrorInlineEditor.focus();
828     }
829     return result;
833  * Check if a form's element is empty.
834  * An element containing only spaces is also considered empty
836  * @param {object} theForm      the form
837  * @param {string} theFieldName the name of the form field to put the focus on
839  * @return {boolean} whether the form field is empty or not
840  */
841 Functions.emptyCheckTheField = function (theForm, theFieldName) {
842     var theField = theForm.elements[theFieldName];
843     var spaceRegExp = new RegExp('\\s+');
844     return theField.value.replace(spaceRegExp, '') === '';
848  * Ensures a value submitted in a form is numeric and is in a range
850  * @param object   the form
851  * @param string   the name of the form field to check
852  * @param integer  the minimum authorized value
853  * @param integer  the maximum authorized value
855  * @return boolean  whether a valid number has been submitted or not
856  */
857 Functions.checkFormElementInRange = function (theForm, theFieldName, message, minimum, maximum) {
858     var theField         = theForm.elements[theFieldName];
859     var val              = parseInt(theField.value, 10);
860     var min = minimum;
861     var max = maximum;
863     if (typeof(min) === 'undefined') {
864         min = 0;
865     }
866     if (typeof(max) === 'undefined') {
867         max = Number.MAX_VALUE;
868     }
870     if (isNaN(val)) {
871         theField.select();
872         alert(Messages.strEnterValidNumber);
873         theField.focus();
874         return false;
875     } else if (val < min || val > max) {
876         theField.select();
877         alert(Functions.sprintf(message, val));
878         theField.focus();
879         return false;
880     } else {
881         theField.value = val;
882     }
883     return true;
886 Functions.checkTableEditForm = function (theForm, fieldsCnt) {
887     // TODO: avoid sending a message if user just wants to add a line
888     // on the form but has not completed at least one field name
890     var atLeastOneField = 0;
891     var i;
892     var elm;
893     var elm2;
894     var elm3;
895     var val;
896     var id;
898     for (i = 0; i < fieldsCnt; i++) {
899         id = '#field_' + i + '_2';
900         elm = $(id);
901         val = elm.val();
902         if (val === 'VARCHAR' || val === 'CHAR' || val === 'BIT' || val === 'VARBINARY' || val === 'BINARY') {
903             elm2 = $('#field_' + i + '_3');
904             val = parseInt(elm2.val(), 10);
905             elm3 = $('#field_' + i + '_1');
906             if (isNaN(val) && elm3.val() !== '') {
907                 elm2.select();
908                 alert(Messages.strEnterValidLength);
909                 elm2.focus();
910                 return false;
911             }
912         }
914         if (atLeastOneField === 0) {
915             id = 'field_' + i + '_1';
916             if (!Functions.emptyCheckTheField(theForm, id)) {
917                 atLeastOneField = 1;
918             }
919         }
920     }
921     if (atLeastOneField === 0) {
922         var theField = theForm.elements.field_0_1;
923         alert(Messages.strFormEmpty);
924         theField.focus();
925         return false;
926     }
928     // at least this section is under jQuery
929     var $input = $('input.textfield[name=\'table\']');
930     if ($input.val() === '') {
931         alert(Messages.strFormEmpty);
932         $input.focus();
933         return false;
934     }
936     return true;
940  * True if last click is to check a row.
941  */
942 var lastClickChecked = false;
945  * Zero-based index of last clicked row.
946  * Used to handle the shift + click event in the code above.
947  */
948 var lastClickedRow = -1;
951  * Zero-based index of last shift clicked row.
952  */
953 var lastShiftClickedRow = -1;
955 var idleSecondsCounter = 0;
956 var incInterval;
957 var updateTimeout;
958 AJAX.registerTeardown('functions.js', function () {
959     clearTimeout(updateTimeout);
960     clearInterval(incInterval);
961     $(document).off('mousemove');
964 AJAX.registerOnload('functions.js', function () {
965     document.onclick = function () {
966         idleSecondsCounter = 0;
967     };
968     $(document).on('mousemove',function () {
969         idleSecondsCounter = 0;
970     });
971     document.onkeypress = function () {
972         idleSecondsCounter = 0;
973     };
974     function guid () {
975         function s4 () {
976             return Math.floor((1 + Math.random()) * 0x10000)
977                 .toString(16)
978                 .substring(1);
979         }
980         return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
981             s4() + '-' + s4() + s4() + s4();
982     }
984     function SetIdleTime () {
985         idleSecondsCounter++;
986     }
987     function UpdateIdleTime () {
988         var href = 'index.php';
989         var guid = 'default';
990         if (isStorageSupported('sessionStorage')) {
991             guid = window.sessionStorage.guid;
992         }
993         var params = {
994             'ajax_request' : true,
995             'server' : CommonParams.get('server'),
996             'db' : CommonParams.get('db'),
997             'guid': guid,
998             'access_time': idleSecondsCounter,
999             'check_timeout': 1
1000         };
1001         $.ajax({
1002             type: 'POST',
1003             url: href,
1004             data: params,
1005             success: function (data) {
1006                 if (data.success) {
1007                     if (CommonParams.get('LoginCookieValidity') - idleSecondsCounter < 0) {
1008                         /* There is other active window, let's reset counter */
1009                         idleSecondsCounter = 0;
1010                     }
1011                     var remaining = Math.min(
1012                         /* Remaining login validity */
1013                         CommonParams.get('LoginCookieValidity') - idleSecondsCounter,
1014                         /* Remaining time till session GC */
1015                         CommonParams.get('session_gc_maxlifetime')
1016                     );
1017                     var interval = 1000;
1018                     if (remaining > 5) {
1019                         // max value for setInterval() function
1020                         interval = Math.min((remaining - 1) * 1000, Math.pow(2, 31) - 1);
1021                     }
1022                     updateTimeout = window.setTimeout(UpdateIdleTime, interval);
1023                 } else { // timeout occurred
1024                     clearInterval(incInterval);
1025                     if (isStorageSupported('sessionStorage')) {
1026                         window.sessionStorage.clear();
1027                     }
1028                     // append the login form on the page, disable all the forms which were not disabled already, close all the open jqueryui modal boxes
1029                     if (!$('#modalOverlay').length) {
1030                         $('fieldset').not(':disabled').attr('disabled', 'disabled').addClass('disabled_for_expiration');
1031                         $('body').append(data.error);
1032                         $('.ui-dialog').each(function () {
1033                             $('#' + $(this).attr('aria-describedby')).dialog('close');
1034                         });
1035                         $('#input_username').trigger('focus');
1036                     } else {
1037                         CommonParams.set('token', data.new_token);
1038                         $('input[name=token]').val(data.new_token);
1039                     }
1040                     idleSecondsCounter = 0;
1041                     Functions.handleRedirectAndReload(data);
1042                 }
1043             }
1044         });
1045     }
1046     if (CommonParams.get('logged_in')) {
1047         incInterval = window.setInterval(SetIdleTime, 1000);
1048         var sessionTimeout = Math.min(
1049             CommonParams.get('LoginCookieValidity'),
1050             CommonParams.get('session_gc_maxlifetime')
1051         );
1052         if (isStorageSupported('sessionStorage')) {
1053             window.sessionStorage.setItem('guid', guid());
1054         }
1055         var interval = (sessionTimeout - 5) * 1000;
1056         if (interval > Math.pow(2, 31) - 1) { // max value for setInterval() function
1057             interval = Math.pow(2, 31) - 1;
1058         }
1059         updateTimeout = window.setTimeout(UpdateIdleTime, interval);
1060     }
1063  * Unbind all event handlers before tearing down a page
1064  */
1065 AJAX.registerTeardown('functions.js', function () {
1066     $(document).off('click', 'input:checkbox.checkall');
1069 AJAX.registerOnload('functions.js', function () {
1070     /**
1071      * Row marking in horizontal mode (use "on" so that it works also for
1072      * next pages reached via AJAX); a tr may have the class noclick to remove
1073      * this behavior.
1074      */
1076     $(document).on('click', 'input:checkbox.checkall', function (e) {
1077         var $this = $(this);
1078         var $tr = $this.closest('tr');
1079         var $table = $this.closest('table');
1081         if (!e.shiftKey || lastClickedRow === -1) {
1082             // usual click
1084             var $checkbox = $tr.find(':checkbox.checkall');
1085             var checked = $this.prop('checked');
1086             $checkbox.prop('checked', checked).trigger('change');
1087             if (checked) {
1088                 $tr.addClass('marked');
1089             } else {
1090                 $tr.removeClass('marked');
1091             }
1092             lastClickChecked = checked;
1094             // remember the last clicked row
1095             lastClickedRow = lastClickChecked ? $table.find('tbody tr:not(.noclick)').index($tr) : -1;
1096             lastShiftClickedRow = -1;
1097         } else {
1098             // handle the shift click
1099             Functions.clearSelection();
1100             var start;
1101             var end;
1103             // clear last shift click result
1104             if (lastShiftClickedRow >= 0) {
1105                 if (lastShiftClickedRow >= lastClickedRow) {
1106                     start = lastClickedRow;
1107                     end = lastShiftClickedRow;
1108                 } else {
1109                     start = lastShiftClickedRow;
1110                     end = lastClickedRow;
1111                 }
1112                 $tr.parent().find('tr:not(.noclick)')
1113                     .slice(start, end + 1)
1114                     .removeClass('marked')
1115                     .find(':checkbox')
1116                     .prop('checked', false)
1117                     .trigger('change');
1118             }
1120             // handle new shift click
1121             var currRow = $table.find('tbody tr:not(.noclick)').index($tr);
1122             if (currRow >= lastClickedRow) {
1123                 start = lastClickedRow;
1124                 end = currRow;
1125             } else {
1126                 start = currRow;
1127                 end = lastClickedRow;
1128             }
1129             $tr.parent().find('tr:not(.noclick)')
1130                 .slice(start, end + 1)
1131                 .addClass('marked')
1132                 .find(':checkbox')
1133                 .prop('checked', true)
1134                 .trigger('change');
1136             // remember the last shift clicked row
1137             lastShiftClickedRow = currRow;
1138         }
1139     });
1141     Functions.addDateTimePicker();
1143     /**
1144      * Add attribute to text boxes for iOS devices (based on bugID: 3508912)
1145      */
1146     if (navigator.userAgent.match(/(iphone|ipod|ipad)/i)) {
1147         $('input[type=text]').attr('autocapitalize', 'off').attr('autocorrect', 'off');
1148     }
1152   * Checks/unchecks all options of a <select> element
1153   *
1154   * @param string   the form name
1155   * @param string   the element name
1156   * @param boolean  whether to check or to uncheck options
1157   *
1158   * @return boolean  always true
1159   */
1160 Functions.setSelectOptions = function (theForm, theSelect, doCheck) {
1161     $('form[name=\'' + theForm + '\'] select[name=\'' + theSelect + '\']').find('option').prop('selected', doCheck);
1162     return true;
1166  * Sets current value for query box.
1167  */
1168 Functions.setQuery = function (query) {
1169     if (codeMirrorEditor) {
1170         codeMirrorEditor.setValue(query);
1171         codeMirrorEditor.focus();
1172     } else if (document.sqlform) {
1173         document.sqlform.sql_query.value = query;
1174         document.sqlform.sql_query.focus();
1175     }
1179  * Handles 'Simulate query' button on SQL query box.
1181  * @return void
1182  */
1183 Functions.handleSimulateQueryButton = function () {
1184     var updateRegExp = new RegExp('^\\s*UPDATE\\s+((`[^`]+`)|([A-Za-z0-9_$]+))\\s+SET\\s', 'i');
1185     var deleteRegExp = new RegExp('^\\s*DELETE\\s+FROM\\s', 'i');
1186     var query = '';
1188     if (codeMirrorEditor) {
1189         query = codeMirrorEditor.getValue();
1190     } else {
1191         query = $('#sqlquery').val();
1192     }
1194     var $simulateDml = $('#simulate_dml');
1195     if (updateRegExp.test(query) || deleteRegExp.test(query)) {
1196         if (! $simulateDml.length) {
1197             $('#button_submit_query')
1198                 .before('<input type="button" id="simulate_dml"' +
1199                 'tabindex="199" class="btn btn-primary" value="' +
1200                 Messages.strSimulateDML +
1201                 '">');
1202         }
1203     } else {
1204         if ($simulateDml.length) {
1205             $simulateDml.remove();
1206         }
1207     }
1211   * Create quick sql statements.
1212   *
1213   */
1214 Functions.insertQuery = function (queryType) {
1215     if (queryType === 'clear') {
1216         Functions.setQuery('');
1217         return;
1218     } else if (queryType === 'format') {
1219         if (codeMirrorEditor) {
1220             $('#querymessage').html(Messages.strFormatting +
1221                 '&nbsp;<img class="ajaxIcon" src="' +
1222                 pmaThemeImage + 'ajax_clock_small.gif" alt="">');
1223             var href = 'db_sql_format.php';
1224             var params = {
1225                 'ajax_request': true,
1226                 'sql': codeMirrorEditor.getValue(),
1227                 'server': CommonParams.get('server')
1228             };
1229             $.ajax({
1230                 type: 'POST',
1231                 url: href,
1232                 data: params,
1233                 success: function (data) {
1234                     if (data.success) {
1235                         codeMirrorEditor.setValue(data.sql);
1236                     }
1237                     $('#querymessage').html('');
1238                 }
1239             });
1240         }
1241         return;
1242     } else if (queryType === 'saved') {
1243         if (isStorageSupported('localStorage') && typeof window.localStorage.autoSavedSql !== 'undefined') {
1244             Functions.setQuery(window.localStorage.autoSavedSql);
1245         } else if (Cookies.get('autoSavedSql')) {
1246             Functions.setQuery(Cookies.get('autoSavedSql'));
1247         } else {
1248             Functions.ajaxShowMessage(Messages.strNoAutoSavedQuery);
1249         }
1250         return;
1251     }
1253     var query = '';
1254     var myListBox = document.sqlform.dummy;
1255     var table = document.sqlform.table.value;
1257     if (myListBox.options.length > 0) {
1258         sqlBoxLocked = true;
1259         var columnsList = '';
1260         var valDis = '';
1261         var editDis = '';
1262         var NbSelect = 0;
1263         for (var i = 0; i < myListBox.options.length; i++) {
1264             NbSelect++;
1265             if (NbSelect > 1) {
1266                 columnsList += ', ';
1267                 valDis += ',';
1268                 editDis += ',';
1269             }
1270             columnsList += myListBox.options[i].value;
1271             valDis += '[value-' + NbSelect + ']';
1272             editDis += myListBox.options[i].value + '=[value-' + NbSelect + ']';
1273         }
1274         if (queryType === 'selectall') {
1275             query = 'SELECT * FROM `' + table + '` WHERE 1';
1276         } else if (queryType === 'select') {
1277             query = 'SELECT ' + columnsList + ' FROM `' + table + '` WHERE 1';
1278         } else if (queryType === 'insert') {
1279             query = 'INSERT INTO `' + table + '`(' + columnsList + ') VALUES (' + valDis + ')';
1280         } else if (queryType === 'update') {
1281             query = 'UPDATE `' + table + '` SET ' + editDis + ' WHERE 1';
1282         } else if (queryType === 'delete') {
1283             query = 'DELETE FROM `' + table + '` WHERE 0';
1284         }
1285         Functions.setQuery(query);
1286         sqlBoxLocked = false;
1287     }
1291   * Inserts multiple fields.
1292   *
1293   */
1294 Functions.insertValueQuery = function () {
1295     var myQuery = document.sqlform.sql_query;
1296     var myListBox = document.sqlform.dummy;
1298     if (myListBox.options.length > 0) {
1299         sqlBoxLocked = true;
1300         var columnsList = '';
1301         var NbSelect = 0;
1302         for (var i = 0; i < myListBox.options.length; i++) {
1303             if (myListBox.options[i].selected) {
1304                 NbSelect++;
1305                 if (NbSelect > 1) {
1306                     columnsList += ', ';
1307                 }
1308                 columnsList += myListBox.options[i].value;
1309             }
1310         }
1312         /* CodeMirror support */
1313         if (codeMirrorEditor) {
1314             codeMirrorEditor.replaceSelection(columnsList);
1315             codeMirrorEditor.focus();
1316         // IE support
1317         } else if (document.selection) {
1318             myQuery.focus();
1319             var sel = document.selection.createRange();
1320             sel.text = columnsList;
1321         // MOZILLA/NETSCAPE support
1322         } else if (document.sqlform.sql_query.selectionStart || document.sqlform.sql_query.selectionStart === '0') {
1323             var startPos = document.sqlform.sql_query.selectionStart;
1324             var endPos = document.sqlform.sql_query.selectionEnd;
1325             var SqlString = document.sqlform.sql_query.value;
1327             myQuery.value = SqlString.substring(0, startPos) + columnsList + SqlString.substring(endPos, SqlString.length);
1328             myQuery.focus();
1329         } else {
1330             myQuery.value += columnsList;
1331         }
1332         sqlBoxLocked = false;
1333     }
1337  * Updates the input fields for the parameters based on the query
1338  */
1339 Functions.updateQueryParameters = function () {
1340     if ($('#parameterized').is(':checked')) {
1341         var query = codeMirrorEditor ? codeMirrorEditor.getValue() : $('#sqlquery').val();
1343         var allParameters = query.match(/:[a-zA-Z0-9_]+/g);
1344         var parameters = [];
1345         // get unique parameters
1346         if (allParameters) {
1347             $.each(allParameters, function (i, parameter) {
1348                 if ($.inArray(parameter, parameters) === -1) {
1349                     parameters.push(parameter);
1350                 }
1351             });
1352         } else {
1353             $('#parametersDiv').text(Messages.strNoParam);
1354             return;
1355         }
1357         var $temp = $('<div></div>');
1358         $temp.append($('#parametersDiv').children());
1359         $('#parametersDiv').empty();
1361         $.each(parameters, function (i, parameter) {
1362             var paramName = parameter.substring(1);
1363             var $param = $temp.find('#paramSpan_' + paramName);
1364             if (! $param.length) {
1365                 $param = $('<span class="parameter" id="paramSpan_' + paramName + '"></span>');
1366                 $('<label for="param_' + paramName + '"></label>').text(parameter).appendTo($param);
1367                 $('<input type="text" name="parameters[' + parameter + ']" id="param_' + paramName + '">').appendTo($param);
1368             }
1369             $('#parametersDiv').append($param);
1370         });
1371     } else {
1372         $('#parametersDiv').empty();
1373     }
1377   * Refresh/resize the WYSIWYG scratchboard
1378   */
1379 Functions.refreshLayout = function () {
1380     var $elm = $('#pdflayout');
1381     var orientation = $('#orientation_opt').val();
1382     var paper = 'A4';
1383     var $paperOpt = $('#paper_opt');
1384     if ($paperOpt.length === 1) {
1385         paper = $paperOpt.val();
1386     }
1387     var posa = 'y';
1388     var posb = 'x';
1389     if (orientation === 'P') {
1390         posa = 'x';
1391         posb = 'y';
1392     }
1393     $elm.css('width', Functions.pdfPaperSize(paper, posa) + 'px');
1394     $elm.css('height', Functions.pdfPaperSize(paper, posb) + 'px');
1398  * Initializes positions of elements.
1399  */
1400 Functions.tableDragInit = function () {
1401     $('.pdflayout_table').each(function () {
1402         var $this = $(this);
1403         var number = $this.data('number');
1404         var x = $('#c_table_' + number + '_x').val();
1405         var y = $('#c_table_' + number + '_y').val();
1406         $this.css('left', x + 'px');
1407         $this.css('top', y + 'px');
1408         /* Make elements draggable */
1409         $this.draggable({
1410             containment: 'parent',
1411             drag: function (evt, ui) {
1412                 var number = $this.data('number');
1413                 $('#c_table_' + number + '_x').val(parseInt(ui.position.left, 10));
1414                 $('#c_table_' + number + '_y').val(parseInt(ui.position.top, 10));
1415             }
1416         });
1417     });
1421  * Resets drag and drop positions.
1422  */
1423 Functions.resetDrag = function () {
1424     $('.pdflayout_table').each(function () {
1425         var $this = $(this);
1426         var x = $this.data('x');
1427         var y = $this.data('y');
1428         $this.css('left', x + 'px');
1429         $this.css('top', y + 'px');
1430     });
1434  * User schema handlers.
1435  */
1436 $(function () {
1437     /* Move in scratchboard on manual change */
1438     $(document).on('change', '.position-change', function () {
1439         var $this = $(this);
1440         var $elm = $('#table_' + $this.data('number'));
1441         $elm.css($this.data('axis'), $this.val() + 'px');
1442     });
1443     /* Refresh on paper size/orientation change */
1444     $(document).on('change', '.paper-change', function () {
1445         var $elm = $('#pdflayout');
1446         if ($elm.css('visibility') === 'visible') {
1447             Functions.refreshLayout();
1448             Functions.tableDragInit();
1449         }
1450     });
1451     /* Show/hide the WYSIWYG scratchboard */
1452     $(document).on('click', '#toggle-dragdrop', function () {
1453         var $elm = $('#pdflayout');
1454         if ($elm.css('visibility') === 'hidden') {
1455             Functions.refreshLayout();
1456             Functions.tableDragInit();
1457             $elm.css('visibility', 'visible');
1458             $elm.css('display', 'block');
1459             $('#showwysiwyg').val('1');
1460         } else {
1461             $elm.css('visibility', 'hidden');
1462             $elm.css('display', 'none');
1463             $('#showwysiwyg').val('0');
1464         }
1465     });
1466     /* Reset scratchboard */
1467     $(document).on('click', '#reset-dragdrop', function () {
1468         Functions.resetDrag();
1469     });
1473  * Returns paper sizes for a given format
1474  */
1475 Functions.pdfPaperSize = function (format, axis) {
1476     switch (format.toUpperCase()) {
1477     case '4A0':
1478         if (axis === 'x') {
1479             return 4767.87;
1480         }
1481         return 6740.79;
1482     case '2A0':
1483         if (axis === 'x') {
1484             return 3370.39;
1485         }
1486         return 4767.87;
1487     case 'A0':
1488         if (axis === 'x') {
1489             return 2383.94;
1490         }
1491         return 3370.39;
1492     case 'A1':
1493         if (axis === 'x') {
1494             return 1683.78;
1495         }
1496         return 2383.94;
1497     case 'A2':
1498         if (axis === 'x') {
1499             return 1190.55;
1500         }
1501         return 1683.78;
1502     case 'A3':
1503         if (axis === 'x') {
1504             return 841.89;
1505         }
1506         return 1190.55;
1507     case 'A4':
1508         if (axis === 'x') {
1509             return 595.28;
1510         }
1511         return 841.89;
1512     case 'A5':
1513         if (axis === 'x') {
1514             return 419.53;
1515         }
1516         return 595.28;
1517     case 'A6':
1518         if (axis === 'x') {
1519             return 297.64;
1520         }
1521         return 419.53;
1522     case 'A7':
1523         if (axis === 'x') {
1524             return 209.76;
1525         }
1526         return 297.64;
1527     case 'A8':
1528         if (axis === 'x') {
1529             return 147.40;
1530         }
1531         return 209.76;
1532     case 'A9':
1533         if (axis === 'x') {
1534             return 104.88;
1535         }
1536         return 147.40;
1537     case 'A10':
1538         if (axis === 'x') {
1539             return 73.70;
1540         }
1541         return 104.88;
1542     case 'B0':
1543         if (axis === 'x') {
1544             return 2834.65;
1545         }
1546         return 4008.19;
1547     case 'B1':
1548         if (axis === 'x') {
1549             return 2004.09;
1550         }
1551         return 2834.65;
1552     case 'B2':
1553         if (axis === 'x') {
1554             return 1417.32;
1555         }
1556         return 2004.09;
1557     case 'B3':
1558         if (axis === 'x') {
1559             return 1000.63;
1560         }
1561         return 1417.32;
1562     case 'B4':
1563         if (axis === 'x') {
1564             return 708.66;
1565         }
1566         return 1000.63;
1567     case 'B5':
1568         if (axis === 'x') {
1569             return 498.90;
1570         }
1571         return 708.66;
1572     case 'B6':
1573         if (axis === 'x') {
1574             return 354.33;
1575         }
1576         return 498.90;
1577     case 'B7':
1578         if (axis === 'x') {
1579             return 249.45;
1580         }
1581         return 354.33;
1582     case 'B8':
1583         if (axis === 'x') {
1584             return 175.75;
1585         }
1586         return 249.45;
1587     case 'B9':
1588         if (axis === 'x') {
1589             return 124.72;
1590         }
1591         return 175.75;
1592     case 'B10':
1593         if (axis === 'x') {
1594             return 87.87;
1595         }
1596         return 124.72;
1597     case 'C0':
1598         if (axis === 'x') {
1599             return 2599.37;
1600         }
1601         return 3676.54;
1602     case 'C1':
1603         if (axis === 'x') {
1604             return 1836.85;
1605         }
1606         return 2599.37;
1607     case 'C2':
1608         if (axis === 'x') {
1609             return 1298.27;
1610         }
1611         return 1836.85;
1612     case 'C3':
1613         if (axis === 'x') {
1614             return 918.43;
1615         }
1616         return 1298.27;
1617     case 'C4':
1618         if (axis === 'x') {
1619             return 649.13;
1620         }
1621         return 918.43;
1622     case 'C5':
1623         if (axis === 'x') {
1624             return 459.21;
1625         }
1626         return 649.13;
1627     case 'C6':
1628         if (axis === 'x') {
1629             return 323.15;
1630         }
1631         return 459.21;
1632     case 'C7':
1633         if (axis === 'x') {
1634             return 229.61;
1635         }
1636         return 323.15;
1637     case 'C8':
1638         if (axis === 'x') {
1639             return 161.57;
1640         }
1641         return 229.61;
1642     case 'C9':
1643         if (axis === 'x') {
1644             return 113.39;
1645         }
1646         return 161.57;
1647     case 'C10':
1648         if (axis === 'x') {
1649             return 79.37;
1650         }
1651         return 113.39;
1652     case 'RA0':
1653         if (axis === 'x') {
1654             return 2437.80;
1655         }
1656         return 3458.27;
1657     case 'RA1':
1658         if (axis === 'x') {
1659             return 1729.13;
1660         }
1661         return 2437.80;
1662     case 'RA2':
1663         if (axis === 'x') {
1664             return 1218.90;
1665         }
1666         return 1729.13;
1667     case 'RA3':
1668         if (axis === 'x') {
1669             return 864.57;
1670         }
1671         return 1218.90;
1672     case 'RA4':
1673         if (axis === 'x') {
1674             return 609.45;
1675         }
1676         return 864.57;
1677     case 'SRA0':
1678         if (axis === 'x') {
1679             return 2551.18;
1680         }
1681         return 3628.35;
1682     case 'SRA1':
1683         if (axis === 'x') {
1684             return 1814.17;
1685         }
1686         return 2551.18;
1687     case 'SRA2':
1688         if (axis === 'x') {
1689             return 1275.59;
1690         }
1691         return 1814.17;
1692     case 'SRA3':
1693         if (axis === 'x') {
1694             return 907.09;
1695         }
1696         return 1275.59;
1697     case 'SRA4':
1698         if (axis === 'x') {
1699             return 637.80;
1700         }
1701         return 907.09;
1702     case 'LETTER':
1703         if (axis === 'x') {
1704             return 612.00;
1705         }
1706         return 792.00;
1707     case 'LEGAL':
1708         if (axis === 'x') {
1709             return 612.00;
1710         }
1711         return 1008.00;
1712     case 'EXECUTIVE':
1713         if (axis === 'x') {
1714             return 521.86;
1715         }
1716         return 756.00;
1717     case 'FOLIO':
1718         if (axis === 'x') {
1719             return 612.00;
1720         }
1721         return 936.00;
1722     }
1723     return 0;
1727  * Get checkbox for foreign key checks
1729  * @return string
1730  */
1731 Functions.getForeignKeyCheckboxLoader = function () {
1732     var html = '';
1733     html    += '<div>';
1734     html    += '<div class="load-default-fk-check-value">';
1735     html    += Functions.getImage('ajax_clock_small');
1736     html    += '</div>';
1737     html    += '</div>';
1738     return html;
1741 Functions.loadForeignKeyCheckbox = function () {
1742     // Load default foreign key check value
1743     var params = {
1744         'ajax_request': true,
1745         'server': CommonParams.get('server'),
1746         'get_default_fk_check_value': true
1747     };
1748     $.get('sql.php', params, function (data) {
1749         var html = '<input type="hidden" name="fk_checks" value="0">' +
1750             '<input type="checkbox" name="fk_checks" id="fk_checks"' +
1751             (data.default_fk_check_value ? ' checked="checked"' : '') + '>' +
1752             '<label for="fk_checks">' + Messages.strForeignKeyCheck + '</label>';
1753         $('.load-default-fk-check-value').replaceWith(html);
1754     });
1757 Functions.getJsConfirmCommonParam = function (elem, parameters) {
1758     var $elem = $(elem);
1759     var params = parameters;
1760     var sep = CommonParams.get('arg_separator');
1761     if (params) {
1762         // Strip possible leading ?
1763         if (params.substring(0,1) === '?') {
1764             params = params.substr(1);
1765         }
1766         params += sep;
1767     } else {
1768         params = '';
1769     }
1770     params += 'is_js_confirmed=1' + sep + 'ajax_request=true' + sep + 'fk_checks=' + ($elem.find('#fk_checks').is(':checked') ? 1 : 0);
1771     return params;
1775  * Unbind all event handlers before tearing down a page
1776  */
1777 AJAX.registerTeardown('functions.js', function () {
1778     $(document).off('click', 'a.inline_edit_sql');
1779     $(document).off('click', 'input#sql_query_edit_save');
1780     $(document).off('click', 'input#sql_query_edit_discard');
1781     $('input.sqlbutton').off('click');
1782     if (codeMirrorEditor) {
1783         codeMirrorEditor.off('blur');
1784     } else {
1785         $(document).off('blur', '#sqlquery');
1786     }
1787     $(document).off('change', '#parameterized');
1788     $(document).off('click', 'input.sqlbutton');
1789     $('#sqlquery').off('keydown');
1790     $('#sql_query_edit').off('keydown');
1792     if (codeMirrorInlineEditor) {
1793         // Copy the sql query to the text area to preserve it.
1794         $('#sql_query_edit').text(codeMirrorInlineEditor.getValue());
1795         $(codeMirrorInlineEditor.getWrapperElement()).off('keydown');
1796         codeMirrorInlineEditor.toTextArea();
1797         codeMirrorInlineEditor = false;
1798     }
1799     if (codeMirrorEditor) {
1800         $(codeMirrorEditor.getWrapperElement()).off('keydown');
1801     }
1805  * Jquery Coding for inline editing SQL_QUERY
1806  */
1807 AJAX.registerOnload('functions.js', function () {
1808     // If we are coming back to the page by clicking forward button
1809     // of the browser, bind the code mirror to inline query editor.
1810     Functions.bindCodeMirrorToInlineEditor();
1811     $(document).on('click', 'a.inline_edit_sql', function () {
1812         if ($('#sql_query_edit').length) {
1813             // An inline query editor is already open,
1814             // we don't want another copy of it
1815             return false;
1816         }
1818         var $form = $(this).prev('form');
1819         var sqlQuery  = $form.find('input[name=\'sql_query\']').val().trim();
1820         var $innerSql = $(this).parent().prev().find('code.sql');
1822         var newContent = '<textarea name="sql_query_edit" id="sql_query_edit">' + Functions.escapeHtml(sqlQuery) + '</textarea>\n';
1823         newContent    += Functions.getForeignKeyCheckboxLoader();
1824         newContent    += '<input type="submit" id="sql_query_edit_save" class="btn btn-secondary button btnSave" value="' + Messages.strGo + '">\n';
1825         newContent    += '<input type="button" id="sql_query_edit_discard" class="btn btn-secondary button btnDiscard" value="' + Messages.strCancel + '">\n';
1826         var $editorArea = $('div#inline_editor');
1827         if ($editorArea.length === 0) {
1828             $editorArea = $('<div id="inline_editor_outer"></div>');
1829             $editorArea.insertBefore($innerSql);
1830         }
1831         $editorArea.html(newContent);
1832         Functions.loadForeignKeyCheckbox();
1833         $innerSql.hide();
1835         Functions.bindCodeMirrorToInlineEditor();
1836         return false;
1837     });
1839     $(document).on('click', 'input#sql_query_edit_save', function () {
1840         // hide already existing success message
1841         var sqlQuery;
1842         if (codeMirrorInlineEditor) {
1843             codeMirrorInlineEditor.save();
1844             sqlQuery = codeMirrorInlineEditor.getValue();
1845         } else {
1846             sqlQuery = $(this).parent().find('#sql_query_edit').val();
1847         }
1848         var fkCheck = $(this).parent().find('#fk_checks').is(':checked');
1850         var $form = $('a.inline_edit_sql').prev('form');
1851         var $fakeForm = $('<form>', { action: 'import.php', method: 'post' })
1852             .append($form.find('input[name=server], input[name=db], input[name=table], input[name=token]').clone())
1853             .append($('<input>', { type: 'hidden', name: 'show_query', value: 1 }))
1854             .append($('<input>', { type: 'hidden', name: 'is_js_confirmed', value: 0 }))
1855             .append($('<input>', { type: 'hidden', name: 'sql_query', value: sqlQuery }))
1856             .append($('<input>', { type: 'hidden', name: 'fk_checks', value: fkCheck ? 1 : 0 }));
1857         if (! Functions.checkSqlQuery($fakeForm[0])) {
1858             return false;
1859         }
1860         $('.success').hide();
1861         $fakeForm.appendTo($('body')).trigger('submit');
1862     });
1864     $(document).on('click', 'input#sql_query_edit_discard', function () {
1865         var $divEditor = $('div#inline_editor_outer');
1866         $divEditor.siblings('code.sql').show();
1867         $divEditor.remove();
1868     });
1870     $(document).on('click', 'input.sqlbutton', function (evt) {
1871         Functions.insertQuery(evt.target.id);
1872         Functions.handleSimulateQueryButton();
1873         return false;
1874     });
1876     $(document).on('change', '#parameterized', Functions.updateQueryParameters);
1878     var $inputUsername = $('#input_username');
1879     if ($inputUsername) {
1880         if ($inputUsername.val() === '') {
1881             $inputUsername.trigger('focus');
1882         } else {
1883             $('#input_password').trigger('focus');
1884         }
1885     }
1889  * "inputRead" event handler for CodeMirror SQL query editors for autocompletion
1890  */
1891 Functions.codeMirrorAutoCompleteOnInputRead = function (instance) {
1892     if (!sqlAutoCompleteInProgress
1893         && (!instance.options.hintOptions.tables || !sqlAutoComplete)) {
1894         if (!sqlAutoComplete) {
1895             // Reset after teardown
1896             instance.options.hintOptions.tables = false;
1897             instance.options.hintOptions.defaultTable = '';
1899             sqlAutoCompleteInProgress = true;
1901             var href = 'db_sql_autocomplete.php';
1902             var params = {
1903                 'ajax_request': true,
1904                 'server': CommonParams.get('server'),
1905                 'db': CommonParams.get('db'),
1906                 'no_debug': true
1907             };
1909             var columnHintRender = function (elem, self, data) {
1910                 $('<div class="autocomplete-column-name">')
1911                     .text(data.columnName)
1912                     .appendTo(elem);
1913                 $('<div class="autocomplete-column-hint">')
1914                     .text(data.columnHint)
1915                     .appendTo(elem);
1916             };
1918             $.ajax({
1919                 type: 'POST',
1920                 url: href,
1921                 data: params,
1922                 success: function (data) {
1923                     if (data.success) {
1924                         var tables = JSON.parse(data.tables);
1925                         sqlAutoCompleteDefaultTable = CommonParams.get('table');
1926                         sqlAutoComplete = [];
1927                         for (var table in tables) {
1928                             if (tables.hasOwnProperty(table)) {
1929                                 var columns = tables[table];
1930                                 table = {
1931                                     text: table,
1932                                     columns: []
1933                                 };
1934                                 for (var column in columns) {
1935                                     if (columns.hasOwnProperty(column)) {
1936                                         var displayText = columns[column].Type;
1937                                         if (columns[column].Key === 'PRI') {
1938                                             displayText += ' | Primary';
1939                                         } else if (columns[column].Key === 'UNI') {
1940                                             displayText += ' | Unique';
1941                                         }
1942                                         table.columns.push({
1943                                             text: column,
1944                                             displayText: column + ' | ' +  displayText,
1945                                             columnName: column,
1946                                             columnHint: displayText,
1947                                             render: columnHintRender
1948                                         });
1949                                     }
1950                                 }
1951                             }
1952                             sqlAutoComplete.push(table);
1953                         }
1954                         instance.options.hintOptions.tables = sqlAutoComplete;
1955                         instance.options.hintOptions.defaultTable = sqlAutoCompleteDefaultTable;
1956                     }
1957                 },
1958                 complete: function () {
1959                     sqlAutoCompleteInProgress = false;
1960                 }
1961             });
1962         } else {
1963             instance.options.hintOptions.tables = sqlAutoComplete;
1964             instance.options.hintOptions.defaultTable = sqlAutoCompleteDefaultTable;
1965         }
1966     }
1967     if (instance.state.completionActive) {
1968         return;
1969     }
1970     var cur = instance.getCursor();
1971     var token = instance.getTokenAt(cur);
1972     var string = '';
1973     if (token.string.match(/^[.`\w@]\w*$/)) {
1974         string = token.string;
1975     }
1976     if (string.length > 0) {
1977         CodeMirror.commands.autocomplete(instance);
1978     }
1982  * Remove autocomplete information before tearing down a page
1983  */
1984 AJAX.registerTeardown('functions.js', function () {
1985     sqlAutoComplete = false;
1986     sqlAutoCompleteDefaultTable = '';
1990  * Binds the CodeMirror to the text area used to inline edit a query.
1991  */
1992 Functions.bindCodeMirrorToInlineEditor = function () {
1993     var $inlineEditor = $('#sql_query_edit');
1994     if ($inlineEditor.length > 0) {
1995         if (typeof CodeMirror !== 'undefined') {
1996             var height = $inlineEditor.css('height');
1997             codeMirrorInlineEditor = Functions.getSqlEditor($inlineEditor);
1998             codeMirrorInlineEditor.getWrapperElement().style.height = height;
1999             codeMirrorInlineEditor.refresh();
2000             codeMirrorInlineEditor.focus();
2001             $(codeMirrorInlineEditor.getWrapperElement())
2002                 .on('keydown', Functions.catchKeypressesFromSqlInlineEdit);
2003         } else {
2004             $inlineEditor
2005                 .trigger('focus')
2006                 .on('keydown', Functions.catchKeypressesFromSqlInlineEdit);
2007         }
2008     }
2011 Functions.catchKeypressesFromSqlInlineEdit = function (event) {
2012     // ctrl-enter is 10 in chrome and ie, but 13 in ff
2013     if ((event.ctrlKey || event.metaKey) && (event.keyCode === 13 || event.keyCode === 10)) {
2014         $('#sql_query_edit_save').trigger('click');
2015     }
2019  * Adds doc link to single highlighted SQL element
2020  */
2021 Functions.documentationAdd = function ($elm, params) {
2022     if (typeof mysqlDocTemplate === 'undefined') {
2023         return;
2024     }
2026     var url = Functions.sprintf(
2027         decodeURIComponent(mysqlDocTemplate),
2028         params[0]
2029     );
2030     if (params.length > 1) {
2031         url += '#' + params[1];
2032     }
2033     var content = $elm.text();
2034     $elm.text('');
2035     $elm.append('<a target="mysql_doc" class="cm-sql-doc" href="' + url + '">' + content + '</a>');
2039  * Generates doc links for keywords inside highlighted SQL
2040  */
2041 Functions.documentationKeyword = function (idx, elm) {
2042     var $elm = $(elm);
2043     /* Skip already processed ones */
2044     if ($elm.find('a').length > 0) {
2045         return;
2046     }
2047     var keyword = $elm.text().toUpperCase();
2048     var $next = $elm.next('.cm-keyword');
2049     if ($next) {
2050         var nextKeyword = $next.text().toUpperCase();
2051         var full = keyword + ' ' + nextKeyword;
2053         var $next2 = $next.next('.cm-keyword');
2054         if ($next2) {
2055             var next2Keyword = $next2.text().toUpperCase();
2056             var full2 = full + ' ' + next2Keyword;
2057             if (full2 in mysqlDocKeyword) {
2058                 Functions.documentationAdd($elm, mysqlDocKeyword[full2]);
2059                 Functions.documentationAdd($next, mysqlDocKeyword[full2]);
2060                 Functions.documentationAdd($next2, mysqlDocKeyword[full2]);
2061                 return;
2062             }
2063         }
2064         if (full in mysqlDocKeyword) {
2065             Functions.documentationAdd($elm, mysqlDocKeyword[full]);
2066             Functions.documentationAdd($next, mysqlDocKeyword[full]);
2067             return;
2068         }
2069     }
2070     if (keyword in mysqlDocKeyword) {
2071         Functions.documentationAdd($elm, mysqlDocKeyword[keyword]);
2072     }
2076  * Generates doc links for builtins inside highlighted SQL
2077  */
2078 Functions.documentationBuiltin = function (idx, elm) {
2079     var $elm = $(elm);
2080     var builtin = $elm.text().toUpperCase();
2081     if (builtin in mysqlDocBuiltin) {
2082         Functions.documentationAdd($elm, mysqlDocBuiltin[builtin]);
2083     }
2087  * Higlights SQL using CodeMirror.
2088  */
2089 Functions.highlightSql = function ($base) {
2090     var $elm = $base.find('code.sql');
2091     $elm.each(function () {
2092         var $sql = $(this);
2093         var $pre = $sql.find('pre');
2094         /* We only care about visible elements to avoid double processing */
2095         if ($pre.is(':visible')) {
2096             var $highlight = $('<div class="sql-highlight cm-s-default"></div>');
2097             $sql.append($highlight);
2098             if (typeof CodeMirror !== 'undefined') {
2099                 CodeMirror.runMode($sql.text(), 'text/x-mysql', $highlight[0]);
2100                 $pre.hide();
2101                 $highlight.find('.cm-keyword').each(Functions.documentationKeyword);
2102                 $highlight.find('.cm-builtin').each(Functions.documentationBuiltin);
2103             }
2104         }
2105     });
2109  * Updates an element containing code.
2111  * @param jQuery Object $base base element which contains the raw and the
2112  *                            highlighted code.
2114  * @param string htmlValue    code in HTML format, displayed if code cannot be
2115  *                            highlighted
2117  * @param string rawValue     raw code, used as a parameter for highlighter
2119  * @return bool               whether content was updated or not
2120  */
2121 Functions.updateCode = function ($base, htmlValue, rawValue) {
2122     var $code = $base.find('code');
2123     if ($code.length === 0) {
2124         return false;
2125     }
2127     // Determines the type of the content and appropriate CodeMirror mode.
2128     var type = '';
2129     var mode = '';
2130     if  ($code.hasClass('json')) {
2131         type = 'json';
2132         mode = 'application/json';
2133     } else if ($code.hasClass('sql')) {
2134         type = 'sql';
2135         mode = 'text/x-mysql';
2136     } else if ($code.hasClass('xml')) {
2137         type = 'xml';
2138         mode = 'application/xml';
2139     } else {
2140         return false;
2141     }
2143     // Element used to display unhighlighted code.
2144     var $notHighlighted = $('<pre>' + htmlValue + '</pre>');
2146     // Tries to highlight code using CodeMirror.
2147     if (typeof CodeMirror !== 'undefined') {
2148         var $highlighted = $('<div class="' + type + '-highlight cm-s-default"></div>');
2149         CodeMirror.runMode(rawValue, mode, $highlighted[0]);
2150         $notHighlighted.hide();
2151         $code.html('').append($notHighlighted, $highlighted[0]);
2152     } else {
2153         $code.html('').append($notHighlighted);
2154     }
2156     return true;
2160  * Show a message on the top of the page for an Ajax request
2162  * Sample usage:
2164  * 1) var $msg = Functions.ajaxShowMessage();
2165  * This will show a message that reads "Loading...". Such a message will not
2166  * disappear automatically and cannot be dismissed by the user. To remove this
2167  * message either the Functions.ajaxRemoveMessage($msg) function must be called or
2168  * another message must be show with Functions.ajaxShowMessage() function.
2170  * 2) var $msg = Functions.ajaxShowMessage(Messages.strProcessingRequest);
2171  * This is a special case. The behaviour is same as above,
2172  * just with a different message
2174  * 3) var $msg = Functions.ajaxShowMessage('The operation was successful');
2175  * This will show a message that will disappear automatically and it can also
2176  * be dismissed by the user.
2178  * 4) var $msg = Functions.ajaxShowMessage('Some error', false);
2179  * This will show a message that will not disappear automatically, but it
2180  * can be dismissed by the user after he has finished reading it.
2182  * @param string  message     string containing the message to be shown.
2183  *                              optional, defaults to 'Loading...'
2184  * @param mixed   timeout     number of milliseconds for the message to be visible
2185  *                              optional, defaults to 5000. If set to 'false', the
2186  *                              notification will never disappear
2187  * @param string  type        string to dictate the type of message shown.
2188  *                              optional, defaults to normal notification.
2189  *                              If set to 'error', the notification will show message
2190  *                              with red background.
2191  *                              If set to 'success', the notification will show with
2192  *                              a green background.
2193  * @return jQuery object       jQuery Element that holds the message div
2194  *                              this object can be passed to Functions.ajaxRemoveMessage()
2195  *                              to remove the notification
2196  */
2197 Functions.ajaxShowMessage = function (message, timeout, type) {
2198     var msg = message;
2199     var newTimeOut = timeout;
2200     /**
2201      * @var self_closing Whether the notification will automatically disappear
2202      */
2203     var selfClosing = true;
2204     /**
2205      * @var dismissable Whether the user will be able to remove
2206      *                  the notification by clicking on it
2207      */
2208     var dismissable = true;
2209     // Handle the case when a empty data.message is passed.
2210     // We don't want the empty message
2211     if (msg === '') {
2212         return true;
2213     } else if (! msg) {
2214         // If the message is undefined, show the default
2215         msg = Messages.strLoading;
2216         dismissable = false;
2217         selfClosing = false;
2218     } else if (msg === Messages.strProcessingRequest) {
2219         // This is another case where the message should not disappear
2220         dismissable = false;
2221         selfClosing = false;
2222     }
2223     // Figure out whether (or after how long) to remove the notification
2224     if (newTimeOut === undefined) {
2225         newTimeOut = 5000;
2226     } else if (newTimeOut === false) {
2227         selfClosing = false;
2228     }
2229     // Determine type of message, add styling as required
2230     if (type === 'error') {
2231         msg = '<div class="error">' + msg + '</div>';
2232     } else if (type === 'success') {
2233         msg = '<div class="success">' + msg + '</div>';
2234     }
2235     // Create a parent element for the AJAX messages, if necessary
2236     if ($('#loading_parent').length === 0) {
2237         $('<div id="loading_parent"></div>')
2238             .prependTo('#page_content');
2239     }
2240     // Update message count to create distinct message elements every time
2241     ajaxMessageCount++;
2242     // Remove all old messages, if any
2243     $('span.ajax_notification[id^=ajax_message_num]').remove();
2244     /**
2245      * @var    $retval    a jQuery object containing the reference
2246      *                    to the created AJAX message
2247      */
2248     var $retval = $(
2249         '<span class="ajax_notification" id="ajax_message_num_' +
2250             ajaxMessageCount +
2251             '"></span>'
2252     )
2253         .hide()
2254         .appendTo('#loading_parent')
2255         .html(msg)
2256         .show();
2257     // If the notification is self-closing we should create a callback to remove it
2258     if (selfClosing) {
2259         $retval
2260             .delay(newTimeOut)
2261             .fadeOut('medium', function () {
2262                 if ($(this).is(':data(tooltip)')) {
2263                     $(this).tooltip('destroy');
2264                 }
2265                 // Remove the notification
2266                 $(this).remove();
2267             });
2268     }
2269     // If the notification is dismissable we need to add the relevant class to it
2270     // and add a tooltip so that the users know that it can be removed
2271     if (dismissable) {
2272         $retval.addClass('dismissable').css('cursor', 'pointer');
2273         /**
2274          * Add a tooltip to the notification to let the user know that (s)he
2275          * can dismiss the ajax notification by clicking on it.
2276          */
2277         Functions.tooltip(
2278             $retval,
2279             'span',
2280             Messages.strDismiss
2281         );
2282     }
2283     // Hide spinner if this is not a loading message
2284     if (msg !== Messages.strLoading) {
2285         $retval.css('background-image', 'none');
2286     }
2287     Functions.highlightSql($retval);
2289     return $retval;
2293  * Removes the message shown for an Ajax operation when it's completed
2295  * @param jQuery object   jQuery Element that holds the notification
2297  * @return nothing
2298  */
2299 Functions.ajaxRemoveMessage = function ($thisMessageBox) {
2300     if ($thisMessageBox !== undefined && $thisMessageBox instanceof jQuery) {
2301         $thisMessageBox
2302             .stop(true, true)
2303             .fadeOut('medium');
2304         if ($thisMessageBox.is(':data(tooltip)')) {
2305             $thisMessageBox.tooltip('destroy');
2306         } else {
2307             $thisMessageBox.remove();
2308         }
2309     }
2313  * Requests SQL for previewing before executing.
2315  * @param jQuery Object $form Form containing query data
2317  * @return void
2318  */
2319 Functions.previewSql = function ($form) {
2320     var formUrl = $form.attr('action');
2321     var sep = CommonParams.get('arg_separator');
2322     var formData = $form.serialize() +
2323         sep + 'do_save_data=1' +
2324         sep + 'preview_sql=1' +
2325         sep + 'ajax_request=1';
2326     var $messageBox = Functions.ajaxShowMessage();
2327     $.ajax({
2328         type: 'POST',
2329         url: formUrl,
2330         data: formData,
2331         success: function (response) {
2332             Functions.ajaxRemoveMessage($messageBox);
2333             if (response.success) {
2334                 var $dialogContent = $('<div></div>')
2335                     .append(response.sql_data);
2336                 var buttonOptions = {};
2337                 buttonOptions[Messages.strClose] = function () {
2338                     $(this).dialog('close');
2339                 };
2340                 $dialogContent.dialog({
2341                     minWidth: 550,
2342                     maxHeight: 400,
2343                     modal: true,
2344                     buttons: buttonOptions,
2345                     title: Messages.strPreviewSQL,
2346                     close: function () {
2347                         $(this).remove();
2348                     },
2349                     open: function () {
2350                         // Pretty SQL printing.
2351                         Functions.highlightSql($(this));
2352                     }
2353                 });
2354             } else {
2355                 Functions.ajaxShowMessage(response.message);
2356             }
2357         },
2358         error: function () {
2359             Functions.ajaxShowMessage(Messages.strErrorProcessingRequest);
2360         }
2361     });
2365  * Callback called when submit/"OK" is clicked on sql preview/confirm modal
2367  * @callback onSubmitCallback
2368  * @param {string} url The url
2369  */
2373  * @param {string}           sqlData  Sql query to preview
2374  * @param {string}           url       Url to be sent to callback
2375  * @param {onSubmitCallback} callback  On submit callback function
2377  * @return void
2378  */
2379 Functions.confirmPreviewSql = function (sqlData, url, callback) {
2380     var $dialogContent = $('<div class="preview_sql"><code class="sql"><pre>'
2381         + sqlData
2382         + '</pre></code></div>'
2383     );
2384     var buttonOptions = [
2385         {
2386             text: Messages.strOK,
2387             class: 'submitOK',
2388             click: function () {
2389                 callback(url);
2390             }
2391         },
2392         {
2393             text: Messages.strCancel,
2394             class: 'submitCancel',
2395             click: function () {
2396                 $(this).dialog('close');
2397             }
2398         }
2399     ];
2400     $dialogContent.dialog({
2401         minWidth: 550,
2402         maxHeight: 400,
2403         modal: true,
2404         buttons: buttonOptions,
2405         title: Messages.strPreviewSQL,
2406         close: function () {
2407             $(this).remove();
2408         },
2409         open: function () {
2410             // Pretty SQL printing.
2411             Functions.highlightSql($(this));
2412         }
2413     });
2417  * check for reserved keyword column name
2419  * @param jQuery Object $form Form
2421  * @returns true|false
2422  */
2423 Functions.checkReservedWordColumns = function ($form) {
2424     var isConfirmed = true;
2425     $.ajax({
2426         type: 'POST',
2427         url: 'tbl_structure.php',
2428         data: $form.serialize() + CommonParams.get('arg_separator') + 'reserved_word_check=1',
2429         success: function (data) {
2430             if (typeof data.success !== 'undefined' && data.success === true) {
2431                 isConfirmed = confirm(data.message);
2432             }
2433         },
2434         async:false
2435     });
2436     return isConfirmed;
2439 // This event only need to be fired once after the initial page load
2440 $(function () {
2441     /**
2442      * Allows the user to dismiss a notification
2443      * created with Functions.ajaxShowMessage()
2444      */
2445     $(document).on('click', 'span.ajax_notification.dismissable', function () {
2446         Functions.ajaxRemoveMessage($(this));
2447     });
2448     /**
2449      * The below two functions hide the "Dismiss notification" tooltip when a user
2450      * is hovering a link or button that is inside an ajax message
2451      */
2452     $(document).on('mouseover', 'span.ajax_notification a, span.ajax_notification button, span.ajax_notification input', function () {
2453         if ($(this).parents('span.ajax_notification').is(':data(tooltip)')) {
2454             $(this).parents('span.ajax_notification').tooltip('disable');
2455         }
2456     });
2457     $(document).on('mouseout', 'span.ajax_notification a, span.ajax_notification button, span.ajax_notification input', function () {
2458         if ($(this).parents('span.ajax_notification').is(':data(tooltip)')) {
2459             $(this).parents('span.ajax_notification').tooltip('enable');
2460         }
2461     });
2463     /**
2464      * Copy text to clipboard
2465      *
2466      * @param text to copy to clipboard
2467      *
2468      * @returns bool true|false
2469      */
2470     function copyToClipboard (text) {
2471         var $temp = $('<input>');
2472         $temp.css({ 'position': 'fixed', 'width': '2em', 'border': 0, 'top': 0, 'left': 0, 'padding': 0, 'background': 'transparent' });
2473         $('body').append($temp);
2474         $temp.val(text).trigger('select');
2475         try {
2476             var res = document.execCommand('copy');
2477             $temp.remove();
2478             return res;
2479         } catch (e) {
2480             $temp.remove();
2481             return false;
2482         }
2483     }
2485     $(document).on('click', 'a.copyQueryBtn', function (event) {
2486         event.preventDefault();
2487         var res = copyToClipboard($(this).attr('data-text'));
2488         if (res) {
2489             $(this).after('<span id=\'copyStatus\'> (' + Messages.strCopyQueryButtonSuccess + ')</span>');
2490         } else {
2491             $(this).after('<span id=\'copyStatus\'> (' + Messages.strCopyQueryButtonFailure + ')</span>');
2492         }
2493         setTimeout(function () {
2494             $('#copyStatus').remove();
2495         }, 2000);
2496     });
2500  * Hides/shows the "Open in ENUM/SET editor" message, depending on the data type of the column currently selected
2501  */
2502 Functions.showNoticeForEnum = function (selectElement) {
2503     var enumNoticeId = selectElement.attr('id').split('_')[1];
2504     enumNoticeId += '_' + (parseInt(selectElement.attr('id').split('_')[2], 10) + 1);
2505     var selectedType = selectElement.val();
2506     if (selectedType === 'ENUM' || selectedType === 'SET') {
2507         $('p#enum_notice_' + enumNoticeId).show();
2508     } else {
2509         $('p#enum_notice_' + enumNoticeId).hide();
2510     }
2514  * Creates a Profiling Chart. Used in sql.js
2515  * and in server/status/monitor.js
2516  */
2517 Functions.createProfilingChart = function (target, data) {
2518     // create the chart
2519     var factory = new JQPlotChartFactory();
2520     var chart = factory.createChart(ChartType.PIE, target);
2522     // create the data table and add columns
2523     var dataTable = new DataTable();
2524     dataTable.addColumn(ColumnType.STRING, '');
2525     dataTable.addColumn(ColumnType.NUMBER, '');
2526     dataTable.setData(data);
2528     var windowWidth = $(window).width();
2529     var location = 's';
2530     if (windowWidth > 768) {
2531         location = 'se';
2532     }
2534     // draw the chart and return the chart object
2535     chart.draw(dataTable, {
2536         seriesDefaults: {
2537             rendererOptions: {
2538                 showDataLabels:  true
2539             }
2540         },
2541         highlighter: {
2542             tooltipLocation: 'se',
2543             sizeAdjust: 0,
2544             tooltipAxes: 'pieref',
2545             formatString: '%s, %.9Ps'
2546         },
2547         legend: {
2548             show: true,
2549             location: location,
2550             rendererOptions: {
2551                 numberColumns: 2
2552             }
2553         },
2554         // from http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines#Color_Palette
2555         seriesColors: [
2556             '#fce94f',
2557             '#fcaf3e',
2558             '#e9b96e',
2559             '#8ae234',
2560             '#729fcf',
2561             '#ad7fa8',
2562             '#ef2929',
2563             '#888a85',
2564             '#c4a000',
2565             '#ce5c00',
2566             '#8f5902',
2567             '#4e9a06',
2568             '#204a87',
2569             '#5c3566',
2570             '#a40000',
2571             '#babdb6',
2572             '#2e3436'
2573         ]
2574     });
2575     return chart;
2579  * Formats a profiling duration nicely (in us and ms time).
2580  * Used in server/status/monitor.js
2582  * @param  integer    Number to be formatted, should be in the range of microsecond to second
2583  * @param  integer    Accuracy, how many numbers right to the comma should be
2584  * @return string     The formatted number
2585  */
2586 Functions.prettyProfilingNum = function (number, accuracy) {
2587     var num = number;
2588     var acc = accuracy;
2589     if (!acc) {
2590         acc = 2;
2591     }
2592     acc = Math.pow(10, acc);
2593     if (num * 1000 < 0.1) {
2594         num = Math.round(acc * (num * 1000 * 1000)) / acc + 'µ';
2595     } else if (num < 0.1) {
2596         num = Math.round(acc * (num * 1000)) / acc + 'm';
2597     } else {
2598         num = Math.round(acc * num) / acc;
2599     }
2601     return num + 's';
2605  * Formats a SQL Query nicely with newlines and indentation. Depends on Codemirror and MySQL Mode!
2607  * @param string      Query to be formatted
2608  * @return string      The formatted query
2609  */
2610 Functions.sqlPrettyPrint = function (string) {
2611     if (typeof CodeMirror === 'undefined') {
2612         return string;
2613     }
2615     var mode = CodeMirror.getMode({}, 'text/x-mysql');
2616     var stream = new CodeMirror.StringStream(string);
2617     var state = mode.startState();
2618     var token;
2619     var tokens = [];
2620     var output = '';
2621     var tabs = function (cnt) {
2622         var ret = '';
2623         for (var i = 0; i < 4 * cnt; i++) {
2624             ret += ' ';
2625         }
2626         return ret;
2627     };
2629     // "root-level" statements
2630     var statements = {
2631         'select': ['select', 'from', 'on', 'where', 'having', 'limit', 'order by', 'group by'],
2632         'update': ['update', 'set', 'where'],
2633         'insert into': ['insert into', 'values']
2634     };
2635     // don't put spaces before these tokens
2636     var spaceExceptionsBefore = { ';': true, ',': true, '.': true, '(': true };
2637     // don't put spaces after these tokens
2638     var spaceExceptionsAfter = { '.': true };
2640     // Populate tokens array
2641     while (! stream.eol()) {
2642         stream.start = stream.pos;
2643         token = mode.token(stream, state);
2644         if (token !== null) {
2645             tokens.push([token, stream.current().toLowerCase()]);
2646         }
2647     }
2649     var currentStatement = tokens[0][1];
2651     if (! statements[currentStatement]) {
2652         return string;
2653     }
2654     // Holds all currently opened code blocks (statement, function or generic)
2655     var blockStack = [];
2656     // If a new code block is found, newBlock contains its type for one iteration and vice versa for endBlock
2657     var newBlock;
2658     var endBlock;
2659     // How much to indent in the current line
2660     var indentLevel = 0;
2661     // Holds the "root-level" statements
2662     var statementPart;
2663     var lastStatementPart = statements[currentStatement][0];
2665     blockStack.unshift('statement');
2667     // Iterate through every token and format accordingly
2668     for (var i = 0; i < tokens.length; i++) {
2669         // New block => push to stack
2670         if (tokens[i][1] === '(') {
2671             if (i < tokens.length - 1 && tokens[i + 1][0] === 'statement-verb') {
2672                 blockStack.unshift(newBlock = 'statement');
2673             } else if (i > 0 && tokens[i - 1][0] === 'builtin') {
2674                 blockStack.unshift(newBlock = 'function');
2675             } else {
2676                 blockStack.unshift(newBlock = 'generic');
2677             }
2678         } else {
2679             newBlock = null;
2680         }
2682         // Block end => pop from stack
2683         if (tokens[i][1] === ')') {
2684             endBlock = blockStack[0];
2685             blockStack.shift();
2686         } else {
2687             endBlock = null;
2688         }
2690         // A subquery is starting
2691         if (i > 0 && newBlock === 'statement') {
2692             indentLevel++;
2693             output += '\n' + tabs(indentLevel) + tokens[i][1] + ' ' + tokens[i + 1][1].toUpperCase() + '\n' + tabs(indentLevel + 1);
2694             currentStatement = tokens[i + 1][1];
2695             i++;
2696             continue;
2697         }
2699         // A subquery is ending
2700         if (endBlock === 'statement' && indentLevel > 0) {
2701             output += '\n' + tabs(indentLevel);
2702             indentLevel--;
2703         }
2705         // One less indentation for statement parts (from, where, order by, etc.) and a newline
2706         statementPart = statements[currentStatement].indexOf(tokens[i][1]);
2707         if (statementPart !== -1) {
2708             if (i > 0) {
2709                 output += '\n';
2710             }
2711             output += tabs(indentLevel) + tokens[i][1].toUpperCase();
2712             output += '\n' + tabs(indentLevel + 1);
2713             lastStatementPart = tokens[i][1];
2714         // Normal indentation and spaces for everything else
2715         } else {
2716             if (! spaceExceptionsBefore[tokens[i][1]] &&
2717                ! (i > 0 && spaceExceptionsAfter[tokens[i - 1][1]]) &&
2718                output.charAt(output.length - 1) !== ' ') {
2719                 output += ' ';
2720             }
2721             if (tokens[i][0] === 'keyword') {
2722                 output += tokens[i][1].toUpperCase();
2723             } else {
2724                 output += tokens[i][1];
2725             }
2726         }
2728         // split columns in select and 'update set' clauses, but only inside statements blocks
2729         if ((lastStatementPart === 'select' || lastStatementPart === 'where'  || lastStatementPart === 'set') &&
2730             tokens[i][1] === ',' && blockStack[0] === 'statement') {
2731             output += '\n' + tabs(indentLevel + 1);
2732         }
2734         // split conditions in where clauses, but only inside statements blocks
2735         if (lastStatementPart === 'where' &&
2736             (tokens[i][1] === 'and' || tokens[i][1] === 'or' || tokens[i][1] === 'xor')) {
2737             if (blockStack[0] === 'statement') {
2738                 output += '\n' + tabs(indentLevel + 1);
2739             }
2740             // Todo: Also split and or blocks in newlines & indentation++
2741             // if (blockStack[0] === 'generic')
2742             //   output += ...
2743         }
2744     }
2745     return output;
2749  * jQuery function that uses jQueryUI's dialogs to confirm with user. Does not
2750  *  return a jQuery object yet and hence cannot be chained
2752  * @param string      question
2753  * @param string      url           URL to be passed to the callbackFn to make
2754  *                                  an Ajax call to
2755  * @param function    callbackFn    callback to execute after user clicks on OK
2756  * @param function    openCallback  optional callback to run when dialog is shown
2757  */
2758 Functions.confirm = function (question, url, callbackFn, openCallback) {
2759     var confirmState = CommonParams.get('confirm');
2760     if (! confirmState) {
2761         // user does not want to confirm
2762         if (typeof callbackFn === 'function') {
2763             callbackFn.call(this, url);
2764             return true;
2765         }
2766     }
2767     if (Messages.strDoYouReally === '') {
2768         return true;
2769     }
2771     /**
2772      * @var    button_options  Object that stores the options passed to jQueryUI
2773      *                          dialog
2774      */
2775     var buttonOptions = [
2776         {
2777             text: Messages.strOK,
2778             'class': 'submitOK',
2779             click: function () {
2780                 $(this).dialog('close');
2781                 if (typeof callbackFn === 'function') {
2782                     callbackFn.call(this, url);
2783                 }
2784             }
2785         },
2786         {
2787             text: Messages.strCancel,
2788             'class': 'submitCancel',
2789             click: function () {
2790                 $(this).dialog('close');
2791             }
2792         }
2793     ];
2795     $('<div></div>', { 'id': 'confirm_dialog', 'title': Messages.strConfirm })
2796         .prepend(question)
2797         .dialog({
2798             buttons: buttonOptions,
2799             close: function () {
2800                 $(this).remove();
2801             },
2802             open: openCallback,
2803             modal: true
2804         });
2806 jQuery.fn.confirm = Functions.confirm;
2809  * jQuery function to sort a table's body after a new row has been appended to it.
2811  * @param string      text_selector   string to select the sortKey's text
2813  * @return jQuery Object for chaining purposes
2814  */
2815 Functions.sortTable = function (textSelector) {
2816     return this.each(function () {
2817         /**
2818          * @var table_body  Object referring to the table's <tbody> element
2819          */
2820         var tableBody = $(this);
2821         /**
2822          * @var rows    Object referring to the collection of rows in {@link tableBody}
2823          */
2824         var rows = $(this).find('tr').get();
2826         // get the text of the field that we will sort by
2827         $.each(rows, function (index, row) {
2828             row.sortKey = $.trim($(row).find(textSelector).text().toLowerCase());
2829         });
2831         // get the sorted order
2832         rows.sort(function (a, b) {
2833             if (a.sortKey < b.sortKey) {
2834                 return -1;
2835             }
2836             if (a.sortKey > b.sortKey) {
2837                 return 1;
2838             }
2839             return 0;
2840         });
2842         // pull out each row from the table and then append it according to it's order
2843         $.each(rows, function (index, row) {
2844             $(tableBody).append(row);
2845             row.sortKey = null;
2846         });
2847     });
2849 jQuery.fn.sortTable = Functions.sortTable;
2852  * Unbind all event handlers before tearing down a page
2853  */
2854 AJAX.registerTeardown('functions.js', function () {
2855     $(document).off('submit', '#create_table_form_minimal.ajax');
2856     $(document).off('submit', 'form.create_table_form.ajax');
2857     $(document).off('click', 'form.create_table_form.ajax input[name=submit_num_fields]');
2858     $(document).off('keyup', 'form.create_table_form.ajax input');
2859     $(document).off('change', 'input[name=partition_count],input[name=subpartition_count],select[name=partition_by]');
2863  * jQuery coding for 'Create Table'.  Used on db_operations.php,
2864  * db_structure.php and db_tracking.php (i.e., wherever
2865  * PhpMyAdmin\Display\CreateTable is used)
2867  * Attach Ajax Event handlers for Create Table
2868  */
2869 AJAX.registerOnload('functions.js', function () {
2870     /**
2871      * Attach event handler for submission of create table form (save)
2872      */
2873     $(document).on('submit', 'form.create_table_form.ajax', function (event) {
2874         event.preventDefault();
2876         /**
2877          * @var    the_form    object referring to the create table form
2878          */
2879         var $form = $(this);
2881         /*
2882          * First validate the form; if there is a problem, avoid submitting it
2883          *
2884          * Functions.checkTableEditForm() needs a pure element and not a jQuery object,
2885          * this is why we pass $form[0] as a parameter (the jQuery object
2886          * is actually an array of DOM elements)
2887          */
2889         if (Functions.checkTableEditForm($form[0], $form.find('input[name=orig_num_fields]').val())) {
2890             Functions.prepareForAjaxRequest($form);
2891             if (Functions.checkReservedWordColumns($form)) {
2892                 Functions.ajaxShowMessage(Messages.strProcessingRequest);
2893                 // User wants to submit the form
2894                 $.post($form.attr('action'), $form.serialize() + CommonParams.get('arg_separator') + 'do_save_data=1', function (data) {
2895                     if (typeof data !== 'undefined' && data.success === true) {
2896                         $('#properties_message')
2897                             .removeClass('error')
2898                             .html('');
2899                         Functions.ajaxShowMessage(data.message);
2900                         // Only if the create table dialog (distinct panel) exists
2901                         var $createTableDialog = $('#create_table_dialog');
2902                         if ($createTableDialog.length > 0) {
2903                             $createTableDialog.dialog('close').remove();
2904                         }
2905                         $('#tableslistcontainer').before(data.formatted_sql);
2907                         /**
2908                          * @var tables_table    Object referring to the <tbody> element that holds the list of tables
2909                          */
2910                         var tablesTable = $('#tablesForm').find('tbody').not('#tbl_summary_row');
2911                         // this is the first table created in this db
2912                         if (tablesTable.length === 0) {
2913                             CommonActions.refreshMain(
2914                                 CommonParams.get('opendb_url')
2915                             );
2916                         } else {
2917                             /**
2918                              * @var curr_last_row   Object referring to the last <tr> element in {@link tablesTable}
2919                              */
2920                             var currLastRow = $(tablesTable).find('tr:last');
2921                             /**
2922                              * @var curr_last_row_index_string   String containing the index of {@link currLastRow}
2923                              */
2924                             var currLastRowIndexString = $(currLastRow).find('input:checkbox').attr('id').match(/\d+/)[0];
2925                             /**
2926                              * @var curr_last_row_index Index of {@link currLastRow}
2927                              */
2928                             var currLastRowIndex = parseFloat(currLastRowIndexString);
2929                             /**
2930                              * @var new_last_row_index   Index of the new row to be appended to {@link tablesTable}
2931                              */
2932                             var newLastRowIndex = currLastRowIndex + 1;
2933                             /**
2934                              * @var new_last_row_id String containing the id of the row to be appended to {@link tablesTable}
2935                              */
2936                             var newLastRowId = 'checkbox_tbl_' + newLastRowIndex;
2938                             data.newTableString = data.newTableString.replace(/checkbox_tbl_/, newLastRowId);
2939                             // append to table
2940                             $(data.newTableString)
2941                                 .appendTo(tablesTable);
2943                             // Sort the table
2944                             $(tablesTable).sortTable('th');
2946                             // Adjust summary row
2947                             DatabaseStructure.adjustTotals();
2948                         }
2950                         // Refresh navigation as a new table has been added
2951                         Navigation.reload();
2952                         // Redirect to table structure page on creation of new table
2953                         var argsep = CommonParams.get('arg_separator');
2954                         var params12 = 'ajax_request=true' + argsep + 'ajax_page_request=true';
2955                         if (! (history && history.pushState)) {
2956                             params12 += MicroHistory.menus.getRequestParam();
2957                         }
2958                         var tableStructureUrl = 'tbl_structure.php?server=' + data.params.server +
2959                             argsep + 'db=' + data.params.db + argsep + 'token=' + data.params.token +
2960                             argsep + 'goto=db_structure.php' + argsep + 'table=' + data.params.table + '';
2961                         $.get(tableStructureUrl, params12, AJAX.responseHandler);
2962                     } else {
2963                         Functions.ajaxShowMessage(
2964                             '<div class="error">' + data.error + '</div>',
2965                             false
2966                         );
2967                     }
2968                 }); // end $.post()
2969             }
2970         }
2971     }); // end create table form (save)
2973     /**
2974      * Submits the intermediate changes in the table creation form
2975      * to refresh the UI accordingly
2976      */
2977     function submitChangesInCreateTableForm (actionParam) {
2978         /**
2979          * @var    the_form    object referring to the create table form
2980          */
2981         var $form = $('form.create_table_form.ajax');
2983         var $msgbox = Functions.ajaxShowMessage(Messages.strProcessingRequest);
2984         Functions.prepareForAjaxRequest($form);
2986         // User wants to add more fields to the table
2987         $.post($form.attr('action'), $form.serialize() + '&' + actionParam, function (data) {
2988             if (typeof data !== 'undefined' && data.success) {
2989                 var $pageContent = $('#page_content');
2990                 $pageContent.html(data.message);
2991                 Functions.highlightSql($pageContent);
2992                 Functions.verifyColumnsProperties();
2993                 Functions.hideShowConnection($('.create_table_form select[name=tbl_storage_engine]'));
2994                 Functions.ajaxRemoveMessage($msgbox);
2995             } else {
2996                 Functions.ajaxShowMessage(data.error);
2997             }
2998         }); // end $.post()
2999     }
3001     /**
3002      * Attach event handler for create table form (add fields)
3003      */
3004     $(document).on('click', 'form.create_table_form.ajax input[name=submit_num_fields]', function (event) {
3005         event.preventDefault();
3006         submitChangesInCreateTableForm('submit_num_fields=1');
3007     }); // end create table form (add fields)
3009     $(document).on('keydown', 'form.create_table_form.ajax input[name=added_fields]', function (event) {
3010         if (event.keyCode === 13) {
3011             event.preventDefault();
3012             event.stopImmediatePropagation();
3013             $(this)
3014                 .closest('form')
3015                 .find('input[name=submit_num_fields]')
3016                 .trigger('click');
3017         }
3018     });
3020     /**
3021      * Attach event handler to manage changes in number of partitions and subpartitions
3022      */
3023     $(document).on('change', 'input[name=partition_count],input[name=subpartition_count],select[name=partition_by]', function () {
3024         var $this = $(this);
3025         var $form = $this.parents('form');
3026         if ($form.is('.create_table_form.ajax')) {
3027             submitChangesInCreateTableForm('submit_partition_change=1');
3028         } else {
3029             $form.submit();
3030         }
3031     });
3033     $(document).on('change', 'input[value=AUTO_INCREMENT]', function () {
3034         if (this.checked) {
3035             var col = /\d/.exec($(this).attr('name'));
3036             col = col[0];
3037             var $selectFieldKey = $('select[name="field_key[' + col + ']"]');
3038             if ($selectFieldKey.val() === 'none_' + col) {
3039                 $selectFieldKey.val('primary_' + col).trigger('change', [false]);
3040             }
3041         }
3042     });
3043     $('body')
3044         .off('click', 'input.preview_sql')
3045         .on('click', 'input.preview_sql', function () {
3046             var $form = $(this).closest('form');
3047             Functions.previewSql($form);
3048         });
3053  * Validates the password field in a form
3055  * @see    Messages.strPasswordEmpty
3056  * @see    Messages.strPasswordNotSame
3057  * @param  object $the_form The form to be validated
3058  * @return bool
3059  */
3060 Functions.checkPassword = function ($theForm) {
3061     // Did the user select 'no password'?
3062     if ($theForm.find('#nopass_1').is(':checked')) {
3063         return true;
3064     } else {
3065         var $pred = $theForm.find('#select_pred_password');
3066         if ($pred.length && ($pred.val() === 'none' || $pred.val() === 'keep')) {
3067             return true;
3068         }
3069     }
3071     var $password = $theForm.find('input[name=pma_pw]');
3072     var $passwordRepeat = $theForm.find('input[name=pma_pw2]');
3073     var alertMessage = false;
3075     if ($password.val() === '') {
3076         alertMessage = Messages.strPasswordEmpty;
3077     } else if ($password.val() !== $passwordRepeat.val()) {
3078         alertMessage = Messages.strPasswordNotSame;
3079     }
3081     if (alertMessage) {
3082         alert(alertMessage);
3083         $password.val('');
3084         $passwordRepeat.val('');
3085         $password.trigger('focus');
3086         return false;
3087     }
3088     return true;
3092  * Attach Ajax event handlers for 'Change Password' on index.php
3093  */
3094 AJAX.registerOnload('functions.js', function () {
3095     /* Handler for hostname type */
3096     $(document).on('change', '#select_pred_hostname', function () {
3097         var hostname = $('#pma_hostname');
3098         if (this.value === 'any') {
3099             hostname.val('%');
3100         } else if (this.value === 'localhost') {
3101             hostname.val('localhost');
3102         } else if (this.value === 'thishost' && $(this).data('thishost')) {
3103             hostname.val($(this).data('thishost'));
3104         } else if (this.value === 'hosttable') {
3105             hostname.val('').prop('required', false);
3106         } else if (this.value === 'userdefined') {
3107             hostname.trigger('focus').select().prop('required', true);
3108         }
3109     });
3111     /* Handler for editing hostname */
3112     $(document).on('change', '#pma_hostname', function () {
3113         $('#select_pred_hostname').val('userdefined');
3114         $('#pma_hostname').prop('required', true);
3115     });
3117     /* Handler for username type */
3118     $(document).on('change', '#select_pred_username', function () {
3119         if (this.value === 'any') {
3120             $('#pma_username').val('').prop('required', false);
3121             $('#user_exists_warning').css('display', 'none');
3122         } else if (this.value === 'userdefined') {
3123             $('#pma_username').trigger('focus').select().prop('required', true);
3124         }
3125     });
3127     /* Handler for editing username */
3128     $(document).on('change', '#pma_username', function () {
3129         $('#select_pred_username').val('userdefined');
3130         $('#pma_username').prop('required', true);
3131     });
3133     /* Handler for password type */
3134     $(document).on('change', '#select_pred_password', function () {
3135         if (this.value === 'none') {
3136             $('#text_pma_pw2').prop('required', false).val('');
3137             $('#text_pma_pw').prop('required', false).val('');
3138         } else if (this.value === 'userdefined') {
3139             $('#text_pma_pw2').prop('required', true);
3140             $('#text_pma_pw').prop('required', true).trigger('focus').select();
3141         } else {
3142             $('#text_pma_pw2').prop('required', false);
3143             $('#text_pma_pw').prop('required', false);
3144         }
3145     });
3147     /* Handler for editing password */
3148     $(document).on('change', '#text_pma_pw,#text_pma_pw2', function () {
3149         $('#select_pred_password').val('userdefined');
3150         $('#text_pma_pw2').prop('required', true);
3151         $('#text_pma_pw').prop('required', true);
3152     });
3154     /**
3155      * Unbind all event handlers before tearing down a page
3156      */
3157     $(document).off('click', '#change_password_anchor.ajax');
3159     /**
3160      * Attach Ajax event handler on the change password anchor
3161      */
3163     $(document).on('click', '#change_password_anchor.ajax', function (event) {
3164         event.preventDefault();
3166         var $msgbox = Functions.ajaxShowMessage();
3168         /**
3169          * @var button_options  Object containing options to be passed to jQueryUI's dialog
3170          */
3171         var buttonOptions = {};
3172         buttonOptions[Messages.strGo] = function () {
3173             event.preventDefault();
3175             /**
3176              * @var $the_form    Object referring to the change password form
3177              */
3178             var $theForm = $('#change_password_form');
3180             if (! Functions.checkPassword($theForm)) {
3181                 return false;
3182             }
3184             /**
3185              * @var this_value  String containing the value of the submit button.
3186              * Need to append this for the change password form on Server Privileges
3187              * page to work
3188              */
3189             var thisValue = $(this).val();
3191             var $msgbox = Functions.ajaxShowMessage(Messages.strProcessingRequest);
3192             $theForm.append('<input type="hidden" name="ajax_request" value="true">');
3194             $.post($theForm.attr('action'), $theForm.serialize() + CommonParams.get('arg_separator') + 'change_pw=' + thisValue, function (data) {
3195                 if (typeof data === 'undefined' || data.success !== true) {
3196                     Functions.ajaxShowMessage(data.error, false);
3197                     return;
3198                 }
3200                 var $pageContent = $('#page_content');
3201                 $pageContent.prepend(data.message);
3202                 Functions.highlightSql($pageContent);
3203                 $('#change_password_dialog').hide().remove();
3204                 $('#edit_user_dialog').dialog('close').remove();
3205                 Functions.ajaxRemoveMessage($msgbox);
3206             }); // end $.post()
3207         };
3209         buttonOptions[Messages.strCancel] = function () {
3210             $(this).dialog('close');
3211         };
3212         $.get($(this).attr('href'), { 'ajax_request': true }, function (data) {
3213             if (typeof data === 'undefined' || !data.success) {
3214                 Functions.ajaxShowMessage(data.error, false);
3215                 return;
3216             }
3218             if (data.scripts) {
3219                 AJAX.scriptHandler.load(data.scripts);
3220             }
3222             $('<div id="change_password_dialog"></div>')
3223                 .dialog({
3224                     title: Messages.strChangePassword,
3225                     width: 600,
3226                     close: function () {
3227                         $(this).remove();
3228                     },
3229                     buttons: buttonOptions,
3230                     modal: true
3231                 })
3232                 .append(data.message);
3233             // for this dialog, we remove the fieldset wrapping due to double headings
3234             $('fieldset#fieldset_change_password')
3235                 .find('legend').remove().end()
3236                 .find('table.noclick').unwrap().addClass('some-margin')
3237                 .find('input#text_pma_pw').trigger('focus');
3238             $('#fieldset_change_password_footer').hide();
3239             Functions.ajaxRemoveMessage($msgbox);
3240             Functions.displayPasswordGenerateButton();
3241             $('#change_password_form').on('submit', function (e) {
3242                 e.preventDefault();
3243                 $(this)
3244                     .closest('.ui-dialog')
3245                     .find('.ui-dialog-buttonpane .ui-button')
3246                     .first()
3247                     .trigger('click');
3248             });
3249         }); // end $.get()
3250     }); // end handler for change password anchor
3251 }); // end $() for Change Password
3254  * Unbind all event handlers before tearing down a page
3255  */
3256 AJAX.registerTeardown('functions.js', function () {
3257     $(document).off('change', 'select.column_type');
3258     $(document).off('change', 'select.default_type');
3259     $(document).off('change', 'select.virtuality');
3260     $(document).off('change', 'input.allow_null');
3261     $(document).off('change', '.create_table_form select[name=tbl_storage_engine]');
3264  * Toggle the hiding/showing of the "Open in ENUM/SET editor" message when
3265  * the page loads and when the selected data type changes
3266  */
3267 AJAX.registerOnload('functions.js', function () {
3268     // is called here for normal page loads and also when opening
3269     // the Create table dialog
3270     Functions.verifyColumnsProperties();
3271     //
3272     // needs on() to work also in the Create Table dialog
3273     $(document).on('change', 'select.column_type', function () {
3274         Functions.showNoticeForEnum($(this));
3275     });
3276     $(document).on('change', 'select.default_type', function () {
3277         Functions.hideShowDefaultValue($(this));
3278     });
3279     $(document).on('change', 'select.virtuality', function () {
3280         Functions.hideShowExpression($(this));
3281     });
3282     $(document).on('change', 'input.allow_null', function () {
3283         Functions.validateDefaultValue($(this));
3284     });
3285     $(document).on('change', '.create_table_form select[name=tbl_storage_engine]', function () {
3286         Functions.hideShowConnection($(this));
3287     });
3291  * If the chosen storage engine is FEDERATED show connection field. Hide otherwise
3293  * @param $engineSelector storage engine selector
3294  */
3295 Functions.hideShowConnection = function ($engineSelector) {
3296     var $connection = $('.create_table_form input[name=connection]');
3297     var index = $connection.parent('td').index() + 1;
3298     var $labelTh = $connection.parents('tr').prev('tr').children('th:nth-child(' + index + ')');
3299     if ($engineSelector.val() !== 'FEDERATED') {
3300         $connection
3301             .prop('disabled', true)
3302             .parent('td').hide();
3303         $labelTh.hide();
3304     } else {
3305         $connection
3306             .prop('disabled', false)
3307             .parent('td').show();
3308         $labelTh.show();
3309     }
3313  * If the column does not allow NULL values, makes sure that default is not NULL
3314  */
3315 Functions.validateDefaultValue = function ($nullCheckbox) {
3316     if (! $nullCheckbox.prop('checked')) {
3317         var $default = $nullCheckbox.closest('tr').find('.default_type');
3318         if ($default.val() === 'NULL') {
3319             $default.val('NONE');
3320         }
3321     }
3325  * function to populate the input fields on picking a column from central list
3327  * @param string  input_id input id of the name field for the column to be populated
3328  * @param integer offset of the selected column in central list of columns
3329  */
3330 Functions.autoPopulate = function (inputId, offset) {
3331     var db = CommonParams.get('db');
3332     var table = CommonParams.get('table');
3333     var newInputId = inputId.substring(0, inputId.length - 1);
3334     $('#' + newInputId + '1').val(centralColumnList[db + '_' + table][offset].col_name);
3335     var colType = centralColumnList[db + '_' + table][offset].col_type.toUpperCase();
3336     $('#' + newInputId + '2').val(colType);
3337     var $input3 = $('#' + newInputId + '3');
3338     $input3.val(centralColumnList[db + '_' + table][offset].col_length);
3339     if (colType === 'ENUM' || colType === 'SET') {
3340         $input3.next().show();
3341     } else {
3342         $input3.next().hide();
3343     }
3344     var colDefault = centralColumnList[db + '_' + table][offset].col_default.toUpperCase();
3345     var $input4 = $('#' + newInputId + '4');
3346     if (colDefault !== '' && colDefault !== 'NULL' && colDefault !== 'CURRENT_TIMESTAMP' && colDefault !== 'CURRENT_TIMESTAMP()') {
3347         $input4.val('USER_DEFINED');
3348         $input4.next().next().show();
3349         $input4.next().next().val(centralColumnList[db + '_' + table][offset].col_default);
3350     } else {
3351         $input4.val(centralColumnList[db + '_' + table][offset].col_default);
3352         $input4.next().next().hide();
3353     }
3354     $('#' + newInputId + '5').val(centralColumnList[db + '_' + table][offset].col_collation);
3355     var $input6 = $('#' + newInputId + '6');
3356     $input6.val(centralColumnList[db + '_' + table][offset].col_attribute);
3357     if (centralColumnList[db + '_' + table][offset].col_extra === 'on update CURRENT_TIMESTAMP') {
3358         $input6.val(centralColumnList[db + '_' + table][offset].col_extra);
3359     }
3360     if (centralColumnList[db + '_' + table][offset].col_extra.toUpperCase() === 'AUTO_INCREMENT') {
3361         $('#' + newInputId + '9').prop('checked',true).trigger('change');
3362     } else {
3363         $('#' + newInputId + '9').prop('checked',false);
3364     }
3365     if (centralColumnList[db + '_' + table][offset].col_isNull !== '0') {
3366         $('#' + newInputId + '7').prop('checked',true);
3367     } else {
3368         $('#' + newInputId + '7').prop('checked',false);
3369     }
3373  * Unbind all event handlers before tearing down a page
3374  */
3375 AJAX.registerTeardown('functions.js', function () {
3376     $(document).off('click', 'a.open_enum_editor');
3377     $(document).off('click', 'input.add_value');
3378     $(document).off('click', '#enum_editor td.drop');
3379     $(document).off('click', 'a.central_columns_dialog');
3383  * @var $enumEditorDialog An object that points to the jQuery
3384  *                          dialog of the ENUM/SET editor
3385  */
3386 var $enumEditorDialog = null;
3389  * Opens the ENUM/SET editor and controls its functions
3390  */
3391 AJAX.registerOnload('functions.js', function () {
3392     $(document).on('click', 'a.open_enum_editor', function () {
3393         // Get the name of the column that is being edited
3394         var colname = $(this).closest('tr').find('input:first').val();
3395         var title;
3396         var i;
3397         // And use it to make up a title for the page
3398         if (colname.length < 1) {
3399             title = Messages.enum_newColumnVals;
3400         } else {
3401             title = Messages.enum_columnVals.replace(
3402                 /%s/,
3403                 '"' + Functions.escapeHtml(decodeURIComponent(colname)) + '"'
3404             );
3405         }
3406         // Get the values as a string
3407         var inputstring = $(this)
3408             .closest('td')
3409             .find('input')
3410             .val();
3411         // Escape html entities
3412         inputstring = $('<div></div>')
3413             .text(inputstring)
3414             .html();
3415         // Parse the values, escaping quotes and
3416         // slashes on the fly, into an array
3417         var values = [];
3418         var inString = false;
3419         var curr;
3420         var next;
3421         var buffer = '';
3422         for (i = 0; i < inputstring.length; i++) {
3423             curr = inputstring.charAt(i);
3424             next = i === inputstring.length ? '' : inputstring.charAt(i + 1);
3425             if (! inString && curr === '\'') {
3426                 inString = true;
3427             } else if (inString && curr === '\\' && next === '\\') {
3428                 buffer += '&#92;';
3429                 i++;
3430             } else if (inString && next === '\'' && (curr === '\'' || curr === '\\')) {
3431                 buffer += '&#39;';
3432                 i++;
3433             } else if (inString && curr === '\'') {
3434                 inString = false;
3435                 values.push(buffer);
3436                 buffer = '';
3437             } else if (inString) {
3438                 buffer += curr;
3439             }
3440         }
3441         if (buffer.length > 0) {
3442             // The leftovers in the buffer are the last value (if any)
3443             values.push(buffer);
3444         }
3445         var fields = '';
3446         // If there are no values, maybe the user is about to make a
3447         // new list so we add a few for him/her to get started with.
3448         if (values.length === 0) {
3449             values.push('', '', '', '');
3450         }
3451         // Add the parsed values to the editor
3452         var dropIcon = Functions.getImage('b_drop');
3453         for (i = 0; i < values.length; i++) {
3454             fields += '<tr><td>' +
3455                    '<input type=\'text\' value=\'' + values[i] + '\'>' +
3456                    '</td><td class=\'drop\'>' +
3457                    dropIcon +
3458                    '</td></tr>';
3459         }
3460         /**
3461          * @var dialog HTML code for the ENUM/SET dialog
3462          */
3463         var dialog = '<div id=\'enum_editor\'>' +
3464                    '<fieldset>' +
3465                     '<legend>' + title + '</legend>' +
3466                     '<p>' + Functions.getImage('s_notice') +
3467                     Messages.enum_hint + '</p>' +
3468                     '<table class=\'values\'>' + fields + '</table>' +
3469                     '</fieldset><fieldset class=\'tblFooters\'>' +
3470                     '<table class=\'add\'><tr><td>' +
3471                     '<div class=\'slider\'></div>' +
3472                     '</td><td>' +
3473                     '<form><div><input type=\'submit\' class=\'add_value btn btn-primary\' value=\'' +
3474                     Functions.sprintf(Messages.enum_addValue, 1) +
3475                     '\'></div></form>' +
3476                     '</td></tr></table>' +
3477                     '<input type=\'hidden\' value=\'' + // So we know which column's data is being edited
3478                     $(this).closest('td').find('input').attr('id') +
3479                     '\'>' +
3480                     '</fieldset>' +
3481                     '</div>';
3482         /**
3483          * @var  Defines functions to be called when the buttons in
3484          * the buttonOptions jQuery dialog bar are pressed
3485          */
3486         var buttonOptions = {};
3487         buttonOptions[Messages.strGo] = function () {
3488             // When the submit button is clicked,
3489             // put the data back into the original form
3490             var valueArray = [];
3491             $(this).find('.values input').each(function (index, elm) {
3492                 var val = elm.value.replace(/\\/g, '\\\\').replace(/'/g, '\'\'');
3493                 valueArray.push('\'' + val + '\'');
3494             });
3495             // get the Length/Values text field where this value belongs
3496             var valuesId = $(this).find('input[type=\'hidden\']').val();
3497             $('input#' + valuesId).val(valueArray.join(','));
3498             $(this).dialog('close');
3499         };
3500         buttonOptions[Messages.strClose] = function () {
3501             $(this).dialog('close');
3502         };
3503         // Show the dialog
3504         var width = parseInt(
3505             (parseInt($('html').css('font-size'), 10) / 13) * 340,
3506             10
3507         );
3508         if (! width) {
3509             width = 340;
3510         }
3511         $enumEditorDialog = $(dialog).dialog({
3512             minWidth: width,
3513             maxHeight: 450,
3514             modal: true,
3515             title: Messages.enum_editor,
3516             buttons: buttonOptions,
3517             open: function () {
3518                 // Focus the "Go" button after opening the dialog
3519                 $(this).closest('.ui-dialog').find('.ui-dialog-buttonpane button:first').trigger('focus');
3520             },
3521             close: function () {
3522                 $(this).remove();
3523             }
3524         });
3525         // slider for choosing how many fields to add
3526         $enumEditorDialog.find('.slider').slider({
3527             animate: true,
3528             range: 'min',
3529             value: 1,
3530             min: 1,
3531             max: 9,
3532             slide: function (event, ui) {
3533                 $(this).closest('table').find('input[type=submit]').val(
3534                     Functions.sprintf(Messages.enum_addValue, ui.value)
3535                 );
3536             }
3537         });
3538         // Focus the slider, otherwise it looks nearly transparent
3539         $('a.ui-slider-handle').addClass('ui-state-focus');
3540         return false;
3541     });
3543     $(document).on('click', 'a.central_columns_dialog', function () {
3544         var href = 'db_central_columns.php';
3545         var db = CommonParams.get('db');
3546         var table = CommonParams.get('table');
3547         var maxRows = $(this).data('maxrows');
3548         var pick = $(this).data('pick');
3549         if (pick !== false) {
3550             pick = true;
3551         }
3552         var params = {
3553             'ajax_request' : true,
3554             'server' : CommonParams.get('server'),
3555             'db' : CommonParams.get('db'),
3556             'cur_table' : CommonParams.get('table'),
3557             'getColumnList':true
3558         };
3559         var colid = $(this).closest('td').find('input').attr('id');
3560         var fields = '';
3561         if (! (db + '_' + table in centralColumnList)) {
3562             centralColumnList.push(db + '_' + table);
3563             $.ajax({
3564                 type: 'POST',
3565                 url: href,
3566                 data: params,
3567                 success: function (data) {
3568                     centralColumnList[db + '_' + table] = data.message;
3569                 },
3570                 async:false
3571             });
3572         }
3573         var i = 0;
3574         var listSize = centralColumnList[db + '_' + table].length;
3575         var min = (listSize <= maxRows) ? listSize : maxRows;
3576         for (i = 0; i < min; i++) {
3577             fields += '<tr><td><div><span class="font_weight_bold">' +
3578                 Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_name) +
3579                 '</span><br><span class="color_gray">' + centralColumnList[db + '_' + table][i].col_type;
3581             if (centralColumnList[db + '_' + table][i].col_attribute !== '') {
3582                 fields += '(' + Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_attribute) + ') ';
3583             }
3584             if (centralColumnList[db + '_' + table][i].col_length !== '') {
3585                 fields += '(' + Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_length) + ') ';
3586             }
3587             fields += Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_extra) + '</span>' +
3588                 '</div></td>';
3589             if (pick) {
3590                 fields += '<td><input class="btn btn-secondary pick all100" type="submit" value="' +
3591                     Messages.pickColumn + '" onclick="Functions.autoPopulate(\'' + colid + '\',' + i + ')"></td>';
3592             }
3593             fields += '</tr>';
3594         }
3595         var resultPointer = i;
3596         var searchIn = '<input type="text" class="filter_rows" placeholder="' + Messages.searchList + '">';
3597         if (fields === '') {
3598             fields = Functions.sprintf(Messages.strEmptyCentralList, '\'' + Functions.escapeHtml(db) + '\'');
3599             searchIn = '';
3600         }
3601         var seeMore = '';
3602         if (listSize > maxRows) {
3603             seeMore = '<fieldset class=\'tblFooters center font_weight_bold\'>' +
3604                 '<a href=\'#\' id=\'seeMore\'>' + Messages.seeMore + '</a></fieldset>';
3605         }
3606         var centralColumnsDialog = '<div class=\'max_height_400\'>' +
3607             '<fieldset>' +
3608             searchIn +
3609             '<table id=\'col_list\' class=\'values all100\'>' + fields + '</table>' +
3610             '</fieldset>' +
3611             seeMore +
3612             '</div>';
3614         var width = parseInt(
3615             (parseInt($('html').css('font-size'), 10) / 13) * 500,
3616             10
3617         );
3618         if (! width) {
3619             width = 500;
3620         }
3621         var buttonOptions = {};
3622         var $centralColumnsDialog = $(centralColumnsDialog).dialog({
3623             minWidth: width,
3624             maxHeight: 450,
3625             modal: true,
3626             title: Messages.pickColumnTitle,
3627             buttons: buttonOptions,
3628             open: function () {
3629                 $('#col_list').on('click', '.pick', function () {
3630                     $centralColumnsDialog.remove();
3631                 });
3632                 $('.filter_rows').on('keyup', function () {
3633                     $.uiTableFilter($('#col_list'), $(this).val());
3634                 });
3635                 $('#seeMore').on('click', function () {
3636                     fields = '';
3637                     min = (listSize <= maxRows + resultPointer) ? listSize : maxRows + resultPointer;
3638                     for (i = resultPointer; i < min; i++) {
3639                         fields += '<tr><td><div><span class="font_weight_bold">' +
3640                             centralColumnList[db + '_' + table][i].col_name +
3641                             '</span><br><span class="color_gray">' +
3642                             centralColumnList[db + '_' + table][i].col_type;
3644                         if (centralColumnList[db + '_' + table][i].col_attribute !== '') {
3645                             fields += '(' + centralColumnList[db + '_' + table][i].col_attribute + ') ';
3646                         }
3647                         if (centralColumnList[db + '_' + table][i].col_length !== '') {
3648                             fields += '(' + centralColumnList[db + '_' + table][i].col_length + ') ';
3649                         }
3650                         fields += centralColumnList[db + '_' + table][i].col_extra + '</span>' +
3651                             '</div></td>';
3652                         if (pick) {
3653                             fields += '<td><input class="btn btn-secondary pick all100" type="submit" value="' +
3654                                 Messages.pickColumn + '" onclick="Functions.autoPopulate(\'' + colid + '\',' + i + ')"></td>';
3655                         }
3656                         fields += '</tr>';
3657                     }
3658                     $('#col_list').append(fields);
3659                     resultPointer = i;
3660                     if (resultPointer === listSize) {
3661                         $('#seeMore').hide();
3662                     }
3663                     return false;
3664                 });
3665                 $(this).closest('.ui-dialog').find('.ui-dialog-buttonpane button:first').trigger('focus');
3666             },
3667             close: function () {
3668                 $('#col_list').off('click', '.pick');
3669                 $('.filter_rows').off('keyup');
3670                 $(this).remove();
3671             }
3672         });
3673         return false;
3674     });
3676     // $(document).on('click', 'a.show_central_list',function(e) {
3678     // });
3679     // When "add a new value" is clicked, append an empty text field
3680     $(document).on('click', 'input.add_value', function (e) {
3681         e.preventDefault();
3682         var numNewRows = $enumEditorDialog.find('div.slider').slider('value');
3683         while (numNewRows--) {
3684             $enumEditorDialog.find('.values')
3685                 .append(
3686                     '<tr class=\'hide\'><td>' +
3687                     '<input type=\'text\'>' +
3688                     '</td><td class=\'drop\'>' +
3689                     Functions.getImage('b_drop') +
3690                     '</td></tr>'
3691                 )
3692                 .find('tr:last')
3693                 .show('fast');
3694         }
3695     });
3697     // Removes the specified row from the enum editor
3698     $(document).on('click', '#enum_editor td.drop', function () {
3699         $(this).closest('tr').hide('fast', function () {
3700             $(this).remove();
3701         });
3702     });
3706  * Ensures indexes names are valid according to their type and, for a primary
3707  * key, lock index name to 'PRIMARY'
3708  * @param string   form_id  Variable which parses the form name as
3709  *                            the input
3710  * @return boolean  false    if there is no index form, true else
3711  */
3712 Functions.checkIndexName = function (formId) {
3713     if ($('#' + formId).length === 0) {
3714         return false;
3715     }
3717     // Gets the elements pointers
3718     var $theIdxName = $('#input_index_name');
3719     var $theIdxChoice = $('#select_index_choice');
3721     // Index is a primary key
3722     if ($theIdxChoice.find('option:selected').val() === 'PRIMARY') {
3723         $theIdxName.val('PRIMARY');
3724         $theIdxName.prop('disabled', true);
3725     } else {
3726         if ($theIdxName.val() === 'PRIMARY') {
3727             $theIdxName.val('');
3728         }
3729         $theIdxName.prop('disabled', false);
3730     }
3732     return true;
3735 AJAX.registerTeardown('functions.js', function () {
3736     $(document).off('click', '#index_frm input[type=submit]');
3738 AJAX.registerOnload('functions.js', function () {
3739     /**
3740      * Handler for adding more columns to an index in the editor
3741      */
3742     $(document).on('click', '#index_frm input[type=submit]', function (event) {
3743         event.preventDefault();
3744         var hadAddButtonHidden = $(this).closest('fieldset').find('.add_fields').hasClass('hide');
3745         if (hadAddButtonHidden === false) {
3746             var rowsToAdd = $(this)
3747                 .closest('fieldset')
3748                 .find('.slider')
3749                 .slider('value');
3751             var tempEmptyVal = function () {
3752                 $(this).val('');
3753             };
3755             var tempSetFocus = function () {
3756                 if ($(this).find('option:selected').val() === '') {
3757                     return true;
3758                 }
3759                 $(this).closest('tr').find('input').trigger('focus');
3760             };
3762             while (rowsToAdd--) {
3763                 var $indexColumns = $('#index_columns');
3764                 var $newrow = $indexColumns
3765                     .find('tbody > tr:first')
3766                     .clone()
3767                     .appendTo(
3768                         $indexColumns.find('tbody')
3769                     );
3770                 $newrow.find(':input').each(tempEmptyVal);
3771                 // focus index size input on column picked
3772                 $newrow.find('select').on('change', tempSetFocus);
3773             }
3774         }
3775     });
3778 Functions.indexEditorDialog = function (url, title, callbackSuccess, callbackFailure) {
3779     /* Remove the hidden dialogs if there are*/
3780     var $editIndexDialog = $('#edit_index_dialog');
3781     if ($editIndexDialog.length !== 0) {
3782         $editIndexDialog.remove();
3783     }
3784     var $div = $('<div id="edit_index_dialog"></div>');
3786     /**
3787      * @var button_options Object that stores the options
3788      *                     passed to jQueryUI dialog
3789      */
3790     var buttonOptions = {};
3791     buttonOptions[Messages.strGo] = function () {
3792         /**
3793          * @var    the_form    object referring to the export form
3794          */
3795         var $form = $('#index_frm');
3796         Functions.ajaxShowMessage(Messages.strProcessingRequest);
3797         Functions.prepareForAjaxRequest($form);
3798         // User wants to submit the form
3799         $.post($form.attr('action'), $form.serialize() + CommonParams.get('arg_separator') + 'do_save_data=1', function (data) {
3800             var $sqlqueryresults = $('.sqlqueryresults');
3801             if ($sqlqueryresults.length !== 0) {
3802                 $sqlqueryresults.remove();
3803             }
3804             if (typeof data !== 'undefined' && data.success === true) {
3805                 Functions.ajaxShowMessage(data.message);
3806                 Functions.highlightSql($('.result_query'));
3807                 $('.result_query .notice').remove();
3808                 /* Reload the field form*/
3809                 $('#table_index').remove();
3810                 $('<div id=\'temp_div\'><div>')
3811                     .append(data.index_table)
3812                     .find('#table_index')
3813                     .insertAfter('#index_header');
3814                 var $editIndexDialog = $('#edit_index_dialog');
3815                 if ($editIndexDialog.length > 0) {
3816                     $editIndexDialog.dialog('close');
3817                 }
3818                 $('div.no_indexes_defined').hide();
3819                 if (callbackSuccess) {
3820                     callbackSuccess();
3821                 }
3822                 Navigation.reload();
3823             } else {
3824                 var $tempDiv = $('<div id=\'temp_div\'><div>').append(data.error);
3825                 var $error;
3826                 if ($tempDiv.find('.error code').length !== 0) {
3827                     $error = $tempDiv.find('.error code').addClass('error');
3828                 } else {
3829                     $error = $tempDiv;
3830                 }
3831                 if (callbackFailure) {
3832                     callbackFailure();
3833                 }
3834                 Functions.ajaxShowMessage($error, false);
3835             }
3836         }); // end $.post()
3837     };
3838     buttonOptions[Messages.strPreviewSQL] = function () {
3839         // Function for Previewing SQL
3840         var $form = $('#index_frm');
3841         Functions.previewSql($form);
3842     };
3843     buttonOptions[Messages.strCancel] = function () {
3844         $(this).dialog('close');
3845     };
3846     var $msgbox = Functions.ajaxShowMessage();
3847     $.post('tbl_indexes.php', url, function (data) {
3848         if (typeof data !== 'undefined' && data.success === false) {
3849             // in the case of an error, show the error message returned.
3850             Functions.ajaxShowMessage(data.error, false);
3851         } else {
3852             Functions.ajaxRemoveMessage($msgbox);
3853             // Show dialog if the request was successful
3854             $div
3855                 .append(data.message)
3856                 .dialog({
3857                     title: title,
3858                     width: 'auto',
3859                     open: Functions.verifyColumnsProperties,
3860                     modal: true,
3861                     buttons: buttonOptions,
3862                     close: function () {
3863                         $(this).remove();
3864                     }
3865                 });
3866             $div.find('.tblFooters').remove();
3867             Functions.showIndexEditDialog($div);
3868         }
3869     }); // end $.get()
3872 Functions.showIndexEditDialog = function ($outer) {
3873     Indexes.checkIndexType();
3874     Functions.checkIndexName('index_frm');
3875     var $indexColumns = $('#index_columns');
3876     $indexColumns.find('td').each(function () {
3877         $(this).css('width', $(this).width() + 'px');
3878     });
3879     $indexColumns.find('tbody').sortable({
3880         axis: 'y',
3881         containment: $indexColumns.find('tbody'),
3882         tolerance: 'pointer'
3883     });
3884     Functions.showHints($outer);
3885     Functions.initSlider();
3886     // Add a slider for selecting how many columns to add to the index
3887     $outer.find('.slider').slider({
3888         animate: true,
3889         value: 1,
3890         min: 1,
3891         max: 16,
3892         slide: function (event, ui) {
3893             $(this).closest('fieldset').find('input[type=submit]').val(
3894                 Functions.sprintf(Messages.strAddToIndex, ui.value)
3895             );
3896         }
3897     });
3898     $('div.add_fields').removeClass('hide');
3899     // focus index size input on column picked
3900     $outer.find('table#index_columns select').on('change', function () {
3901         if ($(this).find('option:selected').val() === '') {
3902             return true;
3903         }
3904         $(this).closest('tr').find('input').trigger('focus');
3905     });
3906     // Focus the slider, otherwise it looks nearly transparent
3907     $('a.ui-slider-handle').addClass('ui-state-focus');
3908     // set focus on index name input, if empty
3909     var input = $outer.find('input#input_index_name');
3910     if (! input.val()) {
3911         input.trigger('focus');
3912     }
3916  * Function to display tooltips that were
3917  * generated on the PHP side by PhpMyAdmin\Util::showHint()
3919  * @param object $div a div jquery object which specifies the
3920  *                    domain for searching for tooltips. If we
3921  *                    omit this parameter the function searches
3922  *                    in the whole body
3923  **/
3924 Functions.showHints = function ($div) {
3925     var $newDiv = $div;
3926     if ($newDiv === undefined || !($newDiv instanceof jQuery) || $newDiv.length === 0) {
3927         $newDiv = $('body');
3928     }
3929     $newDiv.find('.pma_hint').each(function () {
3930         Functions.tooltip(
3931             $(this).children('img'),
3932             'img',
3933             $(this).children('span').html()
3934         );
3935     });
3938 AJAX.registerOnload('functions.js', function () {
3939     Functions.showHints();
3942 Functions.mainMenuResizerCallback = function () {
3943     // 5 px margin for jumping menu in Chrome
3944     return $(document.body).width() - 5;
3947 // This must be fired only once after the initial page load
3948 $(function () {
3949     // Initialise the menu resize plugin
3950     $('#topmenu').menuResizer(Functions.mainMenuResizerCallback);
3951     // register resize event
3952     $(window).on('resize', function () {
3953         $('#topmenu').menuResizer('resize');
3954     });
3958  * Changes status of slider
3959  */
3960 Functions.setStatusLabel = function ($element) {
3961     var text;
3962     if ($element.css('display') === 'none') {
3963         text = '+ ';
3964     } else {
3965         text = '- ';
3966     }
3967     $element.closest('.slide-wrapper').prev().find('span').text(text);
3971  * var  toggleButton  This is a function that creates a toggle
3972  *                    sliding button given a jQuery reference
3973  *                    to the correct DOM element
3974  */
3975 Functions.toggleButton = function ($obj) {
3976     // In rtl mode the toggle switch is flipped horizontally
3977     // so we need to take that into account
3978     var right;
3979     if ($('span.text_direction', $obj).text() === 'ltr') {
3980         right = 'right';
3981     } else {
3982         right = 'left';
3983     }
3984     /**
3985      *  var  h  Height of the button, used to scale the
3986      *          background image and position the layers
3987      */
3988     var h = $obj.height();
3989     $('img', $obj).height(h);
3990     $('table', $obj).css('bottom', h - 1);
3991     /**
3992      *  var  on   Width of the "ON" part of the toggle switch
3993      *  var  off  Width of the "OFF" part of the toggle switch
3994      */
3995     var on  = $('td.toggleOn', $obj).width();
3996     var off = $('td.toggleOff', $obj).width();
3997     // Make the "ON" and "OFF" parts of the switch the same size
3998     // + 2 pixels to avoid overflowed
3999     $('td.toggleOn > div', $obj).width(Math.max(on, off) + 2);
4000     $('td.toggleOff > div', $obj).width(Math.max(on, off) + 2);
4001     /**
4002      *  var  w  Width of the central part of the switch
4003      */
4004     var w = parseInt(($('img', $obj).height() / 16) * 22, 10);
4005     // Resize the central part of the switch on the top
4006     // layer to match the background
4007     $('table td:nth-child(2) > div', $obj).width(w);
4008     /**
4009      *  var  imgw    Width of the background image
4010      *  var  tblw    Width of the foreground layer
4011      *  var  offset  By how many pixels to move the background
4012      *               image, so that it matches the top layer
4013      */
4014     var imgw = $('img', $obj).width();
4015     var tblw = $('table', $obj).width();
4016     var offset = parseInt(((imgw - tblw) / 2), 10);
4017     // Move the background to match the layout of the top layer
4018     $obj.find('img').css(right, offset);
4019     /**
4020      *  var  offw    Outer width of the "ON" part of the toggle switch
4021      *  var  btnw    Outer width of the central part of the switch
4022      */
4023     var offw = $('td.toggleOff', $obj).outerWidth();
4024     var btnw = $('table td:nth-child(2)', $obj).outerWidth();
4025     // Resize the main div so that exactly one side of
4026     // the switch plus the central part fit into it.
4027     $obj.width(offw + btnw + 2);
4028     /**
4029      *  var  move  How many pixels to move the
4030      *             switch by when toggling
4031      */
4032     var move = $('td.toggleOff', $obj).outerWidth();
4033     // If the switch is initialized to the
4034     // OFF state we need to move it now.
4035     if ($('div.container', $obj).hasClass('off')) {
4036         if (right === 'right') {
4037             $('div.container', $obj).animate({ 'left': '-=' + move + 'px' }, 0);
4038         } else {
4039             $('div.container', $obj).animate({ 'left': '+=' + move + 'px' }, 0);
4040         }
4041     }
4042     // Attach an 'onclick' event to the switch
4043     $('div.container', $obj).on('click', function () {
4044         if ($(this).hasClass('isActive')) {
4045             return false;
4046         } else {
4047             $(this).addClass('isActive');
4048         }
4049         var $msg = Functions.ajaxShowMessage();
4050         var $container = $(this);
4051         var callback = $('span.callback', this).text();
4052         var operator;
4053         var url;
4054         var removeClass;
4055         var addClass;
4056         // Perform the actual toggle
4057         if ($(this).hasClass('on')) {
4058             if (right === 'right') {
4059                 operator = '-=';
4060             } else {
4061                 operator = '+=';
4062             }
4063             url = $(this).find('td.toggleOff > span').text();
4064             removeClass = 'on';
4065             addClass = 'off';
4066         } else {
4067             if (right === 'right') {
4068                 operator = '+=';
4069             } else {
4070                 operator = '-=';
4071             }
4072             url = $(this).find('td.toggleOn > span').text();
4073             removeClass = 'off';
4074             addClass = 'on';
4075         }
4077         var parts = url.split('?');
4078         $.post(parts[0], parts[1] + '&ajax_request=true', function (data) {
4079             if (typeof data !== 'undefined' && data.success === true) {
4080                 Functions.ajaxRemoveMessage($msg);
4081                 $container
4082                     .removeClass(removeClass)
4083                     .addClass(addClass)
4084                     .animate({ 'left': operator + move + 'px' }, function () {
4085                         $container.removeClass('isActive');
4086                     });
4087                 // eslint-disable-next-line no-eval
4088                 eval(callback);
4089             } else {
4090                 Functions.ajaxShowMessage(data.error, false);
4091                 $container.removeClass('isActive');
4092             }
4093         });
4094     });
4098  * Unbind all event handlers before tearing down a page
4099  */
4100 AJAX.registerTeardown('functions.js', function () {
4101     $('div.container').off('click');
4104  * Initialise all toggle buttons
4105  */
4106 AJAX.registerOnload('functions.js', function () {
4107     $('div.toggleAjax').each(function () {
4108         var $button = $(this).show();
4109         $button.find('img').each(function () {
4110             if (this.complete) {
4111                 Functions.toggleButton($button);
4112             } else {
4113                 $(this).on('load', function () {
4114                     Functions.toggleButton($button);
4115                 });
4116             }
4117         });
4118     });
4122  * Unbind all event handlers before tearing down a page
4123  */
4124 AJAX.registerTeardown('functions.js', function () {
4125     $(document).off('change', 'select.pageselector');
4126     $('#update_recent_tables').off('ready');
4127     $('#sync_favorite_tables').off('ready');
4130 AJAX.registerOnload('functions.js', function () {
4131     /**
4132      * Autosubmit page selector
4133      */
4134     $(document).on('change', 'select.pageselector', function (event) {
4135         event.stopPropagation();
4136         // Check where to load the new content
4137         if ($(this).closest('#pma_navigation').length === 0) {
4138             // For the main page we don't need to do anything,
4139             $(this).closest('form').submit();
4140         } else {
4141             // but for the navigation we need to manually replace the content
4142             Navigation.treePagination($(this));
4143         }
4144     });
4146     /**
4147      * Load version information asynchronously.
4148      */
4149     if ($('li.jsversioncheck').length > 0) {
4150         $.ajax({
4151             dataType: 'json',
4152             url: 'version_check.php',
4153             method: 'POST',
4154             data: {
4155                 'server': CommonParams.get('server')
4156             },
4157             success: Functions.currentVersion
4158         });
4159     }
4161     if ($('#is_git_revision').length > 0) {
4162         setTimeout(Functions.displayGitRevision, 10);
4163     }
4165     /**
4166      * Slider effect.
4167      */
4168     Functions.initSlider();
4170     var $updateRecentTables = $('#update_recent_tables');
4171     if ($updateRecentTables.length) {
4172         $.get(
4173             $updateRecentTables.attr('href'),
4174             { 'no_debug': true },
4175             function (data) {
4176                 if (typeof data !== 'undefined' && data.success === true) {
4177                     $('#pma_recent_list').html(data.list);
4178                 }
4179             }
4180         );
4181     }
4183     // Sync favorite tables from localStorage to pmadb.
4184     if ($('#sync_favorite_tables').length) {
4185         $.ajax({
4186             url: $('#sync_favorite_tables').attr('href'),
4187             cache: false,
4188             type: 'POST',
4189             data: {
4190                 'favoriteTables': (isStorageSupported('localStorage') && typeof window.localStorage.favoriteTables !== 'undefined')
4191                     ? window.localStorage.favoriteTables
4192                     : '',
4193                 'server': CommonParams.get('server'),
4194                 'no_debug': true
4195             },
4196             success: function (data) {
4197                 // Update localStorage.
4198                 if (isStorageSupported('localStorage')) {
4199                     window.localStorage.favoriteTables = data.favoriteTables;
4200                 }
4201                 $('#pma_favorite_list').html(data.list);
4202             }
4203         });
4204     }
4205 }); // end of $()
4208  * Initializes slider effect.
4209  */
4210 Functions.initSlider = function () {
4211     $('div.pma_auto_slider').each(function () {
4212         var $this = $(this);
4213         if ($this.data('slider_init_done')) {
4214             return;
4215         }
4216         var $wrapper = $('<div>', { 'class': 'slide-wrapper' });
4217         $wrapper.toggle($this.is(':visible'));
4218         $('<a>', { href: '#' + this.id, 'class': 'ajax' })
4219             .text($this.attr('title'))
4220             .prepend($('<span>'))
4221             .insertBefore($this)
4222             .on('click', function () {
4223                 var $wrapper = $this.closest('.slide-wrapper');
4224                 var visible = $this.is(':visible');
4225                 if (!visible) {
4226                     $wrapper.show();
4227                 }
4228                 $this[visible ? 'hide' : 'show']('blind', function () {
4229                     $wrapper.toggle(!visible);
4230                     $wrapper.parent().toggleClass('print_ignore', visible);
4231                     Functions.setStatusLabel($this);
4232                 });
4233                 return false;
4234             });
4235         $this.wrap($wrapper);
4236         $this.removeAttr('title');
4237         Functions.setStatusLabel($this);
4238         $this.data('slider_init_done', 1);
4239     });
4243  * Initializes slider effect.
4244  */
4245 AJAX.registerOnload('functions.js', function () {
4246     Functions.initSlider();
4250  * Restores sliders to the state they were in before initialisation.
4251  */
4252 AJAX.registerTeardown('functions.js', function () {
4253     $('div.pma_auto_slider').each(function () {
4254         var $this = $(this);
4255         $this.removeData();
4256         $this.parent().replaceWith($this);
4257         $this.parent().children('a').remove();
4258     });
4262  * Creates a message inside an object with a sliding effect
4264  * @param msg    A string containing the text to display
4265  * @param $obj   a jQuery object containing the reference
4266  *                 to the element where to put the message
4267  *                 This is optional, if no element is
4268  *                 provided, one will be created below the
4269  *                 navigation links at the top of the page
4271  * @return bool   True on success, false on failure
4272  */
4273 Functions.slidingMessage = function (msg, $object) {
4274     var $obj = $object;
4275     if (msg === undefined || msg.length === 0) {
4276         // Don't show an empty message
4277         return false;
4278     }
4279     if ($obj === undefined || !($obj instanceof jQuery) || $obj.length === 0) {
4280         // If the second argument was not supplied,
4281         // we might have to create a new DOM node.
4282         if ($('#PMA_slidingMessage').length === 0) {
4283             $('#page_content').prepend(
4284                 '<span id="PMA_slidingMessage" ' +
4285                 'class="pma_sliding_message"></span>'
4286             );
4287         }
4288         $obj = $('#PMA_slidingMessage');
4289     }
4290     if ($obj.has('div').length > 0) {
4291         // If there already is a message inside the
4292         // target object, we must get rid of it
4293         $obj
4294             .find('div')
4295             .first()
4296             .fadeOut(function () {
4297                 $obj
4298                     .children()
4299                     .remove();
4300                 $obj
4301                     .append('<div>' + msg + '</div>');
4302                 // highlight any sql before taking height;
4303                 Functions.highlightSql($obj);
4304                 $obj.find('div')
4305                     .first()
4306                     .hide();
4307                 $obj
4308                     .animate({
4309                         height: $obj.find('div').first().height()
4310                     })
4311                     .find('div')
4312                     .first()
4313                     .fadeIn();
4314             });
4315     } else {
4316         // Object does not already have a message
4317         // inside it, so we simply slide it down
4318         $obj.width('100%')
4319             .html('<div>' + msg + '</div>');
4320         // highlight any sql before taking height;
4321         Functions.highlightSql($obj);
4322         var h = $obj
4323             .find('div')
4324             .first()
4325             .hide()
4326             .height();
4327         $obj
4328             .find('div')
4329             .first()
4330             .css('height', 0)
4331             .show()
4332             .animate({
4333                 height: h
4334             }, function () {
4335             // Set the height of the parent
4336             // to the height of the child
4337                 $obj
4338                     .height(
4339                         $obj
4340                             .find('div')
4341                             .first()
4342                             .height()
4343                     );
4344             });
4345     }
4346     return true;
4350  * Attach CodeMirror2 editor to SQL edit area.
4351  */
4352 AJAX.registerOnload('functions.js', function () {
4353     var $elm = $('#sqlquery');
4354     if ($elm.siblings().filter('.CodeMirror').length > 0) {
4355         return;
4356     }
4357     if ($elm.length > 0) {
4358         if (typeof CodeMirror !== 'undefined') {
4359             codeMirrorEditor = Functions.getSqlEditor($elm);
4360             codeMirrorEditor.focus();
4361             codeMirrorEditor.on('blur', Functions.updateQueryParameters);
4362         } else {
4363             // without codemirror
4364             $elm.trigger('focus').on('blur', Functions.updateQueryParameters);
4365         }
4366     }
4367     Functions.highlightSql($('body'));
4369 AJAX.registerTeardown('functions.js', function () {
4370     if (codeMirrorEditor) {
4371         $('#sqlquery').text(codeMirrorEditor.getValue());
4372         codeMirrorEditor.toTextArea();
4373         codeMirrorEditor = false;
4374     }
4376 AJAX.registerOnload('functions.js', function () {
4377     // initializes all lock-page elements lock-id and
4378     // val-hash data property
4379     $('#page_content form.lock-page textarea, ' +
4380             '#page_content form.lock-page input[type="text"], ' +
4381             '#page_content form.lock-page input[type="number"], ' +
4382             '#page_content form.lock-page select').each(function (i) {
4383         $(this).data('lock-id', i);
4384         // val-hash is the hash of default value of the field
4385         // so that it can be compared with new value hash
4386         // to check whether field was modified or not.
4387         $(this).data('val-hash', AJAX.hash($(this).val()));
4388     });
4390     // initializes lock-page elements (input types checkbox and radio buttons)
4391     // lock-id and val-hash data property
4392     $('#page_content form.lock-page input[type="checkbox"], ' +
4393             '#page_content form.lock-page input[type="radio"]').each(function (i) {
4394         $(this).data('lock-id', i);
4395         $(this).data('val-hash', AJAX.hash($(this).is(':checked')));
4396     });
4400  * jQuery plugin to correctly filter input fields by value, needed
4401  * because some nasty values may break selector syntax
4402  */
4403 (function ($) {
4404     $.fn.filterByValue = function (value) {
4405         return this.filter(function () {
4406             return $(this).val() === value;
4407         });
4408     };
4409 }(jQuery));
4412  * Return value of a cell in a table.
4413  */
4414 Functions.getCellValue = function (td) {
4415     var $td = $(td);
4416     if ($td.is('.null')) {
4417         return '';
4418     } else if ((! $td.is('.to_be_saved')
4419         || $td.is('.set'))
4420         && $td.data('original_data')
4421     ) {
4422         return $td.data('original_data');
4423     } else {
4424         return $td.text();
4425     }
4428 $(window).on('popstate', function () {
4429     $('#printcss').attr('media','print');
4430     return true;
4434  * Unbind all event handlers before tearing down a page
4435  */
4436 AJAX.registerTeardown('functions.js', function () {
4437     $(document).off('click', 'a.themeselect');
4438     $(document).off('change', '.autosubmit');
4439     $('a.take_theme').off('click');
4442 AJAX.registerOnload('functions.js', function () {
4443     /**
4444      * Theme selector.
4445      */
4446     $(document).on('click', 'a.themeselect', function (e) {
4447         window.open(
4448             e.target,
4449             'themes',
4450             'left=10,top=20,width=510,height=350,scrollbars=yes,status=yes,resizable=yes'
4451         );
4452         return false;
4453     });
4455     /**
4456      * Automatic form submission on change.
4457      */
4458     $(document).on('change', '.autosubmit', function () {
4459         $(this).closest('form').trigger('submit');
4460     });
4462     /**
4463      * Theme changer.
4464      */
4465     $('a.take_theme').on('click', function () {
4466         var what = this.name;
4467         if (window.opener && window.opener.document.forms.setTheme.elements.set_theme) {
4468             window.opener.document.forms.setTheme.elements.set_theme.value = what;
4469             window.opener.document.forms.setTheme.submit();
4470             window.close();
4471             return false;
4472         }
4473         return true;
4474     });
4478  * Produce print preview
4479  */
4480 Functions.printPreview = function () {
4481     $('#printcss').attr('media','all');
4482     Functions.createPrintAndBackButtons();
4486  * Create print and back buttons in preview page
4487  */
4488 Functions.createPrintAndBackButtons = function () {
4489     var backButton = $('<input>',{
4490         type: 'button',
4491         value: Messages.back,
4492         class: 'btn btn-primary',
4493         id: 'back_button_print_view'
4494     });
4495     backButton.on('click', Functions.removePrintAndBackButton);
4496     backButton.appendTo('#page_content');
4497     var printButton = $('<input>',{
4498         type: 'button',
4499         value: Messages.print,
4500         class: 'btn btn-primary',
4501         id: 'print_button_print_view'
4502     });
4503     printButton.on('click', Functions.printPage);
4504     printButton.appendTo('#page_content');
4508  * Remove print and back buttons and revert to normal view
4509  */
4510 Functions.removePrintAndBackButton = function () {
4511     $('#printcss').attr('media','print');
4512     $('#back_button_print_view').remove();
4513     $('#print_button_print_view').remove();
4517  * Print page
4518  */
4519 Functions.printPage = function () {
4520     if (typeof(window.print) !== 'undefined') {
4521         window.print();
4522     }
4526  * Unbind all event handlers before tearing down a page
4527  */
4528 AJAX.registerTeardown('functions.js', function () {
4529     $('input#print').off('click');
4530     $(document).off('click', 'a.create_view.ajax');
4531     $(document).off('keydown', '#createViewDialog input, #createViewDialog select');
4532     $(document).off('change', '#fkc_checkbox');
4535 AJAX.registerOnload('functions.js', function () {
4536     $('input#print').on('click', Functions.printPage);
4537     $('.logout').on('click', function () {
4538         var form = $(
4539             '<form method="POST" action="' + $(this).attr('href') + '" class="disableAjax">' +
4540             '<input type="hidden" name="token" value="' + Functions.escapeHtml(CommonParams.get('token')) + '">' +
4541             '</form>'
4542         );
4543         $('body').append(form);
4544         form.submit();
4545         sessionStorage.clear();
4546         return false;
4547     });
4548     /**
4549      * Ajaxification for the "Create View" action
4550      */
4551     $(document).on('click', 'a.create_view.ajax', function (e) {
4552         e.preventDefault();
4553         Functions.createViewDialog($(this));
4554     });
4555     /**
4556      * Attach Ajax event handlers for input fields in the editor
4557      * and used to submit the Ajax request when the ENTER key is pressed.
4558      */
4559     if ($('#createViewDialog').length !== 0) {
4560         $(document).on('keydown', '#createViewDialog input, #createViewDialog select', function (e) {
4561             if (e.which === 13) { // 13 is the ENTER key
4562                 e.preventDefault();
4564                 // with preventing default, selection by <select> tag
4565                 // was also prevented in IE
4566                 $(this).trigger('blur');
4568                 $(this).closest('.ui-dialog').find('.ui-button:first').trigger('click');
4569             }
4570         }); // end $(document).on()
4571     }
4573     if ($('textarea[name="view[as]"]').length !== 0) {
4574         codeMirrorEditor = Functions.getSqlEditor($('textarea[name="view[as]"]'));
4575     }
4578 Functions.createViewDialog = function ($this) {
4579     var $msg = Functions.ajaxShowMessage();
4580     var sep = CommonParams.get('arg_separator');
4581     var params = Functions.getJsConfirmCommonParam(this, $this.getPostData());
4582     params += sep + 'ajax_dialog=1';
4583     $.post($this.attr('href'), params, function (data) {
4584         if (typeof data !== 'undefined' && data.success === true) {
4585             Functions.ajaxRemoveMessage($msg);
4586             var buttonOptions = {};
4587             buttonOptions[Messages.strGo] = function () {
4588                 if (typeof CodeMirror !== 'undefined') {
4589                     codeMirrorEditor.save();
4590                 }
4591                 $msg = Functions.ajaxShowMessage();
4592                 $.post('view_create.php', $('#createViewDialog').find('form').serialize(), function (data) {
4593                     Functions.ajaxRemoveMessage($msg);
4594                     if (typeof data !== 'undefined' && data.success === true) {
4595                         $('#createViewDialog').dialog('close');
4596                         $('.result_query').html(data.message);
4597                         Navigation.reload();
4598                     } else {
4599                         Functions.ajaxShowMessage(data.error);
4600                     }
4601                 });
4602             };
4603             buttonOptions[Messages.strClose] = function () {
4604                 $(this).dialog('close');
4605             };
4606             var $dialog = $('<div></div>').attr('id', 'createViewDialog').append(data.message).dialog({
4607                 width: 600,
4608                 minWidth: 400,
4609                 modal: true,
4610                 buttons: buttonOptions,
4611                 title: Messages.strCreateView,
4612                 close: function () {
4613                     $(this).remove();
4614                 }
4615             });
4616             // Attach syntax highlighted editor
4617             codeMirrorEditor = Functions.getSqlEditor($dialog.find('textarea'));
4618             $('input:visible[type=text]', $dialog).first().trigger('focus');
4619         } else {
4620             Functions.ajaxShowMessage(data.error);
4621         }
4622     });
4626  * Makes the breadcrumbs and the menu bar float at the top of the viewport
4627  */
4628 $(function () {
4629     if ($('#floating_menubar').length && $('#PMA_disable_floating_menubar').length === 0) {
4630         var left = $('html').attr('dir') === 'ltr' ? 'left' : 'right';
4631         $('#floating_menubar')
4632             .css('margin-' + left, $('#pma_navigation').width() + $('#pma_navigation_resizer').width())
4633             .css(left, 0)
4634             .css({
4635                 'position': 'fixed',
4636                 'top': 0,
4637                 'width': '100%',
4638                 'z-index': 99
4639             })
4640             .append($('#serverinfo'))
4641             .append($('#topmenucontainer'));
4642         // Allow the DOM to render, then adjust the padding on the body
4643         setTimeout(function () {
4644             $('body').css(
4645                 'padding-top',
4646                 $('#floating_menubar').outerHeight(true)
4647             );
4648             $('#topmenu').menuResizer('resize');
4649         }, 4);
4650     }
4654  * Scrolls the page to the top if clicking the serverinfo bar
4655  */
4656 $(function () {
4657     $(document).on('click', '#serverinfo, #goto_pagetop', function (event) {
4658         event.preventDefault();
4659         $('html, body').animate({ scrollTop: 0 }, 'fast');
4660     });
4663 var checkboxesSel = 'input.checkall:checkbox:enabled';
4666  * Watches checkboxes in a form to set the checkall box accordingly
4667  */
4668 Functions.checkboxesChanged = function () {
4669     var $form = $(this.form);
4670     // total number of checkboxes in current form
4671     var totalBoxes = $form.find(checkboxesSel).length;
4672     // number of checkboxes checked in current form
4673     var checkedBoxes = $form.find(checkboxesSel + ':checked').length;
4674     var $checkall = $form.find('input.checkall_box');
4675     if (totalBoxes === checkedBoxes) {
4676         $checkall.prop({ checked: true, indeterminate: false });
4677     } else if (checkedBoxes > 0) {
4678         $checkall.prop({ checked: true, indeterminate: true });
4679     } else {
4680         $checkall.prop({ checked: false, indeterminate: false });
4681     }
4684 $(document).on('change', checkboxesSel, Functions.checkboxesChanged);
4686 $(document).on('change', 'input.checkall_box', function () {
4687     var isChecked = $(this).is(':checked');
4688     $(this.form).find(checkboxesSel).not('.row-hidden').prop('checked', isChecked)
4689         .parents('tr').toggleClass('marked', isChecked);
4692 $(document).on('click', '.checkall-filter', function () {
4693     var $this = $(this);
4694     var selector = $this.data('checkall-selector');
4695     $('input.checkall_box').prop('checked', false);
4696     $this.parents('form').find(checkboxesSel).filter(selector).prop('checked', true).trigger('change')
4697         .parents('tr').toggleClass('marked', true);
4698     return false;
4702  * Watches checkboxes in a sub form to set the sub checkall box accordingly
4703  */
4704 Functions.subCheckboxesChanged = function () {
4705     var $form = $(this).parent().parent();
4706     // total number of checkboxes in current sub form
4707     var totalBoxes = $form.find(checkboxesSel).length;
4708     // number of checkboxes checked in current sub form
4709     var checkedBoxes = $form.find(checkboxesSel + ':checked').length;
4710     var $checkall = $form.find('input.sub_checkall_box');
4711     if (totalBoxes === checkedBoxes) {
4712         $checkall.prop({ checked: true, indeterminate: false });
4713     } else if (checkedBoxes > 0) {
4714         $checkall.prop({ checked: true, indeterminate: true });
4715     } else {
4716         $checkall.prop({ checked: false, indeterminate: false });
4717     }
4720 $(document).on('change', checkboxesSel + ', input.checkall_box:checkbox:enabled', Functions.subCheckboxesChanged);
4722 $(document).on('change', 'input.sub_checkall_box', function () {
4723     var isChecked = $(this).is(':checked');
4724     var $form = $(this).parent().parent();
4725     $form.find(checkboxesSel).prop('checked', isChecked)
4726         .parents('tr').toggleClass('marked', isChecked);
4730  * Rows filtering
4732  * - rows to filter are identified by data-filter-row attribute
4733  *   which contains uppercase string to filter
4734  * - it is simple substring case insensitive search
4735  * - optionally number of matching rows is written to element with
4736  *   id filter-rows-count
4737  */
4738 $(document).on('keyup', '#filterText', function () {
4739     var filterInput = $(this).val().toUpperCase().replace(/ /g, '_');
4740     var count = 0;
4741     $('[data-filter-row]').each(function () {
4742         var $row = $(this);
4743         /* Can not use data() here as it does magic conversion to int for numeric values */
4744         if ($row.attr('data-filter-row').indexOf(filterInput) > -1) {
4745             count += 1;
4746             $row.show();
4747             $row.find('input.checkall').removeClass('row-hidden');
4748         } else {
4749             $row.hide();
4750             $row.find('input.checkall').addClass('row-hidden').prop('checked', false);
4751             $row.removeClass('marked');
4752         }
4753     });
4754     setTimeout(function () {
4755         $(checkboxesSel).trigger('change');
4756     }, 300);
4757     $('#filter-rows-count').html(count);
4759 AJAX.registerOnload('functions.js', function () {
4760     /* Trigger filtering of the list based on incoming database name */
4761     var $filter = $('#filterText');
4762     if ($filter.val()) {
4763         $filter.trigger('keyup').select();
4764     }
4768  * Formats a byte number to human-readable form
4770  * @param bytes the bytes to format
4771  * @param optional subdecimals the number of digits after the point
4772  * @param optional pointchar the char to use as decimal point
4773  */
4774 Functions.formatBytes = function (bytesToFormat, subDecimals, pointChar) {
4775     var bytes = bytesToFormat;
4776     var decimals = subDecimals;
4777     var point = pointChar;
4778     if (!decimals) {
4779         decimals = 0;
4780     }
4781     if (!point) {
4782         point = '.';
4783     }
4784     var units = ['B', 'KiB', 'MiB', 'GiB'];
4785     for (var i = 0; bytes > 1024 && i < units.length; i++) {
4786         bytes /= 1024;
4787     }
4788     var factor = Math.pow(10, decimals);
4789     bytes = Math.round(bytes * factor) / factor;
4790     bytes = bytes.toString().split('.').join(point);
4791     return bytes + ' ' + units[i];
4794 AJAX.registerOnload('functions.js', function () {
4795     /**
4796      * Reveal the login form to users with JS enabled
4797      * and focus the appropriate input field
4798      */
4799     var $loginform = $('#loginform');
4800     if ($loginform.length) {
4801         $loginform.find('.js-show').show();
4802         if ($('#input_username').val()) {
4803             $('#input_password').trigger('focus');
4804         } else {
4805             $('#input_username').trigger('focus');
4806         }
4807     }
4808     var $httpsWarning = $('#js-https-mismatch');
4809     if ($httpsWarning.length) {
4810         if ((window.location.protocol === 'https:') !== CommonParams.get('is_https')) {
4811             $httpsWarning.show();
4812         }
4813     }
4817  * Formats timestamp for display
4818  */
4819 Functions.formatDateTime = function (date, seconds) {
4820     var result = $.datepicker.formatDate('yy-mm-dd', date);
4821     var timefmt = 'HH:mm';
4822     if (seconds) {
4823         timefmt = 'HH:mm:ss';
4824     }
4825     return result + ' ' + $.datepicker.formatTime(
4826         timefmt, {
4827             hour: date.getHours(),
4828             minute: date.getMinutes(),
4829             second: date.getSeconds()
4830         }
4831     );
4835  * Check than forms have less fields than max allowed by PHP.
4836  */
4837 Functions.checkNumberOfFields = function () {
4838     if (typeof maxInputVars === 'undefined') {
4839         return false;
4840     }
4841     if (false === maxInputVars) {
4842         return false;
4843     }
4844     $('form').each(function () {
4845         var nbInputs = $(this).find(':input').length;
4846         if (nbInputs > maxInputVars) {
4847             var warning = Functions.sprintf(Messages.strTooManyInputs, maxInputVars);
4848             Functions.ajaxShowMessage(warning);
4849             return false;
4850         }
4851         return true;
4852     });
4854     return true;
4858  * Ignore the displayed php errors.
4859  * Simply removes the displayed errors.
4861  * @param  clearPrevErrors whether to clear errors stored
4862  *             in $_SESSION['prev_errors'] at server
4864  */
4865 Functions.ignorePhpErrors = function (clearPrevErrors) {
4866     var clearPrevious = clearPrevErrors;
4867     if (typeof(clearPrevious) === 'undefined' ||
4868         clearPrevious === null
4869     ) {
4870         clearPrevious = false;
4871     }
4872     // send AJAX request to error_report.php with send_error_report=0, exception_type=php & token.
4873     // It clears the prev_errors stored in session.
4874     if (clearPrevious) {
4875         var $pmaReportErrorsForm = $('#pma_report_errors_form');
4876         $pmaReportErrorsForm.find('input[name="send_error_report"]').val(0); // change send_error_report to '0'
4877         $pmaReportErrorsForm.submit();
4878     }
4880     // remove displayed errors
4881     var $pmaErrors = $('#pma_errors');
4882     $pmaErrors.fadeOut('slow');
4883     $pmaErrors.remove();
4887  * Toggle the Datetimepicker UI if the date value entered
4888  * by the user in the 'text box' is not going to be accepted
4889  * by the Datetimepicker plugin (but is accepted by MySQL)
4890  */
4891 Functions.toggleDatepickerIfInvalid = function ($td, $inputField) {
4892     // Regex allowed by the Datetimepicker UI
4893     var dtexpDate = new RegExp(['^([0-9]{4})',
4894         '-(((01|03|05|07|08|10|12)-((0[1-9])|([1-2][0-9])|(3[0-1])))|((02|04|06|09|11)',
4895         '-((0[1-9])|([1-2][0-9])|30)))$'].join(''));
4896     var dtexpTime = new RegExp(['^(([0-1][0-9])|(2[0-3]))',
4897         ':((0[0-9])|([1-5][0-9]))',
4898         ':((0[0-9])|([1-5][0-9]))(.[0-9]{1,6}){0,1}$'].join(''));
4900     // If key-ed in Time or Date values are unsupported by the UI, close it
4901     if ($td.attr('data-type') === 'date' && ! dtexpDate.test($inputField.val())) {
4902         $inputField.datepicker('hide');
4903     } else if ($td.attr('data-type') === 'time' && ! dtexpTime.test($inputField.val())) {
4904         $inputField.datepicker('hide');
4905     } else {
4906         $inputField.datepicker('show');
4907     }
4911  * Function to submit the login form after validation is done.
4912  * NOTE: do NOT use a module or it will break the callback, issue #15435
4913  */
4914 // eslint-disable-next-line no-unused-vars, camelcase
4915 var Functions_recaptchaCallback = function () {
4916     $('#login_form').trigger('submit');
4920  * Unbind all event handlers before tearing down a page
4921  */
4922 AJAX.registerTeardown('functions.js', function () {
4923     $(document).off('keydown', 'form input, form textarea, form select');
4926 AJAX.registerOnload('functions.js', function () {
4927     /**
4928      * Handle 'Ctrl/Alt + Enter' form submits
4929      */
4930     $('form input, form textarea, form select').on('keydown', function (e) {
4931         if ((e.ctrlKey && e.which === 13) || (e.altKey && e.which === 13)) {
4932             var $form = $(this).closest('form');
4934             // There could be multiple submit buttons on the same form,
4935             // we assume all of them behave identical and just click one.
4936             if (! $form.find('input[type="submit"]:first') ||
4937                 ! $form.find('input[type="submit"]:first').trigger('click')
4938             ) {
4939                 $form.trigger('submit');
4940             }
4941         }
4942     });
4946  * Unbind all event handlers before tearing down a page
4947  */
4948 AJAX.registerTeardown('functions.js', function () {
4949     $(document).off('change', 'input[type=radio][name="pw_hash"]');
4950     $(document).off('mouseover', '.sortlink');
4951     $(document).off('mouseout', '.sortlink');
4954 AJAX.registerOnload('functions.js', function () {
4955     /*
4956      * Display warning regarding SSL when sha256_password
4957      * method is selected
4958      * Used in user_password.php (Change Password link on index.php)
4959      */
4960     $(document).on('change', 'select#select_authentication_plugin_cp', function () {
4961         if (this.value === 'sha256_password') {
4962             $('#ssl_reqd_warning_cp').show();
4963         } else {
4964             $('#ssl_reqd_warning_cp').hide();
4965         }
4966     });
4968     Cookies.defaults.path = CommonParams.get('rootPath');
4970     // Bind event handlers for toggling sort icons
4971     $(document).on('mouseover', '.sortlink', function () {
4972         $(this).find('.soimg').toggle();
4973     });
4974     $(document).on('mouseout', '.sortlink', function () {
4975         $(this).find('.soimg').toggle();
4976     });
4980  * Returns an HTML IMG tag for a particular image from a theme,
4981  * which may be an actual file or an icon from a sprite
4983  * @param string image      The name of the file to get
4984  * @param string alternate  Used to set 'alt' and 'title' attributes of the image
4985  * @param object attributes An associative array of other attributes
4987  * @return Object The requested image, this object has two methods:
4988  *                  .toString()        - Returns the IMG tag for the requested image
4989  *                  .attr(name)        - Returns a particular attribute of the IMG
4990  *                                       tag given it's name
4991  *                  .attr(name, value) - Sets a particular attribute of the IMG
4992  *                                       tag to the given value
4993  */
4994 Functions.getImage = function (image, alternate, attributes) {
4995     var alt = alternate;
4996     var attr = attributes;
4997     // custom image object, it will eventually be returned by this functions
4998     var retval = {
4999         data: {
5000             // this is private
5001             alt: '',
5002             title: '',
5003             src: 'themes/dot.gif',
5004         },
5005         attr: function (name, value) {
5006             if (value === undefined) {
5007                 if (this.data[name] === undefined) {
5008                     return '';
5009                 } else {
5010                     return this.data[name];
5011                 }
5012             } else {
5013                 this.data[name] = value;
5014             }
5015         },
5016         toString: function () {
5017             var retval = '<' + 'img';
5018             for (var i in this.data) {
5019                 retval += ' ' + i + '="' + this.data[i] + '"';
5020             }
5021             retval += ' /' + '>';
5022             return retval;
5023         }
5024     };
5025     // initialise missing parameters
5026     if (attr === undefined) {
5027         attr = {};
5028     }
5029     if (alt === undefined) {
5030         alt = '';
5031     }
5032     // set alt
5033     if (attr.alt !== undefined) {
5034         retval.attr('alt', Functions.escapeHtml(attr.alt));
5035     } else {
5036         retval.attr('alt', Functions.escapeHtml(alt));
5037     }
5038     // set title
5039     if (attr.title !== undefined) {
5040         retval.attr('title', Functions.escapeHtml(attr.title));
5041     } else {
5042         retval.attr('title', Functions.escapeHtml(alt));
5043     }
5044     // set css classes
5045     retval.attr('class', 'icon ic_' + image);
5046     // set all other attrubutes
5047     for (var i in attr) {
5048         if (i === 'src') {
5049             // do not allow to override the 'src' attribute
5050             continue;
5051         }
5053         retval.attr(i, attr[i]);
5054     }
5056     return retval;
5060  * Sets a configuration value.
5062  * A configuration value may be set in both browser's local storage and
5063  * remotely in server's configuration table.
5065  * NOTE: Depending on server's configuration, the configuration table may be or
5066  * not persistent.
5068  * @param  {string}     key         Configuration key.
5069  * @param  {object}     value       Configuration value.
5070  */
5071 Functions.configSet = function (key, value) {
5072     var serialized = JSON.stringify(value);
5073     localStorage.setItem(key, serialized);
5074     $.ajax({
5075         url: 'ajax.php',
5076         type: 'POST',
5077         dataType: 'json',
5078         data: {
5079             key: key,
5080             type: 'config-set',
5081             server: CommonParams.get('server'),
5082             value: serialized,
5083         },
5084         success: function (data) {
5085             // Updating value in local storage.
5086             if (! data.success) {
5087                 if (data.error) {
5088                     Functions.ajaxShowMessage(data.error);
5089                 } else {
5090                     Functions.ajaxShowMessage(data.message);
5091                 }
5092             }
5093             // Eventually, call callback.
5094         }
5095     });
5099  * Gets a configuration value. A configuration value will be searched in
5100  * browser's local storage first and if not found, a call to the server will be
5101  * made.
5103  * If value should not be cached and the up-to-date configuration value from
5104  * right from the server is required, the third parameter should be `false`.
5106  * @param  {string}     key         Configuration key.
5107  * @param  {boolean}    cached      Configuration type.
5109  * @return {object}                 Configuration value.
5110  */
5111 Functions.configGet = function (key, cached) {
5112     var isCached = (typeof cached !== 'undefined') ? cached : true;
5113     var value = localStorage.getItem(key);
5114     if (isCached && value !== undefined && value !== null) {
5115         return JSON.parse(value);
5116     }
5118     // Result not found in local storage or ignored.
5119     // Hitting the server.
5120     $.ajax({
5121         // TODO: This is ugly, but usually when a configuration is needed,
5122         // processing cannot continue until that value is found.
5123         // Another solution is to provide a callback as a parameter.
5124         async: false,
5125         url: 'ajax.php',
5126         type: 'POST',
5127         dataType: 'json',
5128         data: {
5129             type: 'config-get',
5130             server: CommonParams.get('server'),
5131             key: key
5132         },
5133         success: function (data) {
5134             // Updating value in local storage.
5135             if (data.success) {
5136                 localStorage.setItem(key, JSON.stringify(data.value));
5137             } else {
5138                 Functions.ajaxShowMessage(data.message);
5139             }
5140             // Eventually, call callback.
5141         }
5142     });
5143     return JSON.parse(localStorage.getItem(key));
5147  * Return POST data as stored by Util::linkOrButton
5148  */
5149 Functions.getPostData = function () {
5150     var dataPost = this.attr('data-post');
5151     // Strip possible leading ?
5152     if (dataPost !== undefined && dataPost.substring(0,1) === '?') {
5153         dataPost = dataPost.substr(1);
5154     }
5155     return dataPost;
5157 jQuery.fn.getPostData = Functions.getPostData;