Translated using Weblate (German)
[phpmyadmin.git] / js / src / functions.js
blob635a8ad6e1473352fe5fd9a8f89527d54109144a
2 /* global isStorageSupported */ // js/config.js
3 /* global ChartType, ColumnType, DataTable, JQPlotChartFactory */ // js/chart.js
4 /* global DatabaseStructure */ // js/database/structure.js
5 /* global mysqlDocBuiltin, mysqlDocKeyword */ // js/doclinks.js
6 /* global Indexes */ // js/indexes.js
7 /* global firstDayOfCalendar, maxInputVars, mysqlDocTemplate, themeImagePath */ // templates/javascript/variables.twig
8 /* global sprintf */ // js/vendor/sprintf.js
9 /* global zxcvbnts */ // js/vendor/zxcvbn-ts.js
11 /**
12  * general function, usually for data manipulation pages
13  * @test-module Functions
14  */
15 var Functions = {};
17 /**
18  * @var sqlBoxLocked lock for the sqlbox textarea in the querybox
19  */
20 // eslint-disable-next-line no-unused-vars
21 var sqlBoxLocked = false;
23 /**
24  * @var {array}, holds elements which content should only selected once
25  */
26 var onlyOnceElements = [];
28 /**
29  * @var {number} ajaxMessageCount Number of AJAX messages shown since page load
30  */
31 var ajaxMessageCount = 0;
33 /**
34  * @var codeMirrorEditor object containing CodeMirror editor of the query editor in SQL tab
35  */
36 var codeMirrorEditor = false;
38 /**
39  * @var codeMirrorInlineEditor object containing CodeMirror editor of the inline query editor
40  */
41 var codeMirrorInlineEditor = false;
43 /**
44  * @var {boolean} sqlAutoCompleteInProgress shows if Table/Column name autocomplete AJAX is in progress
45  */
46 var sqlAutoCompleteInProgress = false;
48 /**
49  * @var sqlAutoComplete object containing list of columns in each table
50  */
51 var sqlAutoComplete = false;
53 /**
54  * @var {string} sqlAutoCompleteDefaultTable string containing default table to autocomplete columns
55  */
56 var sqlAutoCompleteDefaultTable = '';
58 /**
59  * @var {array} centralColumnList array to hold the columns in central list per db.
60  */
61 var centralColumnList = [];
63 /**
64  * @var {array} primaryIndexes array to hold 'Primary' index columns.
65  */
66 // eslint-disable-next-line no-unused-vars
67 var primaryIndexes = [];
69 /**
70  * @var {array} uniqueIndexes array to hold 'Unique' index columns.
71  */
72 // eslint-disable-next-line no-unused-vars
73 var uniqueIndexes = [];
75 /**
76  * @var {array} indexes array to hold 'Index' columns.
77  */
78 // eslint-disable-next-line no-unused-vars
79 var indexes = [];
81 /**
82  * @var {array} fulltextIndexes array to hold 'Fulltext' columns.
83  */
84 // eslint-disable-next-line no-unused-vars
85 var fulltextIndexes = [];
87 /**
88  * @var {array} spatialIndexes array to hold 'Spatial' columns.
89  */
90 // eslint-disable-next-line no-unused-vars
91 var spatialIndexes = [];
93 /**
94  * Make sure that ajax requests will not be cached
95  * by appending a random variable to their parameters
96  */
97 $.ajaxPrefilter(function (options, originalOptions) {
98     var nocache = new Date().getTime() + '' + Math.floor(Math.random() * 1000000);
99     if (typeof options.data === 'string') {
100         options.data += '&_nocache=' + nocache + '&token=' + encodeURIComponent(CommonParams.get('token'));
101     } else if (typeof options.data === 'object') {
102         options.data = $.extend(originalOptions.data, { '_nocache' : nocache, 'token': CommonParams.get('token') });
103     }
107  * Adds a date/time picker to an element
109  * @param {object} $thisElement a jQuery object pointing to the element
110  * @param {string} type
111  * @param {object} options
112  */
113 Functions.addDatepicker = function ($thisElement, type, options) {
114     if (type !== 'date' && type !== 'time' && type !== 'datetime' && type !== 'timestamp') {
115         return;
116     }
118     var showTimepicker = true;
119     if (type === 'date') {
120         showTimepicker = false;
121     }
123     // Getting the current Date and time
124     var currentDateTime = new Date();
126     var defaultOptions = {
127         timeInput : true,
128         hour: currentDateTime.getHours(),
129         minute: currentDateTime.getMinutes(),
130         second: currentDateTime.getSeconds(),
131         showOn: 'button',
132         buttonImage: themeImagePath + 'b_calendar.png',
133         buttonImageOnly: true,
134         stepMinutes: 1,
135         stepHours: 1,
136         showSecond: true,
137         showMillisec: true,
138         showMicrosec: true,
139         showTimepicker: showTimepicker,
140         showButtonPanel: false,
141         changeYear: true,
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.uiTooltip('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.uiTooltip('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                 firstDay: firstDayOfCalendar
231             });
232             // Add a tip regarding entering MySQL allowed-values
233             // for TIME and DATE data-type
234             if ($(this).hasClass('timefield')) {
235                 Functions.tooltip($(this), 'input', Messages.strMysqlAllowedValuesTipTime);
236             } else if ($(this).hasClass('datefield')) {
237                 Functions.tooltip($(this), 'input', Messages.strMysqlAllowedValuesTipDate);
238             }
239         });
240     }
244  * Handle redirect and reload flags sent as part of AJAX requests
246  * @param data ajax response data
247  */
248 Functions.handleRedirectAndReload = function (data) {
249     if (parseInt(data.redirect_flag) === 1) {
250         // add one more GET param to display session expiry msg
251         if (window.location.href.indexOf('?') === -1) {
252             window.location.href += '?session_expired=1';
253         } else {
254             window.location.href += CommonParams.get('arg_separator') + 'session_expired=1';
255         }
256         window.location.reload();
257     } else if (parseInt(data.reload_flag) === 1) {
258         window.location.reload();
259     }
263  * Creates an SQL editor which supports auto completing etc.
265  * @param $textarea   jQuery object wrapping the textarea to be made the editor
266  * @param options     optional options for CodeMirror
267  * @param {'vertical'|'horizontal'|'both'} resize optional resizing ('vertical', 'horizontal', 'both')
268  * @param lintOptions additional options for lint
270  * @return {object|null}
271  */
272 Functions.getSqlEditor = function ($textarea, options, resize, lintOptions) {
273     var resizeType = resize;
274     if ($textarea.length > 0 && typeof CodeMirror !== 'undefined') {
275         // merge options for CodeMirror
276         var defaults = {
277             lineNumbers: true,
278             matchBrackets: true,
279             extraKeys: { 'Ctrl-Space': 'autocomplete' },
280             hintOptions: { 'completeSingle': false, 'completeOnSingleClick': true },
281             indentUnit: 4,
282             mode: 'text/x-mysql',
283             lineWrapping: true
284         };
286         if (CodeMirror.sqlLint) {
287             $.extend(defaults, {
288                 gutters: ['CodeMirror-lint-markers'],
289                 lint: {
290                     'getAnnotations': CodeMirror.sqlLint,
291                     'async': true,
292                     'lintOptions': lintOptions
293                 }
294             });
295         }
297         $.extend(true, defaults, options);
299         // create CodeMirror editor
300         var codemirrorEditor = CodeMirror.fromTextArea($textarea[0], defaults);
301         // allow resizing
302         if (! resizeType) {
303             resizeType = 'vertical';
304         }
305         var handles = '';
306         if (resizeType === 'vertical') {
307             handles = 's';
308         }
309         if (resizeType === 'both') {
310             handles = 'all';
311         }
312         if (resizeType === 'horizontal') {
313             handles = 'e, w';
314         }
315         $(codemirrorEditor.getWrapperElement())
316             .css('resize', resizeType)
317             .resizable({
318                 handles: handles,
319                 resize: function () {
320                     codemirrorEditor.setSize($(this).width(), $(this).height());
321                 }
322             });
323         // enable autocomplete
324         codemirrorEditor.on('inputRead', Functions.codeMirrorAutoCompleteOnInputRead);
326         // page locking
327         codemirrorEditor.on('change', function (e) {
328             e.data = {
329                 value: 3,
330                 content: codemirrorEditor.isClean(),
331             };
332             AJAX.lockPageHandler(e);
333         });
335         return codemirrorEditor;
336     }
337     return null;
341  * Clear text selection
342  */
343 Functions.clearSelection = function () {
344     if (document.selection && document.selection.empty) {
345         document.selection.empty();
346     } else if (window.getSelection) {
347         var sel = window.getSelection();
348         if (sel.empty) {
349             sel.empty();
350         }
351         if (sel.removeAllRanges) {
352             sel.removeAllRanges();
353         }
354     }
358  * Create a jQuery UI tooltip
360  * @param $elements     jQuery object representing the elements
361  * @param item          the item
362  *                      (see https://api.jqueryui.com/tooltip/#option-items)
363  * @param myContent     content of the tooltip
364  * @param additionalOptions to override the default options
366  */
367 Functions.tooltip = function ($elements, item, myContent, additionalOptions) {
368     if ($('#no_hint').length > 0) {
369         return;
370     }
372     var defaultOptions = {
373         content: myContent,
374         items:  item,
375         tooltipClass: 'tooltip',
376         track: true,
377         show: false,
378         hide: false
379     };
381     $elements.uiTooltip($.extend(true, defaultOptions, additionalOptions));
385  * HTML escaping
387  * @param {any} unsafe
388  * @return {string | false}
389  */
390 Functions.escapeHtml = function (unsafe) {
391     if (typeof(unsafe) !== 'undefined') {
392         return unsafe
393             .toString()
394             .replace(/&/g, '&amp;')
395             .replace(/</g, '&lt;')
396             .replace(/>/g, '&gt;')
397             .replace(/"/g, '&quot;')
398             .replace(/'/g, '&#039;');
399     } else {
400         return false;
401     }
405  * JavaScript escaping
407  * @param {any} unsafe
408  * @return {string | false}
409  */
410 Functions.escapeJsString = function (unsafe) {
411     if (typeof(unsafe) !== 'undefined') {
412         return unsafe
413             .toString()
414             .replace('\x00', '')
415             .replace('\\', '\\\\')
416             .replace('\'', '\\\'')
417             .replace('&#039;', '\\&#039;')
418             .replace('"', '\\"')
419             .replace('&quot;', '\\&quot;')
420             .replace('\n', '\n')
421             .replace('\r', '\r')
422             .replace(/<\/script/gi, '</\' + \'script');
423     } else {
424         return false;
425     }
429  * @param {string} s
430  * @return {string}
431  */
432 Functions.escapeBacktick = function (s) {
433     return s.replace('`', '``');
437  * @param {string} s
438  * @return {string}
439  */
440 Functions.escapeSingleQuote = function (s) {
441     return s.replace('\\', '\\\\').replace('\'', '\\\'');
444 Functions.sprintf = function () {
445     return sprintf.apply(this, arguments);
449  * Hides/shows the default value input field, depending on the default type
450  * Ticks the NULL checkbox if NULL is chosen as default value.
452  * @param {JQuery<HTMLElement>} $defaultType
453  */
454 Functions.hideShowDefaultValue = function ($defaultType) {
455     if ($defaultType.val() === 'USER_DEFINED') {
456         $defaultType.siblings('.default_value').show().trigger('focus');
457     } else {
458         $defaultType.siblings('.default_value').hide();
459         if ($defaultType.val() === 'NULL') {
460             var $nullCheckbox = $defaultType.closest('tr').find('.allow_null');
461             $nullCheckbox.prop('checked', true);
462         }
463     }
467  * Hides/shows the input field for column expression based on whether
468  * VIRTUAL/PERSISTENT is selected
470  * @param $virtuality virtuality dropdown
471  */
472 Functions.hideShowExpression = function ($virtuality) {
473     if ($virtuality.val() === '') {
474         $virtuality.siblings('.expression').hide();
475     } else {
476         $virtuality.siblings('.expression').show();
477     }
481  * Show notices for ENUM columns; add/hide the default value
483  */
484 Functions.verifyColumnsProperties = function () {
485     $('select.column_type').each(function () {
486         Functions.showNoticeForEnum($(this));
487         Functions.showWarningForIntTypes();
488     });
489     $('select.default_type').each(function () {
490         Functions.hideShowDefaultValue($(this));
491     });
492     $('select.virtuality').each(function () {
493         Functions.hideShowExpression($(this));
494     });
498  * Add a hidden field to the form to indicate that this will be an
499  * Ajax request (only if this hidden field does not exist)
501  * @param {object} $form the form
502  */
503 Functions.prepareForAjaxRequest = function ($form) {
504     if (! $form.find('input:hidden').is('#ajax_request_hidden')) {
505         $form.append('<input type="hidden" id="ajax_request_hidden" name="ajax_request" value="true">');
506     }
509 Functions.checkPasswordStrength = function (value, meterObject, meterObjectLabel, username) {
510     // List of words we don't want to appear in the password
511     var customDict = [
512         'phpmyadmin',
513         'mariadb',
514         'mysql',
515         'php',
516         'my',
517         'admin',
518     ];
519     if (username) {
520         customDict.push(username);
521     }
523     zxcvbnts.core.zxcvbnOptions.setOptions({ dictionary: { userInputs: customDict } });
524     var zxcvbnObject = zxcvbnts.core.zxcvbn(value);
525     var strength = zxcvbnObject.score;
526     strength = parseInt(strength);
527     meterObject.val(strength);
528     switch (strength) {
529     case 0: meterObjectLabel.html(Messages.strExtrWeak);
530         break;
531     case 1: meterObjectLabel.html(Messages.strVeryWeak);
532         break;
533     case 2: meterObjectLabel.html(Messages.strWeak);
534         break;
535     case 3: meterObjectLabel.html(Messages.strGood);
536         break;
537     case 4: meterObjectLabel.html(Messages.strStrong);
538     }
542  * Generate a new password and copy it to the password input areas
544  * @param {object} passwordForm the form that holds the password fields
546  * @return {boolean} always true
547  */
548 Functions.suggestPassword = function (passwordForm) {
549     // restrict the password to just letters and numbers to avoid problems:
550     // "editors and viewers regard the password as multiple words and
551     // things like double click no longer work"
552     var pwchars = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWYXZ@!_.*/()[]-';
553     var passwordlength = 16;    // do we want that to be dynamic?  no, keep it simple :)
554     var passwd = passwordForm.generated_pw;
555     // eslint-disable-next-line compat/compat
556     var randomWords = new Int32Array(passwordlength);
558     passwd.value = '';
560     var i;
562     // First we're going to try to use a built-in CSPRNG
563     // eslint-disable-next-line compat/compat
564     if (window.crypto && window.crypto.getRandomValues) {
565         // eslint-disable-next-line compat/compat
566         window.crypto.getRandomValues(randomWords);
567     } else if (window.msCrypto && window.msCrypto.getRandomValues) {
568         // Because of course IE calls it msCrypto instead of being standard
569         window.msCrypto.getRandomValues(randomWords);
570     } else {
571         // Fallback to Math.random
572         for (i = 0; i < passwordlength; i++) {
573             randomWords[i] = Math.floor(Math.random() * pwchars.length);
574         }
575     }
577     for (i = 0; i < passwordlength; i++) {
578         passwd.value += pwchars.charAt(Math.abs(randomWords[i]) % pwchars.length);
579     }
581     var $jQueryPasswordForm = $(passwordForm);
583     passwordForm.elements.pma_pw.value = passwd.value;
584     passwordForm.elements.pma_pw2.value = passwd.value;
585     var meterObj = $jQueryPasswordForm.find('meter[name="pw_meter"]').first();
586     var meterObjLabel = $jQueryPasswordForm.find('span[name="pw_strength"]').first();
587     var username = '';
588     if (passwordForm.elements.username) {
589         username = passwordForm.elements.username.value;
590     }
591     Functions.checkPasswordStrength(passwd.value, meterObj, meterObjLabel, username);
592     return true;
596  * for PhpMyAdmin\Display\ChangePassword and /user-password
597  */
598 Functions.displayPasswordGenerateButton = function () {
599     var generatePwdRow = $('<tr></tr>').addClass('align-middle');
600     $('<td></td>').html(Messages.strGeneratePassword).appendTo(generatePwdRow);
601     var pwdCell = $('<td></td>').appendTo(generatePwdRow);
602     var pwdButton = $('<input>')
603         .attr({ type: 'button', id: 'button_generate_password', value: Messages.strGenerate })
604         .addClass('btn btn-secondary button')
605         .on('click', function () {
606             Functions.suggestPassword(this.form);
607         });
608     var pwdTextbox = $('<input>')
609         .attr({ type: 'text', name: 'generated_pw', id: 'generated_pw' });
610     pwdCell.append(pwdButton).append(pwdTextbox);
612     if (document.getElementById('button_generate_password') === null) {
613         $('#tr_element_before_generate_password').parent().append(generatePwdRow);
614     }
616     var generatePwdDiv = $('<div></div>').addClass('item');
617     $('<label></label>').attr({ for: 'button_generate_password' })
618         .html(Messages.strGeneratePassword + ':')
619         .appendTo(generatePwdDiv);
620     var optionsSpan = $('<span></span>').addClass('options')
621         .appendTo(generatePwdDiv);
622     pwdButton.clone(true).appendTo(optionsSpan);
623     pwdTextbox.clone(true).appendTo(generatePwdDiv);
625     if (document.getElementById('button_generate_password') === null) {
626         $('#div_element_before_generate_password').parent().append(generatePwdDiv);
627     }
631  * selects the content of a given object, f.e. a textarea
633  * @param {object} element   element of which the content will be selected
634  * @param {any | true} lock  variable which holds the lock for this element or true, if no lock exists
635  * @param {boolean} onlyOnce boolean if true this is only done once f.e. only on first focus
636  */
637 Functions.selectContent = function (element, lock, onlyOnce) {
638     if (onlyOnce && onlyOnceElements[element.name]) {
639         return;
640     }
642     onlyOnceElements[element.name] = true;
644     if (lock) {
645         return;
646     }
648     element.select();
652  * Displays a confirmation box before submitting a "DROP/DELETE/ALTER" query.
653  * This function is called while clicking links
655  * @param {object} theLink     the link
656  * @param {object} theSqlQuery the sql query to submit
658  * @return {boolean} whether to run the query or not
659  */
660 Functions.confirmLink = function (theLink, theSqlQuery) {
661     // Confirmation is not required in the configuration file
662     // or browser is Opera (crappy js implementation)
663     if (Messages.strDoYouReally === '' || typeof(window.opera) !== 'undefined') {
664         return true;
665     }
667     var isConfirmed = confirm(Functions.sprintf(Messages.strDoYouReally, theSqlQuery));
668     if (isConfirmed) {
669         if (typeof(theLink.href) !== 'undefined') {
670             theLink.href += CommonParams.get('arg_separator') + 'is_js_confirmed=1';
671         } else if (typeof(theLink.form) !== 'undefined') {
672             theLink.form.action += '?is_js_confirmed=1';
673         }
674     }
676     return isConfirmed;
680  * Confirms a "DROP/DELETE/ALTER" query before
681  * submitting it if required.
682  * This function is called by the 'Functions.checkSqlQuery()' js function.
684  * @param {object} theForm1  the form
685  * @param {string} sqlQuery1 the sql query string
687  * @return {boolean} whether to run the query or not
689  * @see Functions.checkSqlQuery()
690  */
691 Functions.confirmQuery = function (theForm1, sqlQuery1) {
692     // Confirmation is not required in the configuration file
693     if (Messages.strDoYouReally === '') {
694         return true;
695     }
697     // Confirms a "DROP/DELETE/ALTER/TRUNCATE" statement
698     //
699     // TODO: find a way (if possible) to use the parser-analyser
700     // for this kind of verification
701     // For now, I just added a ^ to check for the statement at
702     // beginning of expression
704     var doConfirmRegExp0 = new RegExp('^\\s*DROP\\s+(IF EXISTS\\s+)?(TABLE|PROCEDURE)\\s', 'i');
705     var doConfirmRegExp1 = new RegExp('^\\s*ALTER\\s+TABLE\\s+((`[^`]+`)|([A-Za-z0-9_$]+))\\s+DROP\\s', 'i');
706     var doConfirmRegExp2 = new RegExp('^\\s*DELETE\\s+FROM\\s', 'i');
707     var doConfirmRegExp3 = new RegExp('^\\s*TRUNCATE\\s', 'i');
708     var doConfirmRegExp4 = new RegExp('^(?=.*UPDATE\\b)^((?!WHERE).)*$', 'i');
710     if (doConfirmRegExp0.test(sqlQuery1) ||
711         doConfirmRegExp1.test(sqlQuery1) ||
712         doConfirmRegExp2.test(sqlQuery1) ||
713         doConfirmRegExp3.test(sqlQuery1) ||
714         doConfirmRegExp4.test(sqlQuery1)) {
715         var message;
716         if (sqlQuery1.length > 100) {
717             message = sqlQuery1.substr(0, 100) + '\n    ...';
718         } else {
719             message = sqlQuery1;
720         }
721         var isConfirmed = confirm(Functions.sprintf(Messages.strDoYouReally, message));
722         // statement is confirmed -> update the
723         // "is_js_confirmed" form field so the confirm test won't be
724         // run on the server side and allows to submit the form
725         if (isConfirmed) {
726             theForm1.elements.is_js_confirmed.value = 1;
727             return true;
728         } else {
729             // statement is rejected -> do not submit the form
730             window.focus();
731             return false;
732         } // end if (handle confirm box result)
733     } // end if (display confirm box)
735     return true;
739  * Displays an error message if the user submitted the sql query form with no
740  * sql query, else checks for "DROP/DELETE/ALTER" statements
742  * @param {object} theForm the form
744  * @return {boolean} always false
746  * @see Functions.confirmQuery()
747  */
748 Functions.checkSqlQuery = function (theForm) {
749     // get the textarea element containing the query
750     var sqlQuery;
751     if (codeMirrorEditor) {
752         codeMirrorEditor.save();
753         sqlQuery = codeMirrorEditor.getValue();
754     } else {
755         sqlQuery = theForm.elements.sql_query.value;
756     }
757     var spaceRegExp = new RegExp('\\s+');
758     if (typeof(theForm.elements.sql_file) !== 'undefined' &&
759             theForm.elements.sql_file.value.replace(spaceRegExp, '') !== '') {
760         return true;
761     }
762     if (typeof(theForm.elements.id_bookmark) !== 'undefined' &&
763             (theForm.elements.id_bookmark.value !== null || theForm.elements.id_bookmark.value !== '') &&
764             theForm.elements.id_bookmark.selectedIndex !== 0) {
765         return true;
766     }
767     var result = false;
768     // Checks for "DROP/DELETE/ALTER" statements
769     if (sqlQuery.replace(spaceRegExp, '') !== '') {
770         result = Functions.confirmQuery(theForm, sqlQuery);
771     } else {
772         alert(Messages.strFormEmpty);
773     }
775     if (codeMirrorEditor) {
776         codeMirrorEditor.focus();
777     } else if (codeMirrorInlineEditor) {
778         codeMirrorInlineEditor.focus();
779     }
780     return result;
784  * Check if a form's element is empty.
785  * An element containing only spaces is also considered empty
787  * @param {object} theForm      the form
788  * @param {string} theFieldName the name of the form field to put the focus on
790  * @return {boolean} whether the form field is empty or not
791  */
792 Functions.emptyCheckTheField = function (theForm, theFieldName) {
793     var theField = theForm.elements[theFieldName];
794     var spaceRegExp = new RegExp('\\s+');
795     return theField.value.replace(spaceRegExp, '') === '';
799  * Ensures a value submitted in a form is numeric and is in a range
801  * @param {object} theForm the form
802  * @param {string} theFieldName the name of the form field to check
803  * @param {any} message
804  * @param {number} minimum the minimum authorized value
805  * @param {number} maximum the maximum authorized value
807  * @return {boolean}  whether a valid number has been submitted or not
808  */
809 Functions.checkFormElementInRange = function (theForm, theFieldName, message, minimum, maximum) {
810     var theField         = theForm.elements[theFieldName];
811     var val              = parseInt(theField.value, 10);
812     var min = 0;
813     var max = Number.MAX_VALUE;
815     if (typeof(minimum) !== 'undefined') {
816         min = minimum;
817     }
818     if (typeof(maximum) !== 'undefined' && maximum !== null) {
819         max = maximum;
820     }
822     if (isNaN(val)) {
823         theField.select();
824         alert(Messages.strEnterValidNumber);
825         theField.focus();
826         return false;
827     } else if (val < min || val > max) {
828         theField.select();
829         alert(Functions.sprintf(message, val));
830         theField.focus();
831         return false;
832     } else {
833         theField.value = val;
834     }
835     return true;
838 Functions.checkTableEditForm = function (theForm, fieldsCnt) {
839     // TODO: avoid sending a message if user just wants to add a line
840     // on the form but has not completed at least one field name
842     var atLeastOneField = 0;
843     var i;
844     var elm;
845     var elm2;
846     var elm3;
847     var val;
848     var id;
850     for (i = 0; i < fieldsCnt; i++) {
851         id = '#field_' + i + '_2';
852         elm = $(id);
853         val = elm.val();
854         if (val === 'VARCHAR' || val === 'CHAR' || val === 'BIT' || val === 'VARBINARY' || val === 'BINARY') {
855             elm2 = $('#field_' + i + '_3');
856             val = parseInt(elm2.val(), 10);
857             elm3 = $('#field_' + i + '_1');
858             if (isNaN(val) && elm3.val() !== '') {
859                 elm2.select();
860                 alert(Messages.strEnterValidLength);
861                 elm2.focus();
862                 return false;
863             }
864         }
866         if (atLeastOneField === 0) {
867             id = 'field_' + i + '_1';
868             if (!Functions.emptyCheckTheField(theForm, id)) {
869                 atLeastOneField = 1;
870             }
871         }
872     }
873     if (atLeastOneField === 0) {
874         var theField = theForm.elements.field_0_1;
875         alert(Messages.strFormEmpty);
876         theField.focus();
877         return false;
878     }
880     // at least this section is under jQuery
881     var $input = $('input.textfield[name=\'table\']');
882     if ($input.val() === '') {
883         alert(Messages.strFormEmpty);
884         $input.trigger('focus');
885         return false;
886     }
888     return true;
892  * True if last click is to check a row.
893  */
894 var lastClickChecked = false;
897  * Zero-based index of last clicked row.
898  * Used to handle the shift + click event in the code above.
899  */
900 var lastClickedRow = -1;
903  * Zero-based index of last shift clicked row.
904  */
905 var lastShiftClickedRow = -1;
907 var idleSecondsCounter = 0;
908 var incInterval;
909 var updateTimeout;
910 AJAX.registerTeardown('functions.js', function () {
911     clearTimeout(updateTimeout);
912     clearInterval(incInterval);
913     $(document).off('mousemove');
916 AJAX.registerOnload('functions.js', function () {
917     document.onclick = function () {
918         idleSecondsCounter = 0;
919     };
920     $(document).on('mousemove',function () {
921         idleSecondsCounter = 0;
922     });
923     document.onkeypress = function () {
924         idleSecondsCounter = 0;
925     };
926     function guid () {
927         function s4 () {
928             return Math.floor((1 + Math.random()) * 0x10000)
929                 .toString(16)
930                 .substring(1);
931         }
932         return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
933             s4() + '-' + s4() + s4() + s4();
934     }
936     function SetIdleTime () {
937         idleSecondsCounter++;
938     }
939     function UpdateIdleTime () {
940         var href = 'index.php?route=/';
941         var guid = 'default';
942         if (isStorageSupported('sessionStorage')) {
943             guid = window.sessionStorage.guid;
944         }
945         var params = {
946             'ajax_request' : true,
947             'server' : CommonParams.get('server'),
948             'db' : CommonParams.get('db'),
949             'guid': guid,
950             'access_time': idleSecondsCounter,
951             'check_timeout': 1
952         };
953         $.ajax({
954             type: 'POST',
955             url: href,
956             data: params,
957             success: function (data) {
958                 if (data.success) {
959                     if (CommonParams.get('LoginCookieValidity') - idleSecondsCounter < 0) {
960                         /* There is other active window, let's reset counter */
961                         idleSecondsCounter = 0;
962                     }
963                     var remaining = Math.min(
964                         /* Remaining login validity */
965                         CommonParams.get('LoginCookieValidity') - idleSecondsCounter,
966                         /* Remaining time till session GC */
967                         CommonParams.get('session_gc_maxlifetime')
968                     );
969                     var interval = 1000;
970                     if (remaining > 5) {
971                         // max value for setInterval() function
972                         interval = Math.min((remaining - 1) * 1000, Math.pow(2, 31) - 1);
973                     }
974                     updateTimeout = window.setTimeout(UpdateIdleTime, interval);
975                 } else { // timeout occurred
976                     clearInterval(incInterval);
977                     if (isStorageSupported('sessionStorage')) {
978                         window.sessionStorage.clear();
979                     }
980                     // append the login form on the page, disable all the forms which were not disabled already, close all the open jqueryui modal boxes
981                     if (!$('#modalOverlay').length) {
982                         $('fieldset').not(':disabled').attr('disabled', 'disabled').addClass('disabled_for_expiration');
983                         $('body').append(data.error);
984                         $('.ui-dialog').each(function () {
985                             $('#' + $(this).attr('aria-describedby')).dialog('close');
986                         });
987                         $('#input_username').trigger('focus');
988                     } else {
989                         CommonParams.set('token', data.new_token);
990                         $('input[name=token]').val(data.new_token);
991                     }
992                     idleSecondsCounter = 0;
993                     Functions.handleRedirectAndReload(data);
994                 }
995             }
996         });
997     }
998     if (CommonParams.get('logged_in')) {
999         incInterval = window.setInterval(SetIdleTime, 1000);
1000         var sessionTimeout = Math.min(
1001             CommonParams.get('LoginCookieValidity'),
1002             CommonParams.get('session_gc_maxlifetime')
1003         );
1004         if (isStorageSupported('sessionStorage')) {
1005             window.sessionStorage.setItem('guid', guid());
1006         }
1007         var interval = (sessionTimeout - 5) * 1000;
1008         if (interval > Math.pow(2, 31) - 1) { // max value for setInterval() function
1009             interval = Math.pow(2, 31) - 1;
1010         }
1011         updateTimeout = window.setTimeout(UpdateIdleTime, interval);
1012     }
1015  * Unbind all event handlers before tearing down a page
1016  */
1017 AJAX.registerTeardown('functions.js', function () {
1018     $(document).off('click', 'input:checkbox.checkall');
1021 AJAX.registerOnload('functions.js', function () {
1022     /**
1023      * Row marking in horizontal mode (use "on" so that it works also for
1024      * next pages reached via AJAX); a tr may have the class noclick to remove
1025      * this behavior.
1026      */
1028     $(document).on('click', 'input:checkbox.checkall', function (e) {
1029         var $this = $(this);
1030         var $tr = $this.closest('tr');
1031         var $table = $this.closest('table');
1033         if (!e.shiftKey || lastClickedRow === -1) {
1034             // usual click
1036             var $checkbox = $tr.find(':checkbox.checkall');
1037             var checked = $this.prop('checked');
1038             $checkbox.prop('checked', checked).trigger('change');
1039             if (checked) {
1040                 $tr.addClass('marked table-active');
1041             } else {
1042                 $tr.removeClass('marked table-active');
1043             }
1044             lastClickChecked = checked;
1046             // remember the last clicked row
1047             lastClickedRow = lastClickChecked ? $table.find('tbody tr:not(.noclick)').index($tr) : -1;
1048             lastShiftClickedRow = -1;
1049         } else {
1050             // handle the shift click
1051             Functions.clearSelection();
1052             var start;
1053             var end;
1055             // clear last shift click result
1056             if (lastShiftClickedRow >= 0) {
1057                 if (lastShiftClickedRow >= lastClickedRow) {
1058                     start = lastClickedRow;
1059                     end = lastShiftClickedRow;
1060                 } else {
1061                     start = lastShiftClickedRow;
1062                     end = lastClickedRow;
1063                 }
1064                 $tr.parent().find('tr:not(.noclick)')
1065                     .slice(start, end + 1)
1066                     .removeClass('marked table-active')
1067                     .find(':checkbox')
1068                     .prop('checked', false)
1069                     .trigger('change');
1070             }
1072             // handle new shift click
1073             var currRow = $table.find('tbody tr:not(.noclick)').index($tr);
1074             if (currRow >= lastClickedRow) {
1075                 start = lastClickedRow;
1076                 end = currRow;
1077             } else {
1078                 start = currRow;
1079                 end = lastClickedRow;
1080             }
1081             $tr.parent().find('tr:not(.noclick)')
1082                 .slice(start, end + 1)
1083                 .addClass('marked table-active')
1084                 .find(':checkbox')
1085                 .prop('checked', true)
1086                 .trigger('change');
1088             // remember the last shift clicked row
1089             lastShiftClickedRow = currRow;
1090         }
1091     });
1093     Functions.addDateTimePicker();
1095     /**
1096      * Add attribute to text boxes for iOS devices (based on bugID: 3508912)
1097      */
1098     if (navigator.userAgent.match(/(iphone|ipod|ipad)/i)) {
1099         $('input[type=text]').attr('autocapitalize', 'off').attr('autocorrect', 'off');
1100     }
1104   * Checks/unchecks all options of a <select> element
1105   *
1106   * @param {string} theForm   the form name
1107   * @param {string} theSelect the element name
1108   * @param {boolean} doCheck  whether to check or to uncheck options
1109   *
1110   * @return {boolean} always true
1111   */
1112 Functions.setSelectOptions = function (theForm, theSelect, doCheck) {
1113     $('form[name=\'' + theForm + '\'] select[name=\'' + theSelect + '\']').find('option').prop('selected', doCheck);
1114     return true;
1118  * Sets current value for query box.
1119  * @param {string} query
1120  * @return {void}
1121  */
1122 Functions.setQuery = function (query) {
1123     if (codeMirrorEditor) {
1124         codeMirrorEditor.setValue(query);
1125         codeMirrorEditor.focus();
1126     } else if (document.sqlform) {
1127         document.sqlform.sql_query.value = query;
1128         document.sqlform.sql_query.focus();
1129     }
1133  * Handles 'Simulate query' button on SQL query box.
1135  * @return {void}
1136  */
1137 Functions.handleSimulateQueryButton = function () {
1138     var updateRegExp = /^\s*UPDATE\b\s*(((`([^`]|``)+`)|([a-z0-9_$]+))\s*\.\s*)?((`([^`]|``)+`)|([a-z0-9_$]+))\s*\bSET\b/i;
1139     var deleteRegExp = /^\s*DELETE\b\s*((((`([^`]|``)+`)|([a-z0-9_$]+))\s*\.\s*)?((`([^`]|``)+`)|([a-z0-9_$]+))\s*)?\bFROM\b/i;
1141     var query = codeMirrorEditor ? codeMirrorEditor.getValue() : $('#sqlquery').val();
1143     var $simulateDml = $('#simulate_dml');
1144     if (updateRegExp.test(query) || deleteRegExp.test(query)) {
1145         if (! $simulateDml.length) {
1146             $('#button_submit_query')
1147                 .before('<input type="button" id="simulate_dml"' +
1148                 'tabindex="199" class="btn btn-primary" value="' +
1149                 Messages.strSimulateDML +
1150                 '">');
1151         }
1152     } else {
1153         if ($simulateDml.length) {
1154             $simulateDml.remove();
1155         }
1156     }
1160   * Create quick sql statements.
1161   *
1162   * @param {'clear'|'format'|'saved'|'selectall'|'select'|'insert'|'update'|'delete'} queryType
1163   *
1164   */
1165 Functions.insertQuery = function (queryType) {
1166     var table;
1167     if (queryType === 'clear') {
1168         Functions.setQuery('');
1169         return;
1170     } else if (queryType === 'format') {
1171         if (codeMirrorEditor) {
1172             $('#querymessage').html(Messages.strFormatting +
1173                 '&nbsp;<img class="ajaxIcon" src="' +
1174                 themeImagePath + 'ajax_clock_small.gif" alt="">');
1175             var params = {
1176                 'ajax_request': true,
1177                 'sql': codeMirrorEditor.getValue(),
1178                 'server': CommonParams.get('server')
1179             };
1180             $.ajax({
1181                 type: 'POST',
1182                 url: 'index.php?route=/database/sql/format',
1183                 data: params,
1184                 success: function (data) {
1185                     if (data.success) {
1186                         codeMirrorEditor.setValue(data.sql);
1187                     }
1188                     $('#querymessage').html('');
1189                 },
1190                 error: function () {
1191                     $('#querymessage').html('');
1192                 }
1193             });
1194         }
1195         return;
1196     } else if (queryType === 'saved') {
1197         var db = $('input[name="db"]').val();
1198         table = $('input[name="table"]').val();
1199         var key = db;
1200         if (table !== undefined) {
1201             key += '.' + table;
1202         }
1203         key = 'autoSavedSql_' + key;
1204         if (isStorageSupported('localStorage') &&
1205             typeof window.localStorage.getItem(key) === 'string') {
1206             Functions.setQuery(window.localStorage.getItem(key));
1207         } else if (Cookies.get(key)) {
1208             Functions.setQuery(Cookies.get(key));
1209         } else {
1210             Functions.ajaxShowMessage(Messages.strNoAutoSavedQuery);
1211         }
1212         return;
1213     }
1215     var query = '';
1216     var myListBox = document.sqlform.dummy;
1217     table = document.sqlform.table.value;
1219     if (myListBox.options.length > 0) {
1220         sqlBoxLocked = true;
1221         var columnsList = '';
1222         var valDis = '';
1223         var editDis = '';
1224         var NbSelect = 0;
1225         for (var i = 0; i < myListBox.options.length; i++) {
1226             NbSelect++;
1227             if (NbSelect > 1) {
1228                 columnsList += ', ';
1229                 valDis += ',';
1230                 editDis += ',';
1231             }
1232             columnsList += myListBox.options[i].value;
1233             valDis += '\'[value-' + NbSelect + ']\'';
1234             editDis += myListBox.options[i].value + '=\'[value-' + NbSelect + ']\'';
1235         }
1236         if (queryType === 'selectall') {
1237             query = 'SELECT * FROM `' + table + '` WHERE 1';
1238         } else if (queryType === 'select') {
1239             query = 'SELECT ' + columnsList + ' FROM `' + table + '` WHERE 1';
1240         } else if (queryType === 'insert') {
1241             query = 'INSERT INTO `' + table + '`(' + columnsList + ') VALUES (' + valDis + ')';
1242         } else if (queryType === 'update') {
1243             query = 'UPDATE `' + table + '` SET ' + editDis + ' WHERE 1';
1244         } else if (queryType === 'delete') {
1245             query = 'DELETE FROM `' + table + '` WHERE 0';
1246         }
1247         Functions.setQuery(query);
1248         sqlBoxLocked = false;
1249     }
1253   * Inserts multiple fields.
1254   *
1255   */
1256 Functions.insertValueQuery = function () {
1257     var myQuery = document.sqlform.sql_query;
1258     var myListBox = document.sqlform.dummy;
1260     if (myListBox.options.length > 0) {
1261         sqlBoxLocked = true;
1262         var columnsList = '';
1263         var NbSelect = 0;
1264         for (var i = 0; i < myListBox.options.length; i++) {
1265             if (myListBox.options[i].selected) {
1266                 NbSelect++;
1267                 if (NbSelect > 1) {
1268                     columnsList += ', ';
1269                 }
1270                 columnsList += myListBox.options[i].value;
1271             }
1272         }
1274         /* CodeMirror support */
1275         if (codeMirrorEditor) {
1276             codeMirrorEditor.replaceSelection(columnsList);
1277             codeMirrorEditor.focus();
1278         // IE support
1279         } else if (document.selection) {
1280             myQuery.focus();
1281             var sel = document.selection.createRange();
1282             sel.text = columnsList;
1283         // MOZILLA/NETSCAPE support
1284         } else if (document.sqlform.sql_query.selectionStart || document.sqlform.sql_query.selectionStart === '0') {
1285             var startPos = document.sqlform.sql_query.selectionStart;
1286             var endPos = document.sqlform.sql_query.selectionEnd;
1287             var SqlString = document.sqlform.sql_query.value;
1289             myQuery.value = SqlString.substring(0, startPos) + columnsList + SqlString.substring(endPos, SqlString.length);
1290             myQuery.focus();
1291         } else {
1292             myQuery.value += columnsList;
1293         }
1295         // eslint-disable-next-line no-unused-vars
1296         sqlBoxLocked = false;
1297     }
1301  * Updates the input fields for the parameters based on the query
1302  */
1303 Functions.updateQueryParameters = function () {
1304     if ($('#parameterized').is(':checked')) {
1305         var query = codeMirrorEditor ? codeMirrorEditor.getValue() : $('#sqlquery').val();
1307         var allParameters = query.match(/:[a-zA-Z0-9_]+/g);
1308         var parameters = [];
1309         // get unique parameters
1310         if (allParameters) {
1311             $.each(allParameters, function (i, parameter) {
1312                 if ($.inArray(parameter, parameters) === -1) {
1313                     parameters.push(parameter);
1314                 }
1315             });
1316         } else {
1317             $('#parametersDiv').text(Messages.strNoParam);
1318             return;
1319         }
1321         var $temp = $('<div></div>');
1322         $temp.append($('#parametersDiv').children());
1323         $('#parametersDiv').empty();
1325         $.each(parameters, function (i, parameter) {
1326             var paramName = parameter.substring(1);
1327             var $param = $temp.find('#paramSpan_' + paramName);
1328             if (! $param.length) {
1329                 $param = $('<span class="parameter" id="paramSpan_' + paramName + '"></span>');
1330                 $('<label for="param_' + paramName + '"></label>').text(parameter).appendTo($param);
1331                 $('<input type="text" name="parameters[' + parameter + ']" id="param_' + paramName + '">').appendTo($param);
1332             }
1333             $('#parametersDiv').append($param);
1334         });
1335     } else {
1336         $('#parametersDiv').empty();
1337     }
1341  * Get checkbox for foreign key checks
1343  * @return {string}
1344  */
1345 Functions.getForeignKeyCheckboxLoader = function () {
1346     var html = '';
1347     html    += '<div class="mt-1 mb-2">';
1348     html    += '<div class="load-default-fk-check-value">';
1349     html    += Functions.getImage('ajax_clock_small');
1350     html    += '</div>';
1351     html    += '</div>';
1352     return html;
1355 Functions.loadForeignKeyCheckbox = function () {
1356     // Load default foreign key check value
1357     var params = {
1358         'ajax_request': true,
1359         'server': CommonParams.get('server'),
1360     };
1361     $.get('index.php?route=/sql/get-default-fk-check-value', params, function (data) {
1362         var html = '<input type="hidden" name="fk_checks" value="0">' +
1363             '<input type="checkbox" name="fk_checks" id="fk_checks"' +
1364             (data.default_fk_check_value ? ' checked="checked"' : '') + '>' +
1365             '<label for="fk_checks">' + Messages.strForeignKeyCheck + '</label>';
1366         $('.load-default-fk-check-value').replaceWith(html);
1367     });
1370 Functions.getJsConfirmCommonParam = function (elem, parameters) {
1371     var $elem = $(elem);
1372     var params = parameters;
1373     var sep = CommonParams.get('arg_separator');
1374     if (params) {
1375         // Strip possible leading ?
1376         if (params.substring(0,1) === '?') {
1377             params = params.substr(1);
1378         }
1379         params += sep;
1380     } else {
1381         params = '';
1382     }
1383     params += 'is_js_confirmed=1' + sep + 'ajax_request=true' + sep + 'fk_checks=' + ($elem.find('#fk_checks').is(':checked') ? 1 : 0);
1384     return params;
1388  * Unbind all event handlers before tearing down a page
1389  */
1390 AJAX.registerTeardown('functions.js', function () {
1391     $(document).off('click', 'a.inline_edit_sql');
1392     $(document).off('click', 'input#sql_query_edit_save');
1393     $(document).off('click', 'input#sql_query_edit_discard');
1394     $('input.sqlbutton').off('click');
1395     if (codeMirrorEditor) {
1396         codeMirrorEditor.off('blur');
1397     } else {
1398         $(document).off('blur', '#sqlquery');
1399     }
1400     $(document).off('change', '#parameterized');
1401     $(document).off('click', 'input.sqlbutton');
1402     $('#sqlquery').off('keydown');
1403     $('#sql_query_edit').off('keydown');
1405     if (codeMirrorInlineEditor) {
1406         // Copy the sql query to the text area to preserve it.
1407         $('#sql_query_edit').text(codeMirrorInlineEditor.getValue());
1408         $(codeMirrorInlineEditor.getWrapperElement()).off('keydown');
1409         codeMirrorInlineEditor.toTextArea();
1410         codeMirrorInlineEditor = false;
1411     }
1412     if (codeMirrorEditor) {
1413         $(codeMirrorEditor.getWrapperElement()).off('keydown');
1414     }
1418  * Jquery Coding for inline editing SQL_QUERY
1419  */
1420 AJAX.registerOnload('functions.js', function () {
1421     // If we are coming back to the page by clicking forward button
1422     // of the browser, bind the code mirror to inline query editor.
1423     Functions.bindCodeMirrorToInlineEditor();
1424     $(document).on('click', 'a.inline_edit_sql', function () {
1425         if ($('#sql_query_edit').length) {
1426             // An inline query editor is already open,
1427             // we don't want another copy of it
1428             return false;
1429         }
1431         var $form = $(this).prev('form');
1432         var sqlQuery  = $form.find('input[name=\'sql_query\']').val().trim();
1433         var $innerSql = $(this).parent().prev().find('code.sql');
1435         var newContent = '<textarea name="sql_query_edit" id="sql_query_edit">' + Functions.escapeHtml(sqlQuery) + '</textarea>\n';
1436         newContent    += Functions.getForeignKeyCheckboxLoader();
1437         newContent    += '<input type="submit" id="sql_query_edit_save" class="btn btn-secondary button btnSave" value="' + Messages.strGo + '">\n';
1438         newContent    += '<input type="button" id="sql_query_edit_discard" class="btn btn-secondary button btnDiscard" value="' + Messages.strCancel + '">\n';
1439         var $editorArea = $('div#inline_editor');
1440         if ($editorArea.length === 0) {
1441             $editorArea = $('<div id="inline_editor_outer"></div>');
1442             $editorArea.insertBefore($innerSql);
1443         }
1444         $editorArea.html(newContent);
1445         Functions.loadForeignKeyCheckbox();
1446         $innerSql.hide();
1448         Functions.bindCodeMirrorToInlineEditor();
1449         return false;
1450     });
1452     $(document).on('click', 'input#sql_query_edit_save', function () {
1453         // hide already existing success message
1454         var sqlQuery;
1455         if (codeMirrorInlineEditor) {
1456             codeMirrorInlineEditor.save();
1457             sqlQuery = codeMirrorInlineEditor.getValue();
1458         } else {
1459             sqlQuery = $(this).parent().find('#sql_query_edit').val();
1460         }
1461         var fkCheck = $(this).parent().find('#fk_checks').is(':checked');
1463         var $form = $('a.inline_edit_sql').prev('form');
1464         var $fakeForm = $('<form>', { action: 'index.php?route=/import', method: 'post' })
1465             .append($form.find('input[name=server], input[name=db], input[name=table], input[name=token]').clone())
1466             .append($('<input>', { type: 'hidden', name: 'show_query', value: 1 }))
1467             .append($('<input>', { type: 'hidden', name: 'is_js_confirmed', value: 0 }))
1468             .append($('<input>', { type: 'hidden', name: 'sql_query', value: sqlQuery }))
1469             .append($('<input>', { type: 'hidden', name: 'fk_checks', value: fkCheck ? 1 : 0 }));
1470         if (! Functions.checkSqlQuery($fakeForm[0])) {
1471             return false;
1472         }
1473         $('.alert-success').hide();
1474         $fakeForm.appendTo($('body')).trigger('submit');
1475     });
1477     $(document).on('click', 'input#sql_query_edit_discard', function () {
1478         var $divEditor = $('div#inline_editor_outer');
1479         $divEditor.siblings('code.sql').show();
1480         $divEditor.remove();
1481     });
1483     $(document).on('click', 'input.sqlbutton', function (evt) {
1484         Functions.insertQuery(evt.target.id);
1485         Functions.handleSimulateQueryButton();
1486         return false;
1487     });
1489     $(document).on('change', '#parameterized', Functions.updateQueryParameters);
1491     var $inputUsername = $('#input_username');
1492     if ($inputUsername) {
1493         if ($inputUsername.val() === '') {
1494             $inputUsername.trigger('focus');
1495         } else {
1496             $('#input_password').trigger('focus');
1497         }
1498     }
1502  * "inputRead" event handler for CodeMirror SQL query editors for autocompletion
1503  * @param instance
1504  */
1505 Functions.codeMirrorAutoCompleteOnInputRead = function (instance) {
1506     if (!sqlAutoCompleteInProgress
1507         && (!instance.options.hintOptions.tables || !sqlAutoComplete)) {
1508         if (!sqlAutoComplete) {
1509             // Reset after teardown
1510             instance.options.hintOptions.tables = false;
1511             instance.options.hintOptions.defaultTable = '';
1513             sqlAutoCompleteInProgress = true;
1515             var params = {
1516                 'ajax_request': true,
1517                 'server': CommonParams.get('server'),
1518                 'db': CommonParams.get('db'),
1519                 'no_debug': true
1520             };
1522             var columnHintRender = function (elem, self, data) {
1523                 $('<div class="autocomplete-column-name">')
1524                     .text(data.columnName)
1525                     .appendTo(elem);
1526                 $('<div class="autocomplete-column-hint">')
1527                     .text(data.columnHint)
1528                     .appendTo(elem);
1529             };
1531             $.ajax({
1532                 type: 'POST',
1533                 url: 'index.php?route=/database/sql/autocomplete',
1534                 data: params,
1535                 success: function (data) {
1536                     if (data.success) {
1537                         var tables = JSON.parse(data.tables);
1538                         sqlAutoCompleteDefaultTable = CommonParams.get('table');
1539                         sqlAutoComplete = [];
1540                         for (var table in tables) {
1541                             if (tables.hasOwnProperty(table)) {
1542                                 var columns = tables[table];
1543                                 table = {
1544                                     text: table,
1545                                     columns: []
1546                                 };
1547                                 for (var column in columns) {
1548                                     if (columns.hasOwnProperty(column)) {
1549                                         var displayText = columns[column].Type;
1550                                         if (columns[column].Key === 'PRI') {
1551                                             displayText += ' | Primary';
1552                                         } else if (columns[column].Key === 'UNI') {
1553                                             displayText += ' | Unique';
1554                                         }
1555                                         table.columns.push({
1556                                             text: column,
1557                                             displayText: column + ' | ' +  displayText,
1558                                             columnName: column,
1559                                             columnHint: displayText,
1560                                             render: columnHintRender
1561                                         });
1562                                     }
1563                                 }
1564                             }
1565                             sqlAutoComplete.push(table);
1566                         }
1567                         instance.options.hintOptions.tables = sqlAutoComplete;
1568                         instance.options.hintOptions.defaultTable = sqlAutoCompleteDefaultTable;
1569                     }
1570                 },
1571                 complete: function () {
1572                     sqlAutoCompleteInProgress = false;
1573                 }
1574             });
1575         } else {
1576             instance.options.hintOptions.tables = sqlAutoComplete;
1577             instance.options.hintOptions.defaultTable = sqlAutoCompleteDefaultTable;
1578         }
1579     }
1580     if (instance.state.completionActive) {
1581         return;
1582     }
1583     var cur = instance.getCursor();
1584     var token = instance.getTokenAt(cur);
1585     var string = '';
1586     if (token.string.match(/^[.`\w@]\w*$/)) {
1587         string = token.string;
1588     }
1589     if (string.length > 0) {
1590         CodeMirror.commands.autocomplete(instance);
1591     }
1595  * Remove autocomplete information before tearing down a page
1596  */
1597 AJAX.registerTeardown('functions.js', function () {
1598     sqlAutoComplete = false;
1599     sqlAutoCompleteDefaultTable = '';
1603  * Binds the CodeMirror to the text area used to inline edit a query.
1604  */
1605 Functions.bindCodeMirrorToInlineEditor = function () {
1606     var $inlineEditor = $('#sql_query_edit');
1607     if ($inlineEditor.length > 0) {
1608         if (typeof CodeMirror !== 'undefined') {
1609             var height = $inlineEditor.css('height');
1610             codeMirrorInlineEditor = Functions.getSqlEditor($inlineEditor);
1611             codeMirrorInlineEditor.getWrapperElement().style.height = height;
1612             codeMirrorInlineEditor.refresh();
1613             codeMirrorInlineEditor.focus();
1614             $(codeMirrorInlineEditor.getWrapperElement())
1615                 .on('keydown', Functions.catchKeypressesFromSqlInlineEdit);
1616         } else {
1617             $inlineEditor
1618                 .trigger('focus')
1619                 .on('keydown', Functions.catchKeypressesFromSqlInlineEdit);
1620         }
1621     }
1624 Functions.catchKeypressesFromSqlInlineEdit = function (event) {
1625     // ctrl-enter is 10 in chrome and ie, but 13 in ff
1626     if ((event.ctrlKey || event.metaKey) && (event.keyCode === 13 || event.keyCode === 10)) {
1627         $('#sql_query_edit_save').trigger('click');
1628     }
1632  * Adds doc link to single highlighted SQL element
1634  * @param $elm
1635  * @param params
1636  */
1637 Functions.documentationAdd = function ($elm, params) {
1638     if (typeof mysqlDocTemplate === 'undefined') {
1639         return;
1640     }
1642     var url = Functions.sprintf(
1643         decodeURIComponent(mysqlDocTemplate),
1644         params[0]
1645     );
1646     if (params.length > 1) {
1647         // The # needs to be escaped to be part of the destination URL
1648         url += encodeURIComponent('#') + params[1];
1649     }
1650     var content = $elm.text();
1651     $elm.text('');
1652     $elm.append('<a target="mysql_doc" class="cm-sql-doc" href="' + url + '">' + content + '</a>');
1656  * Generates doc links for keywords inside highlighted SQL
1658  * @param idx
1659  * @param elm
1660  */
1661 Functions.documentationKeyword = function (idx, elm) {
1662     var $elm = $(elm);
1663     /* Skip already processed ones */
1664     if ($elm.find('a').length > 0) {
1665         return;
1666     }
1667     var keyword = $elm.text().toUpperCase();
1668     var $next = $elm.next('.cm-keyword');
1669     if ($next) {
1670         var nextKeyword = $next.text().toUpperCase();
1671         var full = keyword + ' ' + nextKeyword;
1673         var $next2 = $next.next('.cm-keyword');
1674         if ($next2) {
1675             var next2Keyword = $next2.text().toUpperCase();
1676             var full2 = full + ' ' + next2Keyword;
1677             if (full2 in mysqlDocKeyword) {
1678                 Functions.documentationAdd($elm, mysqlDocKeyword[full2]);
1679                 Functions.documentationAdd($next, mysqlDocKeyword[full2]);
1680                 Functions.documentationAdd($next2, mysqlDocKeyword[full2]);
1681                 return;
1682             }
1683         }
1684         if (full in mysqlDocKeyword) {
1685             Functions.documentationAdd($elm, mysqlDocKeyword[full]);
1686             Functions.documentationAdd($next, mysqlDocKeyword[full]);
1687             return;
1688         }
1689     }
1690     if (keyword in mysqlDocKeyword) {
1691         Functions.documentationAdd($elm, mysqlDocKeyword[keyword]);
1692     }
1696  * Generates doc links for builtins inside highlighted SQL
1698  * @param idx
1699  * @param elm
1700  */
1701 Functions.documentationBuiltin = function (idx, elm) {
1702     var $elm = $(elm);
1703     var builtin = $elm.text().toUpperCase();
1704     if (builtin in mysqlDocBuiltin) {
1705         Functions.documentationAdd($elm, mysqlDocBuiltin[builtin]);
1706     }
1710  * Higlights SQL using CodeMirror.
1712  * @param $base
1713  */
1714 Functions.highlightSql = function ($base) {
1715     var $elm = $base.find('code.sql');
1716     $elm.each(function () {
1717         var $sql = $(this);
1718         var $pre = $sql.find('pre');
1719         /* We only care about visible elements to avoid double processing */
1720         if ($pre.is(':visible')) {
1721             var $highlight = $('<div class="sql-highlight cm-s-default"></div>');
1722             $sql.append($highlight);
1723             if (typeof CodeMirror !== 'undefined') {
1724                 CodeMirror.runMode($sql.text(), 'text/x-mysql', $highlight[0]);
1725                 $pre.hide();
1726                 $highlight.find('.cm-keyword').each(Functions.documentationKeyword);
1727                 $highlight.find('.cm-builtin').each(Functions.documentationBuiltin);
1728             }
1729         }
1730     });
1734  * Updates an element containing code.
1736  * @param {JQuery} $base     base element which contains the raw and the
1737  *                           highlighted code.
1739  * @param {string} htmlValue code in HTML format, displayed if code cannot be
1740  *                           highlighted
1742  * @param {string} rawValue  raw code, used as a parameter for highlighter
1744  * @return {boolean}        whether content was updated or not
1745  */
1746 Functions.updateCode = function ($base, htmlValue, rawValue) {
1747     var $code = $base.find('code');
1748     if ($code.length === 0) {
1749         return false;
1750     }
1752     // Determines the type of the content and appropriate CodeMirror mode.
1753     var type = '';
1754     var mode = '';
1755     if  ($code.hasClass('json')) {
1756         type = 'json';
1757         mode = 'application/json';
1758     } else if ($code.hasClass('sql')) {
1759         type = 'sql';
1760         mode = 'text/x-mysql';
1761     } else if ($code.hasClass('xml')) {
1762         type = 'xml';
1763         mode = 'application/xml';
1764     } else {
1765         return false;
1766     }
1768     // Element used to display unhighlighted code.
1769     var $notHighlighted = $('<pre>' + htmlValue + '</pre>');
1771     // Tries to highlight code using CodeMirror.
1772     if (typeof CodeMirror !== 'undefined') {
1773         var $highlighted = $('<div class="' + type + '-highlight cm-s-default"></div>');
1774         CodeMirror.runMode(rawValue, mode, $highlighted[0]);
1775         $notHighlighted.hide();
1776         $code.html('').append($notHighlighted, $highlighted[0]);
1777     } else {
1778         $code.html('').append($notHighlighted);
1779     }
1781     return true;
1785  * Show a message on the top of the page for an Ajax request
1787  * Sample usage:
1789  * 1) var $msg = Functions.ajaxShowMessage();
1790  * This will show a message that reads "Loading...". Such a message will not
1791  * disappear automatically and cannot be dismissed by the user. To remove this
1792  * message either the Functions.ajaxRemoveMessage($msg) function must be called or
1793  * another message must be show with Functions.ajaxShowMessage() function.
1795  * 2) var $msg = Functions.ajaxShowMessage(Messages.strProcessingRequest);
1796  * This is a special case. The behaviour is same as above,
1797  * just with a different message
1799  * 3) var $msg = Functions.ajaxShowMessage('The operation was successful');
1800  * This will show a message that will disappear automatically and it can also
1801  * be dismissed by the user.
1803  * 4) var $msg = Functions.ajaxShowMessage('Some error', false);
1804  * This will show a message that will not disappear automatically, but it
1805  * can be dismissed by the user after they have finished reading it.
1807  * @param {string} message      string containing the message to be shown.
1808  *                              optional, defaults to 'Loading...'
1809  * @param {any} timeout         number of milliseconds for the message to be visible
1810  *                              optional, defaults to 5000. If set to 'false', the
1811  *                              notification will never disappear
1812  * @param {string} type         string to dictate the type of message shown.
1813  *                              optional, defaults to normal notification.
1814  *                              If set to 'error', the notification will show message
1815  *                              with red background.
1816  *                              If set to 'success', the notification will show with
1817  *                              a green background.
1818  * @return {JQuery<Element>}   jQuery Element that holds the message div
1819  *                              this object can be passed to Functions.ajaxRemoveMessage()
1820  *                              to remove the notification
1821  */
1822 Functions.ajaxShowMessage = function (message, timeout, type) {
1823     var msg = message;
1824     var newTimeOut = timeout;
1825     /**
1826      * @var self_closing Whether the notification will automatically disappear
1827      */
1828     var selfClosing = true;
1829     /**
1830      * @var dismissable Whether the user will be able to remove
1831      *                  the notification by clicking on it
1832      */
1833     var dismissable = true;
1834     // Handle the case when a empty data.message is passed.
1835     // We don't want the empty message
1836     if (msg === '') {
1837         return true;
1838     } else if (! msg) {
1839         // If the message is undefined, show the default
1840         msg = Messages.strLoading;
1841         dismissable = false;
1842         selfClosing = false;
1843     } else if (msg === Messages.strProcessingRequest) {
1844         // This is another case where the message should not disappear
1845         dismissable = false;
1846         selfClosing = false;
1847     }
1848     // Figure out whether (or after how long) to remove the notification
1849     if (newTimeOut === undefined || newTimeOut === null) {
1850         newTimeOut = 5000;
1851     } else if (newTimeOut === false) {
1852         selfClosing = false;
1853     }
1854     // Determine type of message, add styling as required
1855     if (type === 'error') {
1856         msg = '<div class="alert alert-danger" role="alert">' + msg + '</div>';
1857     } else if (type === 'success') {
1858         msg = '<div class="alert alert-success" role="alert">' + msg + '</div>';
1859     }
1860     // Create a parent element for the AJAX messages, if necessary
1861     if ($('#loading_parent').length === 0) {
1862         $('<div id="loading_parent"></div>')
1863             .prependTo('#page_content');
1864     }
1865     // Update message count to create distinct message elements every time
1866     ajaxMessageCount++;
1867     // Remove all old messages, if any
1868     $('span.ajax_notification[id^=ajax_message_num]').remove();
1869     /**
1870      * @var $retval    a jQuery object containing the reference
1871      *                 to the created AJAX message
1872      */
1873     var $retval = $(
1874         '<span class="ajax_notification" id="ajax_message_num_' +
1875             ajaxMessageCount +
1876             '"></span>'
1877     )
1878         .hide()
1879         .appendTo('#loading_parent')
1880         .html(msg)
1881         .show();
1882     // If the notification is self-closing we should create a callback to remove it
1883     if (selfClosing) {
1884         $retval
1885             .delay(newTimeOut)
1886             .fadeOut('medium', function () {
1887                 if ($(this).is(':data(tooltip)')) {
1888                     $(this).uiTooltip('destroy');
1889                 }
1890                 // Remove the notification
1891                 $(this).remove();
1892             });
1893     }
1894     // If the notification is dismissable we need to add the relevant class to it
1895     // and add a tooltip so that the users know that it can be removed
1896     if (dismissable) {
1897         $retval.addClass('dismissable').css('cursor', 'pointer');
1898         /**
1899          * Add a tooltip to the notification to let the user know that they
1900          * can dismiss the ajax notification by clicking on it.
1901          */
1902         Functions.tooltip(
1903             $retval,
1904             'span',
1905             Messages.strDismiss
1906         );
1907     }
1908     // Hide spinner if this is not a loading message
1909     if (msg !== Messages.strLoading) {
1910         $retval.css('background-image', 'none');
1911     }
1912     Functions.highlightSql($retval);
1914     return $retval;
1918  * Removes the message shown for an Ajax operation when it's completed
1920  * @param {JQuery} $thisMessageBox Element that holds the notification
1922  * @return {void}
1923  */
1924 Functions.ajaxRemoveMessage = function ($thisMessageBox) {
1925     if ($thisMessageBox !== undefined && $thisMessageBox instanceof jQuery) {
1926         $thisMessageBox
1927             .stop(true, true)
1928             .fadeOut('medium');
1929         if ($thisMessageBox.is(':data(tooltip)')) {
1930             $thisMessageBox.uiTooltip('destroy');
1931         } else {
1932             $thisMessageBox.remove();
1933         }
1934     }
1938  * Requests SQL for previewing before executing.
1940  * @param {JQuery<HTMLElement>} $form Form containing query data
1942  * @return {void}
1943  */
1944 Functions.previewSql = function ($form) {
1945     var formUrl = $form.attr('action');
1946     var sep = CommonParams.get('arg_separator');
1947     var formData = $form.serialize() +
1948         sep + 'do_save_data=1' +
1949         sep + 'preview_sql=1' +
1950         sep + 'ajax_request=1';
1951     var $messageBox = Functions.ajaxShowMessage();
1952     $.ajax({
1953         type: 'POST',
1954         url: formUrl,
1955         data: formData,
1956         success: function (response) {
1957             Functions.ajaxRemoveMessage($messageBox);
1958             if (response.success) {
1959                 $('#previewSqlModal').modal('show');
1960                 $('#previewSqlModal').find('.modal-body').first().html(response.sql_data);
1961                 $('#previewSqlModalLabel').first().html(Messages.strPreviewSQL);
1962                 $('#previewSqlModal').on('shown.bs.modal', function () {
1963                     Functions.highlightSql($('#previewSqlModal'));
1964                 });
1965             } else {
1966                 Functions.ajaxShowMessage(response.message);
1967             }
1968         },
1969         error: function () {
1970             Functions.ajaxShowMessage(Messages.strErrorProcessingRequest);
1971         }
1972     });
1976  * Callback called when submit/"OK" is clicked on sql preview/confirm modal
1978  * @callback onSubmitCallback
1979  * @param {string} url The url
1980  */
1984  * @param {string}           sqlData  Sql query to preview
1985  * @param {string}           url      Url to be sent to callback
1986  * @param {onSubmitCallback} callback On submit callback function
1988  * @return {void}
1989  */
1990 Functions.confirmPreviewSql = function (sqlData, url, callback) {
1991     $('#previewSqlConfirmModal').modal('show');
1992     $('#previewSqlConfirmModalLabel').first().html(Messages.strPreviewSQL);
1993     $('#previewSqlConfirmCode').first().text(sqlData);
1994     $('#previewSqlConfirmModal').on('shown.bs.modal', function () {
1995         Functions.highlightSql($('#previewSqlConfirmModal'));
1996     });
1997     $('#previewSQLConfirmOkButton').on('click', function () {
1998         callback(url);
1999         $('#previewSqlConfirmModal').modal('hide');
2000     });
2004  * check for reserved keyword column name
2006  * @param {JQuery} $form Form
2008  * @return {boolean}
2009  */
2010 Functions.checkReservedWordColumns = function ($form) {
2011     var isConfirmed = true;
2012     $.ajax({
2013         type: 'POST',
2014         url: 'index.php?route=/table/structure/reserved-word-check',
2015         data: $form.serialize(),
2016         success: function (data) {
2017             if (typeof data.success !== 'undefined' && data.success === true) {
2018                 isConfirmed = confirm(data.message);
2019             }
2020         },
2021         async:false
2022     });
2023     return isConfirmed;
2026 // This event only need to be fired once after the initial page load
2027 $(function () {
2028     /**
2029      * Allows the user to dismiss a notification
2030      * created with Functions.ajaxShowMessage()
2031      */
2032     var holdStarter = null;
2033     $(document).on('mousedown', 'span.ajax_notification.dismissable', function () {
2034         holdStarter = setTimeout(function () {
2035             holdStarter = null;
2036         }, 250);
2037     });
2039     $(document).on('mouseup', 'span.ajax_notification.dismissable', function (event) {
2040         if (holdStarter && event.which === 1) {
2041             clearTimeout(holdStarter);
2042             Functions.ajaxRemoveMessage($(this));
2043         }
2044     });
2045     /**
2046      * The below two functions hide the "Dismiss notification" tooltip when a user
2047      * is hovering a link or button that is inside an ajax message
2048      */
2049     $(document).on('mouseover', 'span.ajax_notification a, span.ajax_notification button, span.ajax_notification input', function () {
2050         if ($(this).parents('span.ajax_notification').is(':data(tooltip)')) {
2051             $(this).parents('span.ajax_notification').uiTooltip('disable');
2052         }
2053     });
2054     $(document).on('mouseout', 'span.ajax_notification a, span.ajax_notification button, span.ajax_notification input', function () {
2055         if ($(this).parents('span.ajax_notification').is(':data(tooltip)')) {
2056             $(this).parents('span.ajax_notification').uiTooltip('enable');
2057         }
2058     });
2060     /**
2061      * Copy text to clipboard
2062      *
2063      * @param {string | number | string[]} text to copy to clipboard
2064      *
2065      * @return {boolean}
2066      */
2067     Functions.copyToClipboard = function (text) {
2068         var $temp = $('<input>');
2069         $temp.css({ 'position': 'fixed', 'width': '2em', 'border': 0, 'top': 0, 'left': 0, 'padding': 0, 'background': 'transparent' });
2070         $('body').append($temp);
2071         $temp.val(text).trigger('select');
2072         try {
2073             var res = document.execCommand('copy');
2074             $temp.remove();
2075             return res;
2076         } catch (e) {
2077             $temp.remove();
2078             return false;
2079         }
2080     };
2082     $(document).on('click', 'a.copyQueryBtn', function (event) {
2083         event.preventDefault();
2084         var res = Functions.copyToClipboard($(this).attr('data-text'));
2085         if (res) {
2086             $(this).after('<span id=\'copyStatus\'> (' + Messages.strCopyQueryButtonSuccess + ')</span>');
2087         } else {
2088             $(this).after('<span id=\'copyStatus\'> (' + Messages.strCopyQueryButtonFailure + ')</span>');
2089         }
2090         setTimeout(function () {
2091             $('#copyStatus').remove();
2092         }, 2000);
2093     });
2097  * Hides/shows the "Open in ENUM/SET editor" message, depending on the data type of the column currently selected
2099  * @param selectElement
2100  */
2101 Functions.showNoticeForEnum = function (selectElement) {
2102     var enumNoticeId = selectElement.attr('id').split('_')[1];
2103     enumNoticeId += '_' + (parseInt(selectElement.attr('id').split('_')[2], 10) + 1);
2104     var selectedType = selectElement.val();
2105     if (selectedType === 'ENUM' || selectedType === 'SET') {
2106         $('p#enum_notice_' + enumNoticeId).show();
2107     } else {
2108         $('p#enum_notice_' + enumNoticeId).hide();
2109     }
2113  * Hides/shows a warning message when LENGTH is used with inappropriate integer type
2114  */
2115 Functions.showWarningForIntTypes = function () {
2116     if ($('div#length_not_allowed').length) {
2117         var lengthRestrictions = $('select.column_type option').map(function () {
2118             return $(this).filter(':selected').attr('data-length-restricted');
2119         }).get();
2121         var restricationFound = lengthRestrictions.some(restriction => Number(restriction) === 1);
2123         if (restricationFound) {
2124             $('div#length_not_allowed').show();
2125         } else {
2126             $('div#length_not_allowed').hide();
2127         }
2128     }
2132  * Creates a Profiling Chart. Used in sql.js
2133  * and in server/status/monitor.js
2135  * @param target
2136  * @param data
2138  * @return {object}
2139  */
2140 Functions.createProfilingChart = function (target, data) {
2141     // create the chart
2142     var factory = new JQPlotChartFactory();
2143     var chart = factory.createChart(ChartType.PIE, target);
2145     // create the data table and add columns
2146     var dataTable = new DataTable();
2147     dataTable.addColumn(ColumnType.STRING, '');
2148     dataTable.addColumn(ColumnType.NUMBER, '');
2149     dataTable.setData(data);
2151     var windowWidth = $(window).width();
2152     var location = 's';
2153     if (windowWidth > 768) {
2154         location = 'se';
2155     }
2157     // draw the chart and return the chart object
2158     chart.draw(dataTable, {
2159         seriesDefaults: {
2160             rendererOptions: {
2161                 showDataLabels:  true
2162             }
2163         },
2164         highlighter: {
2165             tooltipLocation: 'se',
2166             sizeAdjust: 0,
2167             tooltipAxes: 'pieref',
2168             formatString: '%s, %.9Ps'
2169         },
2170         legend: {
2171             show: true,
2172             location: location,
2173             rendererOptions: {
2174                 numberColumns: 2
2175             }
2176         },
2177         // from https://web.archive.org/web/20190321233412/http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines
2178         seriesColors: [
2179             '#fce94f',
2180             '#fcaf3e',
2181             '#e9b96e',
2182             '#8ae234',
2183             '#729fcf',
2184             '#ad7fa8',
2185             '#ef2929',
2186             '#888a85',
2187             '#c4a000',
2188             '#ce5c00',
2189             '#8f5902',
2190             '#4e9a06',
2191             '#204a87',
2192             '#5c3566',
2193             '#a40000',
2194             '#babdb6',
2195             '#2e3436'
2196         ]
2197     });
2198     return chart;
2202  * Formats a profiling duration nicely (in us and ms time).
2203  * Used in server/status/monitor.js
2205  * @param {number} number   Number to be formatted, should be in the range of microsecond to second
2206  * @param {number} accuracy Accuracy, how many numbers right to the comma should be
2207  * @return {string}        The formatted number
2208  */
2209 Functions.prettyProfilingNum = function (number, accuracy) {
2210     var num = number;
2211     var acc = accuracy;
2212     if (!acc) {
2213         acc = 2;
2214     }
2215     acc = Math.pow(10, acc);
2216     if (num * 1000 < 0.1) {
2217         num = Math.round(acc * (num * 1000 * 1000)) / acc + 'µ';
2218     } else if (num < 0.1) {
2219         num = Math.round(acc * (num * 1000)) / acc + 'm';
2220     } else {
2221         num = Math.round(acc * num) / acc;
2222     }
2224     return num + 's';
2228  * Formats a SQL Query nicely with newlines and indentation. Depends on Codemirror and MySQL Mode!
2230  * @param {string} string Query to be formatted
2231  * @return {string}      The formatted query
2232  */
2233 Functions.sqlPrettyPrint = function (string) {
2234     if (typeof CodeMirror === 'undefined') {
2235         return string;
2236     }
2238     var mode = CodeMirror.getMode({}, 'text/x-mysql');
2239     var stream = new CodeMirror.StringStream(string);
2240     var state = mode.startState();
2241     var token;
2242     var tokens = [];
2243     var output = '';
2244     var tabs = function (cnt) {
2245         var ret = '';
2246         for (var i = 0; i < 4 * cnt; i++) {
2247             ret += ' ';
2248         }
2249         return ret;
2250     };
2252     // "root-level" statements
2253     var statements = {
2254         'select': ['select', 'from', 'on', 'where', 'having', 'limit', 'order by', 'group by'],
2255         'update': ['update', 'set', 'where'],
2256         'insert into': ['insert into', 'values']
2257     };
2258     // don't put spaces before these tokens
2259     var spaceExceptionsBefore = { ';': true, ',': true, '.': true, '(': true };
2260     // don't put spaces after these tokens
2261     var spaceExceptionsAfter = { '.': true };
2263     // Populate tokens array
2264     while (! stream.eol()) {
2265         stream.start = stream.pos;
2266         token = mode.token(stream, state);
2267         if (token !== null) {
2268             tokens.push([token, stream.current().toLowerCase()]);
2269         }
2270     }
2272     var currentStatement = tokens[0][1];
2274     if (! statements[currentStatement]) {
2275         return string;
2276     }
2277     // Holds all currently opened code blocks (statement, function or generic)
2278     var blockStack = [];
2279     // If a new code block is found, newBlock contains its type for one iteration and vice versa for endBlock
2280     var newBlock;
2281     var endBlock;
2282     // How much to indent in the current line
2283     var indentLevel = 0;
2284     // Holds the "root-level" statements
2285     var statementPart;
2286     var lastStatementPart = statements[currentStatement][0];
2288     blockStack.unshift('statement');
2290     // Iterate through every token and format accordingly
2291     for (var i = 0; i < tokens.length; i++) {
2292         // New block => push to stack
2293         if (tokens[i][1] === '(') {
2294             if (i < tokens.length - 1 && tokens[i + 1][0] === 'statement-verb') {
2295                 blockStack.unshift(newBlock = 'statement');
2296             } else if (i > 0 && tokens[i - 1][0] === 'builtin') {
2297                 blockStack.unshift(newBlock = 'function');
2298             } else {
2299                 blockStack.unshift(newBlock = 'generic');
2300             }
2301         } else {
2302             newBlock = null;
2303         }
2305         // Block end => pop from stack
2306         if (tokens[i][1] === ')') {
2307             endBlock = blockStack[0];
2308             blockStack.shift();
2309         } else {
2310             endBlock = null;
2311         }
2313         // A subquery is starting
2314         if (i > 0 && newBlock === 'statement') {
2315             indentLevel++;
2316             output += '\n' + tabs(indentLevel) + tokens[i][1] + ' ' + tokens[i + 1][1].toUpperCase() + '\n' + tabs(indentLevel + 1);
2317             currentStatement = tokens[i + 1][1];
2318             i++;
2319             continue;
2320         }
2322         // A subquery is ending
2323         if (endBlock === 'statement' && indentLevel > 0) {
2324             output += '\n' + tabs(indentLevel);
2325             indentLevel--;
2326         }
2328         // One less indentation for statement parts (from, where, order by, etc.) and a newline
2329         statementPart = statements[currentStatement].indexOf(tokens[i][1]);
2330         if (statementPart !== -1) {
2331             if (i > 0) {
2332                 output += '\n';
2333             }
2334             output += tabs(indentLevel) + tokens[i][1].toUpperCase();
2335             output += '\n' + tabs(indentLevel + 1);
2336             lastStatementPart = tokens[i][1];
2337         // Normal indentation and spaces for everything else
2338         } else {
2339             if (! spaceExceptionsBefore[tokens[i][1]] &&
2340                ! (i > 0 && spaceExceptionsAfter[tokens[i - 1][1]]) &&
2341                output.charAt(output.length - 1) !== ' ') {
2342                 output += ' ';
2343             }
2344             if (tokens[i][0] === 'keyword') {
2345                 output += tokens[i][1].toUpperCase();
2346             } else {
2347                 output += tokens[i][1];
2348             }
2349         }
2351         // split columns in select and 'update set' clauses, but only inside statements blocks
2352         if ((lastStatementPart === 'select' || lastStatementPart === 'where'  || lastStatementPart === 'set') &&
2353             tokens[i][1] === ',' && blockStack[0] === 'statement') {
2354             output += '\n' + tabs(indentLevel + 1);
2355         }
2357         // split conditions in where clauses, but only inside statements blocks
2358         if (lastStatementPart === 'where' &&
2359             (tokens[i][1] === 'and' || tokens[i][1] === 'or' || tokens[i][1] === 'xor')) {
2360             if (blockStack[0] === 'statement') {
2361                 output += '\n' + tabs(indentLevel + 1);
2362             }
2363             // Todo: Also split and or blocks in newlines & indentation++
2364             // if (blockStack[0] === 'generic')
2365             //   output += ...
2366         }
2367     }
2368     return output;
2372  * jQuery function that uses jQueryUI's dialogs to confirm with user. Does not
2373  * return a jQuery object yet and hence cannot be chained
2375  * @param {string}   question
2376  * @param {string}   url          URL to be passed to the callbackFn to make
2377  *                                an Ajax call to
2378  * @param {Function} callbackFn   callback to execute after user clicks on OK
2379  * @param {Function} openCallback optional callback to run when dialog is shown
2381  * @return {bool}
2382  */
2383 Functions.confirm = function (question, url, callbackFn, openCallback) {
2384     var confirmState = CommonParams.get('confirm');
2385     if (! confirmState) {
2386         // user does not want to confirm
2387         if (typeof callbackFn === 'function') {
2388             callbackFn.call(this, url);
2389             return true;
2390         }
2391     }
2392     if (Messages.strDoYouReally === '') {
2393         return true;
2394     }
2396     /**
2397      * @var button_options Object that stores the options passed to jQueryUI
2398      *                     dialog
2399      */
2400     var buttonOptions = [
2401         {
2402             text: Messages.strOK,
2403             'class': 'btn btn-primary submitOK',
2404             click: function () {
2405                 $(this).dialog('close');
2406                 if (typeof callbackFn === 'function') {
2407                     callbackFn.call(this, url);
2408                 }
2409             }
2410         },
2411         {
2412             text: Messages.strCancel,
2413             'class': 'btn btn-secondary submitCancel',
2414             click: function () {
2415                 $(this).dialog('close');
2416             }
2417         }
2418     ];
2420     $('<div></div>', { 'id': 'confirm_dialog', 'title': Messages.strConfirm })
2421         .prepend(question)
2422         .dialog({
2423             classes: {
2424                 'ui-dialog-titlebar-close': 'btn-close'
2425             },
2426             buttons: buttonOptions,
2427             close: function () {
2428                 $(this).remove();
2429             },
2430             open: openCallback,
2431             modal: true
2432         });
2434 jQuery.fn.confirm = Functions.confirm;
2437  * jQuery function to sort a table's body after a new row has been appended to it.
2439  * @param {string} textSelector string to select the sortKey's text
2441  * @return {JQuery<HTMLElement>} for chaining purposes
2442  */
2443 Functions.sortTable = function (textSelector) {
2444     return this.each(function () {
2445         /**
2446          * @var table_body  Object referring to the table's <tbody> element
2447          */
2448         var tableBody = $(this);
2449         /**
2450          * @var rows    Object referring to the collection of rows in {@link tableBody}
2451          */
2452         var rows = $(this).find('tr').get();
2454         // get the text of the field that we will sort by
2455         $.each(rows, function (index, row) {
2456             row.sortKey = $(row).find(textSelector).text().toLowerCase().trim();
2457         });
2459         // get the sorted order
2460         rows.sort(function (a, b) {
2461             if (a.sortKey < b.sortKey) {
2462                 return -1;
2463             }
2464             if (a.sortKey > b.sortKey) {
2465                 return 1;
2466             }
2467             return 0;
2468         });
2470         // pull out each row from the table and then append it according to it's order
2471         $.each(rows, function (index, row) {
2472             $(tableBody).append(row);
2473             row.sortKey = null;
2474         });
2475     });
2477 jQuery.fn.sortTable = Functions.sortTable;
2480  * Unbind all event handlers before tearing down a page
2481  */
2482 AJAX.registerTeardown('functions.js', function () {
2483     $(document).off('submit', 'form.create_table_form.ajax');
2484     $(document).off('click', 'form.create_table_form.ajax input[name=submit_num_fields]');
2485     $(document).off('keyup', 'form.create_table_form.ajax input');
2486     $(document).off('change', 'input[name=partition_count],input[name=subpartition_count],select[name=partition_by]');
2490  * jQuery coding for 'Create Table'. Used on /database/operations,
2491  * /database/structure and /database/tracking (i.e., wherever
2492  * PhpMyAdmin\Display\CreateTable is used)
2494  * Attach Ajax Event handlers for Create Table
2495  */
2496 AJAX.registerOnload('functions.js', function () {
2497     /**
2498      * Attach event handler for submission of create table form (save)
2499      */
2500     $(document).on('submit', 'form.create_table_form.ajax', function (event) {
2501         event.preventDefault();
2503         /**
2504          * @var    the_form    object referring to the create table form
2505          */
2506         var $form = $(this);
2508         /*
2509          * First validate the form; if there is a problem, avoid submitting it
2510          *
2511          * Functions.checkTableEditForm() needs a pure element and not a jQuery object,
2512          * this is why we pass $form[0] as a parameter (the jQuery object
2513          * is actually an array of DOM elements)
2514          */
2516         if (Functions.checkTableEditForm($form[0], $form.find('input[name=orig_num_fields]').val())) {
2517             Functions.prepareForAjaxRequest($form);
2518             if (Functions.checkReservedWordColumns($form)) {
2519                 Functions.ajaxShowMessage(Messages.strProcessingRequest);
2520                 // User wants to submit the form
2521                 $.post($form.attr('action'), $form.serialize() + CommonParams.get('arg_separator') + 'do_save_data=1', function (data) {
2522                     if (typeof data !== 'undefined' && data.success === true) {
2523                         $('#properties_message')
2524                             .removeClass('alert-danger')
2525                             .html('');
2526                         Functions.ajaxShowMessage(data.message);
2527                         // Only if the create table dialog (distinct panel) exists
2528                         var $createTableDialog = $('#create_table_dialog');
2529                         if ($createTableDialog.length > 0) {
2530                             $createTableDialog.dialog('close').remove();
2531                         }
2532                         $('#tableslistcontainer').before(data.formatted_sql);
2534                         /**
2535                          * @var tables_table    Object referring to the <tbody> element that holds the list of tables
2536                          */
2537                         var tablesTable = $('#tablesForm').find('tbody').not('#tbl_summary_row');
2538                         // this is the first table created in this db
2539                         if (tablesTable.length === 0) {
2540                             CommonActions.refreshMain(
2541                                 CommonParams.get('opendb_url')
2542                             );
2543                         } else {
2544                             /**
2545                              * @var curr_last_row   Object referring to the last <tr> element in {@link tablesTable}
2546                              */
2547                             var currLastRow = $(tablesTable).find('tr').last();
2548                             /**
2549                              * @var curr_last_row_index_string   String containing the index of {@link currLastRow}
2550                              */
2551                             var currLastRowIndexString = $(currLastRow).find('input:checkbox').attr('id').match(/\d+/)[0];
2552                             /**
2553                              * @var curr_last_row_index Index of {@link currLastRow}
2554                              */
2555                             var currLastRowIndex = parseFloat(currLastRowIndexString);
2556                             /**
2557                              * @var new_last_row_index   Index of the new row to be appended to {@link tablesTable}
2558                              */
2559                             var newLastRowIndex = currLastRowIndex + 1;
2560                             /**
2561                              * @var new_last_row_id String containing the id of the row to be appended to {@link tablesTable}
2562                              */
2563                             var newLastRowId = 'checkbox_tbl_' + newLastRowIndex;
2565                             data.newTableString = data.newTableString.replace(/checkbox_tbl_/, newLastRowId);
2566                             // append to table
2567                             $(data.newTableString)
2568                                 .appendTo(tablesTable);
2570                             // Sort the table
2571                             $(tablesTable).sortTable('th');
2573                             // Adjust summary row
2574                             DatabaseStructure.adjustTotals();
2575                         }
2577                         // Refresh navigation as a new table has been added
2578                         Navigation.reload();
2579                         // Redirect to table structure page on creation of new table
2580                         var argsep = CommonParams.get('arg_separator');
2581                         var params12 = 'ajax_request=true' + argsep + 'ajax_page_request=true';
2582                         var tableStructureUrl = 'index.php?route=/table/structure' + argsep + 'server=' + data.params.server +
2583                             argsep + 'db=' + data.params.db + argsep + 'token=' + data.params.token +
2584                             argsep + 'goto=' + encodeURIComponent('index.php?route=/database/structure') + argsep + 'table=' + data.params.table + '';
2585                         $.get(tableStructureUrl, params12, AJAX.responseHandler);
2586                     } else {
2587                         Functions.ajaxShowMessage(
2588                             '<div class="alert alert-danger" role="alert">' + data.error + '</div>',
2589                             false
2590                         );
2591                     }
2592                 }); // end $.post()
2593             }
2594         }
2595     }); // end create table form (save)
2597     /**
2598      * Submits the intermediate changes in the table creation form
2599      * to refresh the UI accordingly
2600      *
2601      * @param actionParam
2602      */
2603     function submitChangesInCreateTableForm (actionParam) {
2604         /**
2605          * @var    the_form    object referring to the create table form
2606          */
2607         var $form = $('form.create_table_form.ajax');
2609         var $msgbox = Functions.ajaxShowMessage(Messages.strProcessingRequest);
2610         Functions.prepareForAjaxRequest($form);
2612         // User wants to add more fields to the table
2613         $.post($form.attr('action'), $form.serialize() + '&' + actionParam, function (data) {
2614             if (typeof data !== 'undefined' && data.success) {
2615                 var $pageContent = $('#page_content');
2616                 $pageContent.html(data.message);
2617                 Functions.highlightSql($pageContent);
2618                 Functions.verifyColumnsProperties();
2619                 Functions.hideShowConnection($('.create_table_form select[name=tbl_storage_engine]'));
2620                 Functions.ajaxRemoveMessage($msgbox);
2621             } else {
2622                 Functions.ajaxShowMessage(data.error);
2623             }
2624         }); // end $.post()
2625     }
2627     /**
2628      * Attach event handler for create table form (add fields)
2629      */
2630     $(document).on('click', 'form.create_table_form.ajax input[name=submit_num_fields]', function (event) {
2631         event.preventDefault();
2632         submitChangesInCreateTableForm('submit_num_fields=1');
2633     }); // end create table form (add fields)
2635     $(document).on('keydown', 'form.create_table_form.ajax input[name=added_fields]', function (event) {
2636         if (event.keyCode === 13) {
2637             event.preventDefault();
2638             event.stopImmediatePropagation();
2639             $(this)
2640                 .closest('form')
2641                 .find('input[name=submit_num_fields]')
2642                 .trigger('click');
2643         }
2644     });
2646     /**
2647      * Attach event handler to manage changes in number of partitions and subpartitions
2648      */
2649     $(document).on('change', 'input[name=partition_count],input[name=subpartition_count],select[name=partition_by]', function () {
2650         var $this = $(this);
2651         var $form = $this.parents('form');
2652         if ($form.is('.create_table_form.ajax')) {
2653             submitChangesInCreateTableForm('submit_partition_change=1');
2654         } else {
2655             $form.trigger('submit');
2656         }
2657     });
2659     $(document).on('change', 'input[value=AUTO_INCREMENT]', function () {
2660         if (this.checked) {
2661             var col = /\d/.exec($(this).attr('name'));
2662             col = col[0];
2663             var $selectFieldKey = $('select[name="field_key[' + col + ']"]');
2664             if ($selectFieldKey.val() === 'none_' + col) {
2665                 $selectFieldKey.val('primary_' + col).trigger('change', [false]);
2666             }
2667         }
2668     });
2669     $('body')
2670         .off('click', 'input.preview_sql')
2671         .on('click', 'input.preview_sql', function () {
2672             var $form = $(this).closest('form');
2673             Functions.previewSql($form);
2674         });
2679  * Validates the password field in a form
2681  * @see    Messages.strPasswordEmpty
2682  * @see    Messages.strPasswordNotSame
2683  * @param {object} $theForm The form to be validated
2684  * @return {boolean}
2685  */
2686 Functions.checkPassword = function ($theForm) {
2687     // Did the user select 'no password'?
2688     if ($theForm.find('#nopass_1').is(':checked')) {
2689         return true;
2690     } else {
2691         var $pred = $theForm.find('#select_pred_password');
2692         if ($pred.length && ($pred.val() === 'none' || $pred.val() === 'keep')) {
2693             return true;
2694         }
2695     }
2697     var $password = $theForm.find('input[name=pma_pw]');
2698     var $passwordRepeat = $theForm.find('input[name=pma_pw2]');
2699     var alertMessage = false;
2701     if ($password.val() === '') {
2702         alertMessage = Messages.strPasswordEmpty;
2703     } else if ($password.val() !== $passwordRepeat.val()) {
2704         alertMessage = Messages.strPasswordNotSame;
2705     }
2707     if (alertMessage) {
2708         alert(alertMessage);
2709         $password.val('');
2710         $passwordRepeat.val('');
2711         $password.trigger('focus');
2712         return false;
2713     }
2714     return true;
2718  * Attach Ajax event handlers for 'Change Password' on index.php
2719  */
2720 AJAX.registerOnload('functions.js', function () {
2721     /* Handler for hostname type */
2722     $(document).on('change', '#select_pred_hostname', function () {
2723         var hostname = $('#pma_hostname');
2724         if (this.value === 'any') {
2725             hostname.val('%');
2726         } else if (this.value === 'localhost') {
2727             hostname.val('localhost');
2728         } else if (this.value === 'thishost' && $(this).data('thishost')) {
2729             hostname.val($(this).data('thishost'));
2730         } else if (this.value === 'hosttable') {
2731             hostname.val('').prop('required', false);
2732         } else if (this.value === 'userdefined') {
2733             hostname.trigger('focus').select().prop('required', true);
2734         }
2735     });
2737     /* Handler for editing hostname */
2738     $(document).on('change', '#pma_hostname', function () {
2739         $('#select_pred_hostname').val('userdefined');
2740         $('#pma_hostname').prop('required', true);
2741     });
2743     /* Handler for username type */
2744     $(document).on('change', '#select_pred_username', function () {
2745         if (this.value === 'any') {
2746             $('#pma_username').val('').prop('required', false);
2747             $('#user_exists_warning').css('display', 'none');
2748         } else if (this.value === 'userdefined') {
2749             $('#pma_username').trigger('focus').trigger('select').prop('required', true);
2750         }
2751     });
2753     /* Handler for editing username */
2754     $(document).on('change', '#pma_username', function () {
2755         $('#select_pred_username').val('userdefined');
2756         $('#pma_username').prop('required', true);
2757     });
2759     /* Handler for password type */
2760     $(document).on('change', '#select_pred_password', function () {
2761         if (this.value === 'none') {
2762             $('#text_pma_pw2').prop('required', false).val('');
2763             $('#text_pma_pw').prop('required', false).val('');
2764         } else if (this.value === 'userdefined') {
2765             $('#text_pma_pw2').prop('required', true);
2766             $('#text_pma_pw').prop('required', true).trigger('focus').trigger('select');
2767         } else {
2768             $('#text_pma_pw2').prop('required', false);
2769             $('#text_pma_pw').prop('required', false);
2770         }
2771     });
2773     /* Handler for editing password */
2774     $(document).on('change', '#text_pma_pw,#text_pma_pw2', function () {
2775         $('#select_pred_password').val('userdefined');
2776         $('#text_pma_pw2').prop('required', true);
2777         $('#text_pma_pw').prop('required', true);
2778     });
2780     /**
2781      * Unbind all event handlers before tearing down a page
2782      */
2783     $(document).off('click', '#change_password_anchor.ajax');
2785     /**
2786      * Attach Ajax event handler on the change password anchor
2787      */
2789     $(document).on('click', '#change_password_anchor.ajax', function (event) {
2790         event.preventDefault();
2792         var $msgbox = Functions.ajaxShowMessage();
2794         /**
2795          * @var buttonOptions Object containing options to be passed to jQueryUI's dialog
2796          */
2797         var buttonOptions = {
2798             [Messages.strGo]: {
2799                 text: Messages.strGo,
2800                 'class': 'btn btn-primary',
2801             },
2802             [Messages.strCancel]: {
2803                 text: Messages.strCancel,
2804                 'class': 'btn btn-secondary',
2805             },
2806         };
2808         buttonOptions[Messages.strGo].click = function () {
2809             event.preventDefault();
2811             /**
2812              * @var $the_form    Object referring to the change password form
2813              */
2814             var $theForm = $('#change_password_form');
2816             if (! Functions.checkPassword($theForm)) {
2817                 return false;
2818             }
2820             /**
2821              * @var {string} thisValue String containing the value of the submit button.
2822              * Need to append this for the change password form on Server Privileges
2823              * page to work
2824              */
2825             var thisValue = $(this).val();
2827             var $msgbox = Functions.ajaxShowMessage(Messages.strProcessingRequest);
2828             $theForm.append('<input type="hidden" name="ajax_request" value="true">');
2830             $.post($theForm.attr('action'), $theForm.serialize() + CommonParams.get('arg_separator') + 'change_pw=' + thisValue, function (data) {
2831                 if (typeof data === 'undefined' || data.success !== true) {
2832                     Functions.ajaxShowMessage(data.error, false);
2833                     return;
2834                 }
2836                 var $pageContent = $('#page_content');
2837                 $pageContent.prepend(data.message);
2838                 Functions.highlightSql($pageContent);
2839                 $('#change_password_dialog').hide().remove();
2840                 $('#edit_user_dialog').dialog('close').remove();
2841                 Functions.ajaxRemoveMessage($msgbox);
2842             }); // end $.post()
2843         };
2845         buttonOptions[Messages.strCancel].click = function () {
2846             $(this).dialog('close');
2847         };
2848         $.get($(this).attr('href'), { 'ajax_request': true }, function (data) {
2849             if (typeof data === 'undefined' || !data.success) {
2850                 Functions.ajaxShowMessage(data.error, false);
2851                 return;
2852             }
2854             if (data.scripts) {
2855                 AJAX.scriptHandler.load(data.scripts);
2856             }
2858             $('<div id="change_password_dialog"></div>')
2859                 .dialog({
2860                     classes: {
2861                         'ui-dialog-titlebar-close': 'btn-close'
2862                     },
2863                     title: Messages.strChangePassword,
2864                     width: 600,
2865                     close: function () {
2866                         $(this).remove();
2867                     },
2868                     buttons: buttonOptions,
2869                     modal: true
2870                 })
2871                 .append(data.message);
2872             // for this dialog, we remove the fieldset wrapping due to double headings
2873             $('fieldset#fieldset_change_password')
2874                 .find('legend').remove().end()
2875                 .find('table.table').unwrap().addClass('m-3')
2876                 .find('input#text_pma_pw').trigger('focus');
2877             $('#fieldset_change_password_footer').hide();
2878             Functions.ajaxRemoveMessage($msgbox);
2879             Functions.displayPasswordGenerateButton();
2880             $('#change_password_form').on('submit', function (e) {
2881                 e.preventDefault();
2882                 $(this)
2883                     .closest('.ui-dialog')
2884                     .find('.ui-dialog-buttonpane .ui-button')
2885                     .first()
2886                     .trigger('click');
2887             });
2888         }); // end $.get()
2889     }); // end handler for change password anchor
2890 }); // end $() for Change Password
2893  * Unbind all event handlers before tearing down a page
2894  */
2895 AJAX.registerTeardown('functions.js', function () {
2896     $(document).off('change', 'select.column_type');
2897     $(document).off('change', 'select.default_type');
2898     $(document).off('change', 'select.virtuality');
2899     $(document).off('change', 'input.allow_null');
2900     $(document).off('change', '.create_table_form select[name=tbl_storage_engine]');
2903  * Toggle the hiding/showing of the "Open in ENUM/SET editor" message when
2904  * the page loads and when the selected data type changes
2905  */
2906 AJAX.registerOnload('functions.js', function () {
2907     // is called here for normal page loads and also when opening
2908     // the Create table dialog
2909     Functions.verifyColumnsProperties();
2910     //
2911     // needs on() to work also in the Create Table dialog
2912     $(document).on('change', 'select.column_type', function () {
2913         Functions.showNoticeForEnum($(this));
2914         Functions.showWarningForIntTypes();
2915     });
2916     $(document).on('change', 'select.default_type', function () {
2917         Functions.hideShowDefaultValue($(this));
2918     });
2919     $(document).on('change', 'select.virtuality', function () {
2920         Functions.hideShowExpression($(this));
2921     });
2922     $(document).on('change', 'input.allow_null', function () {
2923         Functions.validateDefaultValue($(this));
2924     });
2925     $(document).on('change', '.create_table_form select[name=tbl_storage_engine]', function () {
2926         Functions.hideShowConnection($(this));
2927     });
2931  * If the chosen storage engine is FEDERATED show connection field. Hide otherwise
2933  * @param $engineSelector storage engine selector
2934  */
2935 Functions.hideShowConnection = function ($engineSelector) {
2936     var $connection = $('.create_table_form input[name=connection]');
2937     var $labelTh = $('.create_table_form #storage-engine-connection');
2938     if ($engineSelector.val() !== 'FEDERATED') {
2939         $connection
2940             .prop('disabled', true)
2941             .parent('td').hide();
2942         $labelTh.hide();
2943     } else {
2944         $connection
2945             .prop('disabled', false)
2946             .parent('td').show();
2947         $labelTh.show();
2948     }
2952  * If the column does not allow NULL values, makes sure that default is not NULL
2954  * @param $nullCheckbox
2955  */
2956 Functions.validateDefaultValue = function ($nullCheckbox) {
2957     if (! $nullCheckbox.prop('checked')) {
2958         var $default = $nullCheckbox.closest('tr').find('.default_type');
2959         if ($default.val() === 'NULL') {
2960             $default.val('NONE');
2961         }
2962     }
2966  * function to populate the input fields on picking a column from central list
2968  * @param {string} inputId input id of the name field for the column to be populated
2969  * @param {number} offset of the selected column in central list of columns
2970  */
2971 Functions.autoPopulate = function (inputId, offset) {
2972     var db = CommonParams.get('db');
2973     var table = CommonParams.get('table');
2974     var newInputId = inputId.substring(0, inputId.length - 1);
2975     $('#' + newInputId + '1').val(centralColumnList[db + '_' + table][offset].col_name);
2976     var colType = centralColumnList[db + '_' + table][offset].col_type.toUpperCase();
2977     $('#' + newInputId + '2').val(colType);
2978     var $input3 = $('#' + newInputId + '3');
2979     $input3.val(centralColumnList[db + '_' + table][offset].col_length);
2980     if (colType === 'ENUM' || colType === 'SET') {
2981         $input3.next().show();
2982     } else {
2983         $input3.next().hide();
2984     }
2985     var colDefault = centralColumnList[db + '_' + table][offset].col_default.toUpperCase();
2986     var $input4 = $('#' + newInputId + '4');
2987     if (colDefault === 'NULL' || colDefault === 'CURRENT_TIMESTAMP' || colDefault === 'CURRENT_TIMESTAMP()') {
2988         if (colDefault === 'CURRENT_TIMESTAMP()') {
2989             colDefault = 'CURRENT_TIMESTAMP';
2990         }
2991         $input4.val(colDefault);
2992         $input4.siblings('.default_value').hide();
2993     } if (colDefault === '') {
2994         $input4.val('NONE');
2995         $input4.siblings('.default_value').hide();
2996     } else {
2997         $input4.val('USER_DEFINED');
2998         $input4.siblings('.default_value').show();
2999         $input4.siblings('.default_value').val(centralColumnList[db + '_' + table][offset].col_default);
3000     }
3001     $('#' + newInputId + '5').val(centralColumnList[db + '_' + table][offset].col_collation);
3002     var $input6 = $('#' + newInputId + '6');
3003     $input6.val(centralColumnList[db + '_' + table][offset].col_attribute);
3004     if (centralColumnList[db + '_' + table][offset].col_extra === 'on update CURRENT_TIMESTAMP') {
3005         $input6.val(centralColumnList[db + '_' + table][offset].col_extra);
3006     }
3007     if (centralColumnList[db + '_' + table][offset].col_extra.toUpperCase() === 'AUTO_INCREMENT') {
3008         $('#' + newInputId + '9').prop('checked',true).trigger('change');
3009     } else {
3010         $('#' + newInputId + '9').prop('checked',false);
3011     }
3012     if (centralColumnList[db + '_' + table][offset].col_isNull !== '0') {
3013         $('#' + newInputId + '7').prop('checked',true);
3014     } else {
3015         $('#' + newInputId + '7').prop('checked',false);
3016     }
3020  * Unbind all event handlers before tearing down a page
3021  */
3022 AJAX.registerTeardown('functions.js', function () {
3023     $(document).off('click', 'a.open_enum_editor');
3024     $(document).off('click', 'input.add_value');
3025     $(document).off('click', '#enum_editor td.drop');
3026     $(document).off('click', 'a.central_columns_dialog');
3030  * Opens the ENUM/SET editor and controls its functions
3031  */
3032 AJAX.registerOnload('functions.js', function () {
3033     $(document).on('click', 'a.open_enum_editor', function () {
3034         // Get the name of the column that is being edited
3035         var colname = $(this).closest('tr').find('input').first().val();
3036         var title;
3037         var i;
3038         // And use it to make up a title for the page
3039         if (colname.length < 1) {
3040             title = Messages.enum_newColumnVals;
3041         } else {
3042             title = Messages.enum_columnVals.replace(
3043                 /%s/,
3044                 '"' + Functions.escapeHtml(decodeURIComponent(colname)) + '"'
3045             );
3046         }
3047         // Get the values as a string
3048         var inputstring = $(this)
3049             .closest('td')
3050             .find('input')
3051             .val();
3052         // Escape html entities
3053         inputstring = $('<div></div>')
3054             .text(inputstring)
3055             .html();
3056         // Parse the values, escaping quotes and
3057         // slashes on the fly, into an array
3058         var values = [];
3059         var inString = false;
3060         var curr;
3061         var next;
3062         var buffer = '';
3063         for (i = 0; i < inputstring.length; i++) {
3064             curr = inputstring.charAt(i);
3065             next = i === inputstring.length ? '' : inputstring.charAt(i + 1);
3066             if (! inString && curr === '\'') {
3067                 inString = true;
3068             } else if (inString && curr === '\\' && next === '\\') {
3069                 buffer += '&#92;';
3070                 i++;
3071             } else if (inString && next === '\'' && (curr === '\'' || curr === '\\')) {
3072                 buffer += '&#39;';
3073                 i++;
3074             } else if (inString && curr === '\'') {
3075                 inString = false;
3076                 values.push(buffer);
3077                 buffer = '';
3078             } else if (inString) {
3079                 buffer += curr;
3080             }
3081         }
3082         if (buffer.length > 0) {
3083             // The leftovers in the buffer are the last value (if any)
3084             values.push(buffer);
3085         }
3086         var fields = '';
3087         // If there are no values, maybe the user is about to make a
3088         // new list so we add a few for them to get started with.
3089         if (values.length === 0) {
3090             values.push('', '', '', '');
3091         }
3092         // Add the parsed values to the editor
3093         var dropIcon = Functions.getImage('b_drop');
3094         for (i = 0; i < values.length; i++) {
3095             fields += '<tr><td>' +
3096                    '<input type=\'text\' value=\'' + values[i] + '\'>' +
3097                    '</td><td class=\'drop\'>' +
3098                    dropIcon +
3099                    '</td></tr>';
3100         }
3101         /**
3102          * @var dialog HTML code for the ENUM/SET dialog
3103          */
3104         var dialog = '<div id=\'enum_editor\'>' +
3105                    '<fieldset class="pma-fieldset">' +
3106                     '<legend>' + title + '</legend>' +
3107                     '<p>' + Functions.getImage('s_notice') +
3108                     Messages.enum_hint + '</p>' +
3109                     '<table class="table table-borderless values">' + fields + '</table>' +
3110                     '</fieldset><fieldset class="pma-fieldset tblFooters">' +
3111                     '<table class="table table-borderless add"><tr><td>' +
3112                     '<div class=\'slider\'></div>' +
3113                     '</td><td>' +
3114                     '<form><div><input type=\'submit\' class=\'add_value btn btn-primary\' value=\'' +
3115                     Functions.sprintf(Messages.enum_addValue, 1) +
3116                     '\'></div></form>' +
3117                     '</td></tr></table>' +
3118                     '<input type=\'hidden\' value=\'' + // So we know which column's data is being edited
3119                     $(this).closest('td').find('input').attr('id') +
3120                     '\'>' +
3121                     '</fieldset>' +
3122                     '</div>';
3123         $('#enumEditorGoButton').on('click', function () {
3124             // When the submit button is clicked,
3125             // put the data back into the original form
3126             var valueArray = [];
3127             $('#enumEditorModal').find('.values input').each(function (index, elm) {
3128                 var val = elm.value.replace(/\\/g, '\\\\').replace(/'/g, '\'\'');
3129                 valueArray.push('\'' + val + '\'');
3130             });
3131             // get the Length/Values text field where this value belongs
3132             var valuesId = $('#enumEditorModal').find('input[type=\'hidden\']').val();
3133             $('input#' + valuesId).val(valueArray.join(','));
3134         });
3135         // Show the dialog
3136         var width = parseInt(
3137             (parseInt($('html').css('font-size'), 10) / 13) * 340,
3138             10
3139         );
3140         if (! width) {
3141             width = 340;
3142         }
3143         $('#enumEditorModal').modal('show');
3144         $('#enumEditorModal').find('.modal-body').first().html(dialog);
3145         // slider for choosing how many fields to add
3146         $('#enumEditorModal').find('.slider').slider({
3147             animate: true,
3148             range: 'min',
3149             value: 1,
3150             min: 1,
3151             max: 9,
3152             slide: function (event, ui) {
3153                 $(this).closest('table').find('input[type=submit]').val(
3154                     Functions.sprintf(Messages.enum_addValue, ui.value)
3155                 );
3156             }
3157         });
3158         // Focus the slider, otherwise it looks nearly transparent
3159         $('a.ui-slider-handle').addClass('ui-state-focus');
3160         return false;
3161     });
3163     $(document).on('click', 'a.central_columns_dialog', function () {
3164         var href = 'index.php?route=/database/central-columns';
3165         var db = CommonParams.get('db');
3166         var table = CommonParams.get('table');
3167         var maxRows = $(this).data('maxrows');
3168         var pick = $(this).data('pick');
3169         if (pick !== false) {
3170             pick = true;
3171         }
3172         var params = {
3173             'ajax_request' : true,
3174             'server' : CommonParams.get('server'),
3175             'db' : CommonParams.get('db'),
3176             'cur_table' : CommonParams.get('table'),
3177             'getColumnList':true
3178         };
3179         var colid = $(this).closest('td').find('input').attr('id');
3180         var fields = '';
3181         if (! (db + '_' + table in centralColumnList)) {
3182             centralColumnList.push(db + '_' + table);
3183             $.ajax({
3184                 type: 'POST',
3185                 url: href,
3186                 data: params,
3187                 success: function (data) {
3188                     centralColumnList[db + '_' + table] = data.message;
3189                 },
3190                 async:false
3191             });
3192         }
3193         var i = 0;
3194         var listSize = centralColumnList[db + '_' + table].length;
3195         var min = (listSize <= maxRows) ? listSize : maxRows;
3196         for (i = 0; i < min; i++) {
3197             fields += '<tr><td><div><span class="fw-bold">' +
3198                 Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_name) +
3199                 '</span><br><span class="color_gray">' + centralColumnList[db + '_' + table][i].col_type;
3201             if (centralColumnList[db + '_' + table][i].col_attribute !== '') {
3202                 fields += '(' + Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_attribute) + ') ';
3203             }
3204             if (centralColumnList[db + '_' + table][i].col_length !== '') {
3205                 fields += '(' + Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_length) + ') ';
3206             }
3207             fields += Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_extra) + '</span>' +
3208                 '</div></td>';
3209             if (pick) {
3210                 fields += '<td><input class="btn btn-secondary pick w-100" type="submit" value="' +
3211                     Messages.pickColumn + '" onclick="Functions.autoPopulate(\'' + colid + '\',' + i + ')"></td>';
3212             }
3213             fields += '</tr>';
3214         }
3215         var resultPointer = i;
3216         var searchIn = '<input type="text" class="filter_rows" placeholder="' + Messages.searchList + '">';
3217         if (fields === '') {
3218             fields = Functions.sprintf(Messages.strEmptyCentralList, '\'' + Functions.escapeHtml(db) + '\'');
3219             searchIn = '';
3220         }
3221         var seeMore = '';
3222         if (listSize > maxRows) {
3223             seeMore = '<fieldset class="pma-fieldset tblFooters text-center fw-bold">' +
3224                 '<a href=\'#\' id=\'seeMore\'>' + Messages.seeMore + '</a></fieldset>';
3225         }
3226         var centralColumnsDialog = '<div class=\'max_height_400\'>' +
3227             '<fieldset class="pma-fieldset">' +
3228             searchIn +
3229             '<table id="col_list" class="table table-borderless values">' + fields + '</table>' +
3230             '</fieldset>' +
3231             seeMore +
3232             '</div>';
3234         var width = parseInt(
3235             (parseInt($('html').css('font-size'), 10) / 13) * 500,
3236             10
3237         );
3238         if (! width) {
3239             width = 500;
3240         }
3241         var buttonOptions = {};
3242         var $centralColumnsDialog = $(centralColumnsDialog).dialog({
3243             classes: {
3244                 'ui-dialog-titlebar-close': 'btn-close'
3245             },
3246             minWidth: width,
3247             maxHeight: 450,
3248             modal: true,
3249             title: Messages.pickColumnTitle,
3250             buttons: buttonOptions,
3251             open: function () {
3252                 $('#col_list').on('click', '.pick', function () {
3253                     $centralColumnsDialog.remove();
3254                 });
3255                 $('.filter_rows').on('keyup', function () {
3256                     $.uiTableFilter($('#col_list'), $(this).val());
3257                 });
3258                 $('#seeMore').on('click', function () {
3259                     fields = '';
3260                     min = (listSize <= maxRows + resultPointer) ? listSize : maxRows + resultPointer;
3261                     for (i = resultPointer; i < min; i++) {
3262                         fields += '<tr><td><div><span class="fw-bold">' +
3263                             centralColumnList[db + '_' + table][i].col_name +
3264                             '</span><br><span class="color_gray">' +
3265                             centralColumnList[db + '_' + table][i].col_type;
3267                         if (centralColumnList[db + '_' + table][i].col_attribute !== '') {
3268                             fields += '(' + centralColumnList[db + '_' + table][i].col_attribute + ') ';
3269                         }
3270                         if (centralColumnList[db + '_' + table][i].col_length !== '') {
3271                             fields += '(' + centralColumnList[db + '_' + table][i].col_length + ') ';
3272                         }
3273                         fields += centralColumnList[db + '_' + table][i].col_extra + '</span>' +
3274                             '</div></td>';
3275                         if (pick) {
3276                             fields += '<td><input class="btn btn-secondary pick w-100" type="submit" value="' +
3277                                 Messages.pickColumn + '" onclick="Functions.autoPopulate(\'' + colid + '\',' + i + ')"></td>';
3278                         }
3279                         fields += '</tr>';
3280                     }
3281                     $('#col_list').append(fields);
3282                     resultPointer = i;
3283                     if (resultPointer === listSize) {
3284                         $('#seeMore').hide();
3285                     }
3286                     return false;
3287                 });
3288                 $(this).closest('.ui-dialog').find('.ui-dialog-buttonpane button').first().trigger('focus');
3289             },
3290             close: function () {
3291                 $('#col_list').off('click', '.pick');
3292                 $('.filter_rows').off('keyup');
3293                 $(this).remove();
3294             }
3295         });
3296         return false;
3297     });
3299     // $(document).on('click', 'a.show_central_list',function(e) {
3301     // });
3302     // When "add a new value" is clicked, append an empty text field
3303     $(document).on('click', 'input.add_value', function (e) {
3304         e.preventDefault();
3305         var numNewRows = $('#enumEditorModal').find('div.slider').slider('value');
3306         while (numNewRows--) {
3307             $('#enumEditorModal').find('.values')
3308                 .append(
3309                     '<tr class=\'hide\'><td>' +
3310                     '<input type=\'text\'>' +
3311                     '</td><td class=\'drop\'>' +
3312                     Functions.getImage('b_drop') +
3313                     '</td></tr>'
3314                 )
3315                 .find('tr').last()
3316                 .show('fast');
3317         }
3318     });
3320     // Removes the specified row from the enum editor
3321     $(document).on('click', '#enum_editor td.drop', function () {
3322         $(this).closest('tr').hide('fast', function () {
3323             $(this).remove();
3324         });
3325     });
3329  * Ensures indexes names are valid according to their type and, for a primary
3330  * key, lock index name to 'PRIMARY'
3331  * @param {string} formId Variable which parses the form name as
3332  *                        the input
3333  * @return {boolean} false if there is no index form, true else
3334  */
3335 Functions.checkIndexName = function (formId) {
3336     if ($('#' + formId).length === 0) {
3337         return false;
3338     }
3340     // Gets the elements pointers
3341     var $theIdxName = $('#input_index_name');
3342     var $theIdxChoice = $('#select_index_choice');
3344     // Index is a primary key
3345     if ($theIdxChoice.find('option:selected').val() === 'PRIMARY') {
3346         $theIdxName.val('PRIMARY');
3347         $theIdxName.prop('disabled', true);
3348     } else {
3349         if ($theIdxName.val() === 'PRIMARY') {
3350             $theIdxName.val('');
3351         }
3352         $theIdxName.prop('disabled', false);
3353     }
3355     return true;
3358 AJAX.registerTeardown('functions.js', function () {
3359     $(document).off('click', '#index_frm input[type=submit]');
3361 AJAX.registerOnload('functions.js', function () {
3362     /**
3363      * Handler for adding more columns to an index in the editor
3364      */
3365     $(document).on('click', '#index_frm input[type=submit]', function (event) {
3366         event.preventDefault();
3367         var hadAddButtonHidden = $(this).closest('fieldset').find('.add_fields').hasClass('hide');
3368         if (hadAddButtonHidden === false) {
3369             var rowsToAdd = $(this)
3370                 .closest('fieldset')
3371                 .find('.slider')
3372                 .slider('value');
3374             var tempEmptyVal = function () {
3375                 $(this).val('');
3376             };
3378             var tempSetFocus = function () {
3379                 if ($(this).find('option:selected').val() === '') {
3380                     return true;
3381                 }
3382                 $(this).closest('tr').find('input').trigger('focus');
3383             };
3385             while (rowsToAdd--) {
3386                 var $indexColumns = $('#index_columns');
3387                 var $newrow = $indexColumns
3388                     .find('tbody > tr').first()
3389                     .clone()
3390                     .appendTo(
3391                         $indexColumns.find('tbody')
3392                     );
3393                 $newrow.find(':input').each(tempEmptyVal);
3394                 // focus index size input on column picked
3395                 $newrow.find('select').on('change', tempSetFocus);
3396             }
3397         }
3398     });
3400 Functions.indexDialogModal = function (routeUrl, url, title, callbackSuccess, callbackFailure) {
3401     /* Remove the hidden dialogs if there are*/
3402     var modal = $('#indexDialogModal');
3404     const indexDialogPreviewModal = document.getElementById('indexDialogPreviewModal');
3405     indexDialogPreviewModal.addEventListener('shown.bs.modal', () => {
3406         const modalBody = indexDialogPreviewModal.querySelector('.modal-body');
3407         const $form = $('#index_frm');
3408         const formUrl = $form.attr('action');
3409         const sep = CommonParams.get('arg_separator');
3410         const formData = $form.serialize() +
3411             sep + 'do_save_data=1' +
3412             sep + 'preview_sql=1' +
3413             sep + 'ajax_request=1';
3414         $.post({
3415             url: formUrl,
3416             data: formData,
3417             success: response => {
3418                 if (! response.success) {
3419                     modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' + Messages.strErrorProcessingRequest + '</div>';
3420                     return;
3421                 }
3423                 modalBody.innerHTML = response.sql_data;
3424                 Functions.highlightSql($('#indexDialogPreviewModal'));
3425             },
3426             error: () => {
3427                 modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' + Messages.strErrorProcessingRequest + '</div>';
3428             }
3429         });
3430     });
3431     indexDialogPreviewModal.addEventListener('hidden.bs.modal', () => {
3432         indexDialogPreviewModal.querySelector('.modal-body').innerHTML = '<div class="spinner-border" role="status">' +
3433             '<span class="visually-hidden">' + Messages.strLoading + '</span></div>';
3434     });
3436     // Remove previous click listeners from other modal openings (issue: #17892)
3437     $('#indexDialogModalGoButton').off('click');
3438     $('#indexDialogModalGoButton').on('click', function () {
3439         /**
3440          * @var the_form object referring to the export form
3441          */
3442         var $form = $('#index_frm');
3443         Functions.ajaxShowMessage(Messages.strProcessingRequest);
3444         Functions.prepareForAjaxRequest($form);
3445         // User wants to submit the form
3446         $.post($form.attr('action'), $form.serialize() + CommonParams.get('arg_separator') + 'do_save_data=1', function (data) {
3447             var $sqlqueryresults = $('.sqlqueryresults');
3448             if ($sqlqueryresults.length !== 0) {
3449                 $sqlqueryresults.remove();
3450             }
3451             if (typeof data !== 'undefined' && data.success === true) {
3452                 Functions.ajaxShowMessage(data.message);
3453                 Functions.highlightSql($('.result_query'));
3454                 $('.result_query .alert').remove();
3455                 /* Reload the field form*/
3456                 $('#table_index').remove();
3457                 $('<div id=\'temp_div\'><div>')
3458                     .append(data.index_table)
3459                     .find('#table_index')
3460                     .insertAfter('#index_header');
3461                 var $editIndexDialog = $('#indexDialogModal');
3462                 if ($editIndexDialog.length > 0) {
3463                     $editIndexDialog.modal('hide');
3464                 }
3465                 $('div.no_indexes_defined').hide();
3466                 if (callbackSuccess) {
3467                     callbackSuccess(data);
3468                 }
3469                 Navigation.reload();
3470             } else {
3471                 var $tempDiv = $('<div id=\'temp_div\'><div>').append(data.error);
3472                 var $error;
3473                 if ($tempDiv.find('.error code').length !== 0) {
3474                     $error = $tempDiv.find('.error code').addClass('error');
3475                 } else {
3476                     $error = $tempDiv;
3477                 }
3478                 if (callbackFailure) {
3479                     callbackFailure();
3480                 }
3481                 Functions.ajaxShowMessage($error, false);
3482             }
3483         }); // end $.post()
3484     });
3486     var $msgbox = Functions.ajaxShowMessage();
3487     $.post(routeUrl, url, function (data) {
3488         if (typeof data !== 'undefined' && data.success === false) {
3489             // in the case of an error, show the error message returned.
3490             Functions.ajaxShowMessage(data.error, false);
3491         } else {
3492             Functions.ajaxRemoveMessage($msgbox);
3493             // Show dialog if the request was successful
3494             modal.modal('show');
3495             modal.find('.modal-body').first().html(data.message);
3496             $('#indexDialogModalLabel').first().text(title);
3497             Functions.verifyColumnsProperties();
3498             modal.find('.tblFooters').remove();
3499             Functions.showIndexEditDialog(modal);
3500         }
3501     }); // end $.get()
3504 Functions.indexEditorDialog = function (url, title, callbackSuccess, callbackFailure) {
3505     Functions.indexDialogModal('index.php?route=/table/indexes', url, title, callbackSuccess, callbackFailure);
3508 Functions.indexRenameDialog = function (url, title, callbackSuccess, callbackFailure) {
3509     Functions.indexDialogModal('index.php?route=/table/indexes/rename', url, title, callbackSuccess, callbackFailure);
3512 Functions.showIndexEditDialog = function ($outer) {
3513     Indexes.checkIndexType();
3514     Functions.checkIndexName('index_frm');
3515     var $indexColumns = $('#index_columns');
3516     $indexColumns.find('tbody').sortable({
3517         axis: 'y',
3518         containment: $indexColumns.find('tbody'),
3519         tolerance: 'pointer',
3520         forcePlaceholderSize: true,
3521         // Add custom dragged row
3522         helper: function (event, tr) {
3523             var $originalCells = tr.children();
3524             var $helper = tr.clone();
3525             $helper.children().each(function (index) {
3526                 // Set cell width in dragged row
3527                 $(this).width($originalCells.eq(index).outerWidth());
3528                 var $childrenSelect = $originalCells.eq(index).children('select');
3529                 if ($childrenSelect.length) {
3530                     var selectedIndex = $childrenSelect.prop('selectedIndex');
3531                     // Set correct select value in dragged row
3532                     $(this).children('select').prop('selectedIndex', selectedIndex);
3533                 }
3534             });
3535             return $helper;
3536         }
3537     });
3538     Functions.showHints($outer);
3539     // Add a slider for selecting how many columns to add to the index
3540     $outer.find('.slider').slider({
3541         animate: true,
3542         value: 1,
3543         min: 1,
3544         max: 16,
3545         slide: function (event, ui) {
3546             $(this).closest('fieldset').find('input[type=submit]').val(
3547                 Functions.sprintf(Messages.strAddToIndex, ui.value)
3548             );
3549         }
3550     });
3551     $('div.add_fields').removeClass('hide');
3552     // focus index size input on column picked
3553     $outer.find('table#index_columns select').on('change', function () {
3554         if ($(this).find('option:selected').val() === '') {
3555             return true;
3556         }
3557         $(this).closest('tr').find('input').trigger('focus');
3558     });
3559     // Focus the slider, otherwise it looks nearly transparent
3560     $('a.ui-slider-handle').addClass('ui-state-focus');
3561     // set focus on index name input, if empty
3562     var input = $outer.find('input#input_index_name');
3563     if (! input.val()) {
3564         input.trigger('focus');
3565     }
3569  * Function to display tooltips that were
3570  * generated on the PHP side by PhpMyAdmin\Util::showHint()
3572  * @param {object} $div a div jquery object which specifies the
3573  *                    domain for searching for tooltips. If we
3574  *                    omit this parameter the function searches
3575  *                    in the whole body
3576  **/
3577 Functions.showHints = function ($div) {
3578     var $newDiv = $div;
3579     if ($newDiv === undefined || !($newDiv instanceof jQuery) || $newDiv.length === 0) {
3580         $newDiv = $('body');
3581     }
3582     $newDiv.find('.pma_hint').each(function () {
3583         Functions.tooltip(
3584             $(this).children('img'),
3585             'img',
3586             $(this).children('span').html()
3587         );
3588     });
3591 AJAX.registerOnload('functions.js', function () {
3592     Functions.showHints();
3595 Functions.mainMenuResizerCallback = function () {
3596     // 5 px margin for jumping menu in Chrome
3597     // eslint-disable-next-line compat/compat
3598     return $(document.body).width() - 5;
3601 // This must be fired only once after the initial page load
3602 $(function () {
3603     // Initialise the menu resize plugin
3604     $('#topmenu').menuResizer(Functions.mainMenuResizerCallback);
3605     // register resize event
3606     $(window).on('resize', function () {
3607         $('#topmenu').menuResizer('resize');
3608     });
3612  * var  toggleButton  This is a function that creates a toggle
3613  *                    sliding button given a jQuery reference
3614  *                    to the correct DOM element
3616  * @param $obj
3617  */
3618 Functions.toggleButton = function ($obj) {
3619     // In rtl mode the toggle switch is flipped horizontally
3620     // so we need to take that into account
3621     var right;
3622     if ($('span.text_direction', $obj).text() === 'ltr') {
3623         right = 'right';
3624     } else {
3625         right = 'left';
3626     }
3627     /**
3628      * @var  h  Height of the button, used to scale the
3629      *          background image and position the layers
3630      */
3631     var h = $obj.height();
3632     $('img', $obj).height(h);
3633     $('table', $obj).css('bottom', h - 1);
3634     /**
3635      * @var  on   Width of the "ON" part of the toggle switch
3636      * @var  off  Width of the "OFF" part of the toggle switch
3637      */
3638     var on  = $('td.toggleOn', $obj).width();
3639     var off = $('td.toggleOff', $obj).width();
3640     // Make the "ON" and "OFF" parts of the switch the same size
3641     // + 2 pixels to avoid overflowed
3642     $('td.toggleOn > div', $obj).width(Math.max(on, off) + 2);
3643     $('td.toggleOff > div', $obj).width(Math.max(on, off) + 2);
3644     /**
3645      *  @var  w  Width of the central part of the switch
3646      */
3647     var w = parseInt(($('img', $obj).height() / 16) * 22, 10);
3648     // Resize the central part of the switch on the top
3649     // layer to match the background
3650     $($obj).find('table td').eq(1).children('div').width(w);
3651     /**
3652      * @var  imgw    Width of the background image
3653      * @var  tblw    Width of the foreground layer
3654      * @var  offset  By how many pixels to move the background
3655      *               image, so that it matches the top layer
3656      */
3657     var imgw = $('img', $obj).width();
3658     var tblw = $('table', $obj).width();
3659     var offset = parseInt(((imgw - tblw) / 2), 10);
3660     // Move the background to match the layout of the top layer
3661     $obj.find('img').css(right, offset);
3662     /**
3663      * @var  offw    Outer width of the "ON" part of the toggle switch
3664      * @var  btnw    Outer width of the central part of the switch
3665      */
3666     var offw = $('td.toggleOff', $obj).outerWidth();
3667     var btnw = $($obj).find('table td').eq(1).outerWidth();
3668     // Resize the main div so that exactly one side of
3669     // the switch plus the central part fit into it.
3670     $obj.width(offw + btnw + 2);
3671     /**
3672      * @var  move  How many pixels to move the
3673      *             switch by when toggling
3674      */
3675     var move = $('td.toggleOff', $obj).outerWidth();
3676     // If the switch is initialized to the
3677     // OFF state we need to move it now.
3678     if ($('div.toggle-container', $obj).hasClass('off')) {
3679         if (right === 'right') {
3680             $('div.toggle-container', $obj).animate({ 'left': '-=' + move + 'px' }, 0);
3681         } else {
3682             $('div.toggle-container', $obj).animate({ 'left': '+=' + move + 'px' }, 0);
3683         }
3684     }
3685     // Attach an 'onclick' event to the switch
3686     $('div.toggle-container', $obj).on('click', function () {
3687         if ($(this).hasClass('isActive')) {
3688             return false;
3689         } else {
3690             $(this).addClass('isActive');
3691         }
3692         var $msg = Functions.ajaxShowMessage();
3693         var $container = $(this);
3694         var callback = $('span.callback', this).text();
3695         var operator;
3696         var url;
3697         var removeClass;
3698         var addClass;
3699         // Perform the actual toggle
3700         if ($(this).hasClass('on')) {
3701             if (right === 'right') {
3702                 operator = '-=';
3703             } else {
3704                 operator = '+=';
3705             }
3706             url = $(this).find('td.toggleOff > span').text();
3707             removeClass = 'on';
3708             addClass = 'off';
3709         } else {
3710             if (right === 'right') {
3711                 operator = '+=';
3712             } else {
3713                 operator = '-=';
3714             }
3715             url = $(this).find('td.toggleOn > span').text();
3716             removeClass = 'off';
3717             addClass = 'on';
3718         }
3720         var parts = url.split('?');
3721         $.post(parts[0], parts[1] + '&ajax_request=true', function (data) {
3722             if (typeof data !== 'undefined' && data.success === true) {
3723                 Functions.ajaxRemoveMessage($msg);
3724                 $container
3725                     .removeClass(removeClass)
3726                     .addClass(addClass)
3727                     .animate({ 'left': operator + move + 'px' }, function () {
3728                         $container.removeClass('isActive');
3729                     });
3730                 // eslint-disable-next-line no-eval
3731                 eval(callback);
3732             } else {
3733                 Functions.ajaxShowMessage(data.error, false);
3734                 $container.removeClass('isActive');
3735             }
3736         });
3737     });
3741  * Unbind all event handlers before tearing down a page
3742  */
3743 AJAX.registerTeardown('functions.js', function () {
3744     $('div.toggle-container').off('click');
3747  * Initialise all toggle buttons
3748  */
3749 AJAX.registerOnload('functions.js', function () {
3750     $('div.toggleAjax').each(function () {
3751         var $button = $(this).show();
3752         $button.find('img').each(function () {
3753             if (this.complete) {
3754                 Functions.toggleButton($button);
3755             } else {
3756                 $(this).on('load', function () {
3757                     Functions.toggleButton($button);
3758                 });
3759             }
3760         });
3761     });
3765  * Unbind all event handlers before tearing down a page
3766  */
3767 AJAX.registerTeardown('functions.js', function () {
3768     $(document).off('change', 'select.pageselector');
3769     $('#update_recent_tables').off('ready');
3770     $('#sync_favorite_tables').off('ready');
3773 AJAX.registerOnload('functions.js', function () {
3774     /**
3775      * Autosubmit page selector
3776      */
3777     $(document).on('change', 'select.pageselector', function (event) {
3778         event.stopPropagation();
3779         // Check where to load the new content
3780         if ($(this).closest('#pma_navigation').length === 0) {
3781             // For the main page we don't need to do anything,
3782             $(this).closest('form').trigger('submit');
3783         } else {
3784             // but for the navigation we need to manually replace the content
3785             Navigation.treePagination($(this));
3786         }
3787     });
3789     var $updateRecentTables = $('#update_recent_tables');
3790     if ($updateRecentTables.length) {
3791         $.get(
3792             $updateRecentTables.attr('href'),
3793             { 'no_debug': true },
3794             function (data) {
3795                 if (typeof data !== 'undefined' && data.success === true) {
3796                     $('#pma_recent_list').html(data.list);
3797                 }
3798             }
3799         );
3800     }
3802     // Sync favorite tables from localStorage to pmadb.
3803     if ($('#sync_favorite_tables').length) {
3804         var favoriteTables = '';
3805         if (isStorageSupported('localStorage')
3806             && typeof window.localStorage.favoriteTables !== 'undefined'
3807             && window.localStorage.favoriteTables !== 'undefined') {
3808             favoriteTables = window.localStorage.favoriteTables;
3809             if (favoriteTables === 'undefined') {
3810                 // Do not send an invalid value
3811                 return;
3812             }
3813         }
3814         $.ajax({
3815             url: $('#sync_favorite_tables').attr('href'),
3816             cache: false,
3817             type: 'POST',
3818             data: {
3819                 'favoriteTables': favoriteTables,
3820                 'server': CommonParams.get('server'),
3821                 'no_debug': true
3822             },
3823             success: function (data) {
3824                 // Update localStorage.
3825                 if (isStorageSupported('localStorage')) {
3826                     window.localStorage.favoriteTables = data.favoriteTables;
3827                 }
3828                 $('#pma_favorite_list').html(data.list);
3829             }
3830         });
3831     }
3832 }); // end of $()
3835  * Creates a message inside an object with a sliding effect
3837  * @param {string} msg    A string containing the text to display
3838  * @param {JQuery} $object   a jQuery object containing the reference
3839  *                 to the element where to put the message
3840  *                 This is optional, if no element is
3841  *                 provided, one will be created below the
3842  *                 navigation links at the top of the page
3844  * @return {boolean} True on success, false on failure
3845  */
3846 Functions.slidingMessage = function (msg, $object) {
3847     var $obj = $object;
3848     if (msg === undefined || msg.length === 0) {
3849         // Don't show an empty message
3850         return false;
3851     }
3852     if ($obj === undefined || !($obj instanceof jQuery) || $obj.length === 0) {
3853         // If the second argument was not supplied,
3854         // we might have to create a new DOM node.
3855         if ($('#PMA_slidingMessage').length === 0) {
3856             $('#page_content').prepend(
3857                 '<span id="PMA_slidingMessage" ' +
3858                 'class="d-inline-block"></span>'
3859             );
3860         }
3861         $obj = $('#PMA_slidingMessage');
3862     }
3863     if ($obj.has('div').length > 0) {
3864         // If there already is a message inside the
3865         // target object, we must get rid of it
3866         $obj
3867             .find('div')
3868             .first()
3869             .fadeOut(function () {
3870                 $obj
3871                     .children()
3872                     .remove();
3873                 $obj
3874                     .append('<div>' + msg + '</div>');
3875                 // highlight any sql before taking height;
3876                 Functions.highlightSql($obj);
3877                 $obj.find('div')
3878                     .first()
3879                     .hide();
3880                 $obj
3881                     .animate({
3882                         height: $obj.find('div').first().height()
3883                     })
3884                     .find('div')
3885                     .first()
3886                     .fadeIn();
3887             });
3888     } else {
3889         // Object does not already have a message
3890         // inside it, so we simply slide it down
3891         $obj.width('100%')
3892             .html('<div>' + msg + '</div>');
3893         // highlight any sql before taking height;
3894         Functions.highlightSql($obj);
3895         var h = $obj
3896             .find('div')
3897             .first()
3898             .hide()
3899             .height();
3900         $obj
3901             .find('div')
3902             .first()
3903             .css('height', 0)
3904             .show()
3905             .animate({
3906                 height: h
3907             }, function () {
3908             // Set the height of the parent
3909             // to the height of the child
3910                 $obj
3911                     .height(
3912                         $obj
3913                             .find('div')
3914                             .first()
3915                             .height()
3916                     );
3917             });
3918     }
3919     return true;
3923  * Attach CodeMirror2 editor to SQL edit area.
3924  */
3925 AJAX.registerOnload('functions.js', function () {
3926     var $elm = $('#sqlquery');
3927     if ($elm.siblings().filter('.CodeMirror').length > 0) {
3928         return;
3929     }
3930     if ($elm.length > 0) {
3931         if (typeof CodeMirror !== 'undefined') {
3932             codeMirrorEditor = Functions.getSqlEditor($elm);
3933             codeMirrorEditor.focus();
3934             codeMirrorEditor.on('blur', Functions.updateQueryParameters);
3935         } else {
3936             // without codemirror
3937             $elm.trigger('focus').on('blur', Functions.updateQueryParameters);
3938         }
3939     }
3940     Functions.highlightSql($('body'));
3942 AJAX.registerTeardown('functions.js', function () {
3943     if (codeMirrorEditor) {
3944         $('#sqlquery').text(codeMirrorEditor.getValue());
3945         codeMirrorEditor.toTextArea();
3946         codeMirrorEditor = false;
3947     }
3949 AJAX.registerOnload('functions.js', function () {
3950     // initializes all lock-page elements lock-id and
3951     // val-hash data property
3952     $('#page_content form.lock-page textarea, ' +
3953             '#page_content form.lock-page input[type="text"], ' +
3954             '#page_content form.lock-page input[type="number"], ' +
3955             '#page_content form.lock-page select').each(function (i) {
3956         $(this).data('lock-id', i);
3957         // val-hash is the hash of default value of the field
3958         // so that it can be compared with new value hash
3959         // to check whether field was modified or not.
3960         $(this).data('val-hash', AJAX.hash($(this).val()));
3961     });
3963     // initializes lock-page elements (input types checkbox and radio buttons)
3964     // lock-id and val-hash data property
3965     $('#page_content form.lock-page input[type="checkbox"], ' +
3966             '#page_content form.lock-page input[type="radio"]').each(function (i) {
3967         $(this).data('lock-id', i);
3968         $(this).data('val-hash', AJAX.hash($(this).is(':checked')));
3969     });
3973  * jQuery plugin to correctly filter input fields by value, needed
3974  * because some nasty values may break selector syntax
3975  */
3976 (function ($) {
3977     $.fn.filterByValue = function (value) {
3978         return this.filter(function () {
3979             return $(this).val() === value;
3980         });
3981     };
3982 }(jQuery));
3985  * Return value of a cell in a table.
3987  * @param {string} td
3988  * @return {string}
3989  */
3990 Functions.getCellValue = function (td) {
3991     var $td = $(td);
3992     if ($td.is('.null')) {
3993         return '';
3994     } else if ((! $td.is('.to_be_saved')
3995         || $td.is('.set'))
3996         && $td.data('original_data')
3997     ) {
3998         return $td.data('original_data');
3999     } else {
4000         return $td.text();
4001     }
4005  * Validate and return stringified JSON inputs, or plain if invalid.
4007  * @param json the json input to be validated and stringified
4008  * @param replacer An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified.
4009  * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
4010  * @return {string}
4011  */
4012 Functions.stringifyJSON = function (json, replacer = null, space = 0) {
4013     try {
4014         return JSON.stringify(JSON.parse(json), replacer, space);
4015     } catch (e) {
4016         return json;
4017     }
4021  * Unbind all event handlers before tearing down a page
4022  */
4023 AJAX.registerTeardown('functions.js', function () {
4024     $(document).off('change', '.autosubmit');
4027 AJAX.registerOnload('functions.js', function () {
4028     /**
4029      * Automatic form submission on change.
4030      */
4031     $(document).on('change', '.autosubmit', function () {
4032         $(this).closest('form').trigger('submit');
4033     });
4037  * @implements EventListener
4038  */
4039 const PrintPage = {
4040     handleEvent: () => {
4041         window.print();
4042     }
4046  * Unbind all event handlers before tearing down a page
4047  */
4048 AJAX.registerTeardown('functions.js', function () {
4049     document.querySelectorAll('.jsPrintButton').forEach(item => {
4050         item.removeEventListener('click', PrintPage);
4051     });
4053     $(document).off('click', 'a.create_view.ajax');
4054     $(document).off('keydown', '#createViewModal input, #createViewModal select');
4055     $(document).off('change', '#fkc_checkbox');
4058 AJAX.registerOnload('functions.js', function () {
4059     document.querySelectorAll('.jsPrintButton').forEach(item => {
4060         item.addEventListener('click', PrintPage);
4061     });
4063     $('.logout').on('click', function () {
4064         var form = $(
4065             '<form method="POST" action="' + $(this).attr('href') + '" class="disableAjax">' +
4066             '<input type="hidden" name="token" value="' + Functions.escapeHtml(CommonParams.get('token')) + '">' +
4067             '</form>'
4068         );
4069         $('body').append(form);
4070         form.submit();
4071         sessionStorage.clear();
4072         return false;
4073     });
4074     /**
4075      * Ajaxification for the "Create View" action
4076      */
4077     $(document).on('click', 'a.create_view.ajax', function (e) {
4078         e.preventDefault();
4079         Functions.createViewModal($(this));
4080     });
4081     /**
4082      * Attach Ajax event handlers for input fields in the editor
4083      * and used to submit the Ajax request when the ENTER key is pressed.
4084      */
4085     if ($('#createViewModal').length !== 0) {
4086         $(document).on('keydown', '#createViewModal input, #createViewModal select', function (e) {
4087             if (e.which === 13) { // 13 is the ENTER key
4088                 e.preventDefault();
4090                 // with preventing default, selection by <select> tag
4091                 // was also prevented in IE
4092                 $(this).trigger('blur');
4094                 $(this).closest('.ui-dialog').find('.ui-button').first().trigger('click');
4095             }
4096         }); // end $(document).on()
4097     }
4099     if ($('textarea[name="view[as]"]').length !== 0) {
4100         codeMirrorEditor = Functions.getSqlEditor($('textarea[name="view[as]"]'));
4101     }
4104 Functions.createViewModal = function ($this) {
4105     var $msg = Functions.ajaxShowMessage();
4106     var sep = CommonParams.get('arg_separator');
4107     var params = Functions.getJsConfirmCommonParam(this, $this.getPostData());
4108     params += sep + 'ajax_dialog=1';
4109     $.post($this.attr('href'), params, function (data) {
4110         if (typeof data !== 'undefined' && data.success === true) {
4111             Functions.ajaxRemoveMessage($msg);
4112             $('#createViewModalGoButton').on('click', function () {
4113                 if (typeof CodeMirror !== 'undefined') {
4114                     codeMirrorEditor.save();
4115                 }
4116                 $msg = Functions.ajaxShowMessage();
4117                 $.post('index.php?route=/view/create', $('#createViewModal').find('form').serialize(), function (data) {
4118                     Functions.ajaxRemoveMessage($msg);
4119                     if (typeof data !== 'undefined' && data.success === true) {
4120                         $('#createViewModal').modal('hide');
4121                         $('.result_query').html(data.message);
4122                         Navigation.reload();
4123                     } else {
4124                         Functions.ajaxShowMessage(data.error);
4125                     }
4126                 });
4127             });
4128             $('#createViewModal').find('.modal-body').first().html(data.message);
4129             // Attach syntax highlighted editor
4130             $('#createViewModal').on('shown.bs.modal', function () {
4131                 codeMirrorEditor = Functions.getSqlEditor($('#createViewModal').find('textarea'));
4132                 $('input:visible[type=text]', $('#createViewModal')).first().trigger('focus');
4133                 $('#createViewModal').off('shown.bs.modal');
4134             });
4135             $('#createViewModal').modal('show');
4136         } else {
4137             Functions.ajaxShowMessage(data.error);
4138         }
4139     });
4143  * Makes the breadcrumbs and the menu bar float at the top of the viewport
4144  */
4145 $(function () {
4146     if ($('#floating_menubar').length && $('#PMA_disable_floating_menubar').length === 0) {
4147         var left = $('html').attr('dir') === 'ltr' ? 'left' : 'right';
4148         $('#floating_menubar')
4149             .css('margin-' + left, $('#pma_navigation').width() + $('#pma_navigation_resizer').width())
4150             .css(left, 0)
4151             .css({
4152                 'position': 'fixed',
4153                 'top': 0,
4154                 'width': '100%',
4155                 'z-index': 99
4156             })
4157             .append($('#server-breadcrumb'))
4158             .append($('#topmenucontainer'));
4159         // Allow the DOM to render, then adjust the padding on the body
4160         setTimeout(function () {
4161             $('body').css(
4162                 'padding-top',
4163                 $('#floating_menubar').outerHeight(true)
4164             );
4165             $('#topmenu').menuResizer('resize');
4166         }, 4);
4167     }
4171  * Scrolls the page to the top if clicking the server-breadcrumb bar
4172  */
4173 $(function () {
4174     $(document).on('click', '#server-breadcrumb, #goto_pagetop', function (event) {
4175         event.preventDefault();
4176         $('html, body').animate({ scrollTop: 0 }, 'fast');
4177     });
4180 var checkboxesSel = 'input.checkall:checkbox:enabled';
4181 Functions.checkboxesSel = checkboxesSel;
4184  * Watches checkboxes in a form to set the checkall box accordingly
4185  */
4186 Functions.checkboxesChanged = function () {
4187     var $form = $(this.form);
4188     // total number of checkboxes in current form
4189     var totalBoxes = $form.find(checkboxesSel).length;
4190     // number of checkboxes checked in current form
4191     var checkedBoxes = $form.find(checkboxesSel + ':checked').length;
4192     var $checkall = $form.find('input.checkall_box');
4193     if (totalBoxes === checkedBoxes) {
4194         $checkall.prop({ checked: true, indeterminate: false });
4195     } else if (checkedBoxes > 0) {
4196         $checkall.prop({ checked: true, indeterminate: true });
4197     } else {
4198         $checkall.prop({ checked: false, indeterminate: false });
4199     }
4202 $(document).on('change', checkboxesSel, Functions.checkboxesChanged);
4204 $(document).on('change', 'input.checkall_box', function () {
4205     var isChecked = $(this).is(':checked');
4206     $(this.form).find(checkboxesSel).not('.row-hidden').prop('checked', isChecked)
4207         .parents('tr').toggleClass('marked table-active', isChecked);
4210 $(document).on('click', '.checkall-filter', function () {
4211     var $this = $(this);
4212     var selector = $this.data('checkall-selector');
4213     $('input.checkall_box').prop('checked', false);
4214     $this.parents('form').find(checkboxesSel).filter(selector).prop('checked', true).trigger('change')
4215         .parents('tr').toggleClass('marked', true);
4216     return false;
4220  * Watches checkboxes in a sub form to set the sub checkall box accordingly
4221  */
4222 Functions.subCheckboxesChanged = function () {
4223     var $form = $(this).parent().parent();
4224     // total number of checkboxes in current sub form
4225     var totalBoxes = $form.find(checkboxesSel).length;
4226     // number of checkboxes checked in current sub form
4227     var checkedBoxes = $form.find(checkboxesSel + ':checked').length;
4228     var $checkall = $form.find('input.sub_checkall_box');
4229     if (totalBoxes === checkedBoxes) {
4230         $checkall.prop({ checked: true, indeterminate: false });
4231     } else if (checkedBoxes > 0) {
4232         $checkall.prop({ checked: true, indeterminate: true });
4233     } else {
4234         $checkall.prop({ checked: false, indeterminate: false });
4235     }
4238 $(document).on('change', checkboxesSel + ', input.checkall_box:checkbox:enabled', Functions.subCheckboxesChanged);
4240 $(document).on('change', 'input.sub_checkall_box', function () {
4241     var isChecked = $(this).is(':checked');
4242     var $form = $(this).parent().parent();
4243     $form.find(checkboxesSel).prop('checked', isChecked)
4244         .parents('tr').toggleClass('marked', isChecked);
4248  * Rows filtering
4250  * - rows to filter are identified by data-filter-row attribute
4251  *   which contains uppercase string to filter
4252  * - it is simple substring case insensitive search
4253  * - optionally number of matching rows is written to element with
4254  *   id filter-rows-count
4255  */
4256 $(document).on('keyup', '#filterText', function () {
4257     var filterInput = $(this).val().toUpperCase().replace(/ /g, '_');
4258     var count = 0;
4259     $('[data-filter-row]').each(function () {
4260         var $row = $(this);
4261         /* Can not use data() here as it does magic conversion to int for numeric values */
4262         if ($row.attr('data-filter-row').indexOf(filterInput) > -1) {
4263             count += 1;
4264             $row.show();
4265             $row.find('input.checkall').removeClass('row-hidden');
4266         } else {
4267             $row.hide();
4268             $row.find('input.checkall').addClass('row-hidden').prop('checked', false);
4269             $row.removeClass('marked');
4270         }
4271     });
4272     setTimeout(function () {
4273         $(checkboxesSel).trigger('change');
4274     }, 300);
4275     $('#filter-rows-count').html(count);
4277 AJAX.registerOnload('functions.js', function () {
4278     /* Trigger filtering of the list based on incoming database name */
4279     var $filter = $('#filterText');
4280     if ($filter.val()) {
4281         $filter.trigger('keyup').trigger('select');
4282     }
4286  * Formats a byte number to human-readable form
4288  * @param bytesToFormat the bytes to format
4289  * @param subDecimals optional subdecimals the number of digits after the point
4290  * @param pointChar optional pointchar the char to use as decimal point
4292  * @return {string}
4293  */
4294 Functions.formatBytes = function (bytesToFormat, subDecimals, pointChar) {
4295     var bytes = bytesToFormat;
4296     var decimals = subDecimals;
4297     var point = pointChar;
4298     if (!decimals) {
4299         decimals = 0;
4300     }
4301     if (!point) {
4302         point = '.';
4303     }
4304     var units = ['B', 'KiB', 'MiB', 'GiB'];
4305     for (var i = 0; bytes > 1024 && i < units.length; i++) {
4306         bytes /= 1024;
4307     }
4308     var factor = Math.pow(10, decimals);
4309     bytes = Math.round(bytes * factor) / factor;
4310     bytes = bytes.toString().split('.').join(point);
4311     return bytes + ' ' + units[i];
4314 AJAX.registerOnload('functions.js', function () {
4315     /**
4316      * Reveal the login form to users with JS enabled
4317      * and focus the appropriate input field
4318      */
4319     var $loginform = $('#loginform');
4320     if ($loginform.length) {
4321         $loginform.find('.js-show').show();
4322         if ($('#input_username').val()) {
4323             $('#input_password').trigger('focus');
4324         } else {
4325             $('#input_username').trigger('focus');
4326         }
4327     }
4328     var $httpsWarning = $('#js-https-mismatch');
4329     if ($httpsWarning.length) {
4330         if ((window.location.protocol === 'https:') !== CommonParams.get('is_https')) {
4331             $httpsWarning.show();
4332         }
4333     }
4337  * Formats timestamp for display
4339  * @param {string} date
4340  * @param {bool} seconds
4341  * @return {string}
4342  */
4343 Functions.formatDateTime = function (date, seconds) {
4344     var result = $.datepicker.formatDate('yy-mm-dd', date);
4345     var timefmt = 'HH:mm';
4346     if (seconds) {
4347         timefmt = 'HH:mm:ss';
4348     }
4349     return result + ' ' + $.datepicker.formatTime(
4350         timefmt, {
4351             hour: date.getHours(),
4352             minute: date.getMinutes(),
4353             second: date.getSeconds()
4354         }
4355     );
4359  * Check than forms have less fields than max allowed by PHP.
4360  * @return {boolean}
4361  */
4362 Functions.checkNumberOfFields = function () {
4363     if (typeof maxInputVars === 'undefined') {
4364         return false;
4365     }
4366     if (false === maxInputVars) {
4367         return false;
4368     }
4369     $('form').each(function () {
4370         var nbInputs = $(this).find(':input').length;
4371         if (nbInputs > maxInputVars) {
4372             var warning = Functions.sprintf(Messages.strTooManyInputs, maxInputVars);
4373             Functions.ajaxShowMessage(warning);
4374             return false;
4375         }
4376         return true;
4377     });
4379     return true;
4383  * Ignore the displayed php errors.
4384  * Simply removes the displayed errors.
4386  * @param clearPrevErrors whether to clear errors stored
4387  *             in $_SESSION['prev_errors'] at server
4389  */
4390 Functions.ignorePhpErrors = function (clearPrevErrors) {
4391     var clearPrevious = clearPrevErrors;
4392     if (typeof(clearPrevious) === 'undefined' ||
4393         clearPrevious === null
4394     ) {
4395         clearPrevious = false;
4396     }
4397     // send AJAX request to /error-report with send_error_report=0, exception_type=php & token.
4398     // It clears the prev_errors stored in session.
4399     if (clearPrevious) {
4400         var $pmaReportErrorsForm = $('#pma_report_errors_form');
4401         $pmaReportErrorsForm.find('input[name="send_error_report"]').val(0); // change send_error_report to '0'
4402         $pmaReportErrorsForm.trigger('submit');
4403     }
4405     // remove displayed errors
4406     var $pmaErrors = $('#pma_errors');
4407     $pmaErrors.fadeOut('slow');
4408     $pmaErrors.remove();
4412  * Toggle the Datetimepicker UI if the date value entered
4413  * by the user in the 'text box' is not going to be accepted
4414  * by the Datetimepicker plugin (but is accepted by MySQL)
4416  * @param $td
4417  * @param $inputField
4418  */
4419 Functions.toggleDatepickerIfInvalid = function ($td, $inputField) {
4420     // If the Datetimepicker UI is not present, return
4421     if ($inputField.hasClass('hasDatepicker')) {
4422         // Regex allowed by the Datetimepicker UI
4423         var dtexpDate = new RegExp(['^([0-9]{4})',
4424             '-(((01|03|05|07|08|10|12)-((0[1-9])|([1-2][0-9])|(3[0-1])))|((02|04|06|09|11)',
4425             '-((0[1-9])|([1-2][0-9])|30)))$'].join(''));
4426         var dtexpTime = new RegExp(['^(([0-1][0-9])|(2[0-3]))',
4427             ':((0[0-9])|([1-5][0-9]))',
4428             ':((0[0-9])|([1-5][0-9]))(.[0-9]{1,6}){0,1}$'].join(''));
4430         // If key-ed in Time or Date values are unsupported by the UI, close it
4431         if ($td.attr('data-type') === 'date' && ! dtexpDate.test($inputField.val())) {
4432             $inputField.datepicker('hide');
4433         } else if ($td.attr('data-type') === 'time' && ! dtexpTime.test($inputField.val())) {
4434             $inputField.datepicker('hide');
4435         } else {
4436             $inputField.datepicker('show');
4437         }
4438     }
4442  * Function to submit the login form after validation is done.
4443  * NOTE: do NOT use a module or it will break the callback, issue #15435
4444  */
4445 // eslint-disable-next-line no-unused-vars, camelcase
4446 var Functions_recaptchaCallback = function () {
4447     $('#login_form').trigger('submit');
4451  * Unbind all event handlers before tearing down a page
4452  */
4453 AJAX.registerTeardown('functions.js', function () {
4454     $(document).off('keydown', 'form input, form textarea, form select');
4457 AJAX.registerOnload('functions.js', function () {
4458     /**
4459      * Handle 'Ctrl/Alt + Enter' form submits
4460      */
4461     $('form input, form textarea, form select').on('keydown', function (e) {
4462         if ((e.ctrlKey && e.which === 13) || (e.altKey && e.which === 13)) {
4463             var $form = $(this).closest('form');
4465             // There could be multiple submit buttons on the same form,
4466             // we assume all of them behave identical and just click one.
4467             if (! $form.find('input[type="submit"]').first() ||
4468                 ! $form.find('input[type="submit"]').first().trigger('click')
4469             ) {
4470                 $form.trigger('submit');
4471             }
4472         }
4473     });
4477  * Unbind all event handlers before tearing down a page
4478  */
4479 AJAX.registerTeardown('functions.js', function () {
4480     $(document).off('change', 'input[type=radio][name="pw_hash"]');
4481     $(document).off('mouseover', '.sortlink');
4482     $(document).off('mouseout', '.sortlink');
4485 AJAX.registerOnload('functions.js', function () {
4486     /*
4487      * Display warning regarding SSL when sha256_password
4488      * method is selected
4489      * Used in /user-password (Change Password link on index.php)
4490      */
4491     $(document).on('change', 'select#select_authentication_plugin_cp', function () {
4492         if (this.value === 'sha256_password') {
4493             $('#ssl_reqd_warning_cp').show();
4494         } else {
4495             $('#ssl_reqd_warning_cp').hide();
4496         }
4497     });
4499     Cookies.defaults.path = CommonParams.get('rootPath');
4501     // Bind event handlers for toggling sort icons
4502     $(document).on('mouseover', '.sortlink', function () {
4503         $(this).find('.soimg').toggle();
4504     });
4505     $(document).on('mouseout', '.sortlink', function () {
4506         $(this).find('.soimg').toggle();
4507     });
4511  * Returns an HTML IMG tag for a particular image from a theme,
4512  * which may be an actual file or an icon from a sprite
4514  * @param {string} image      The name of the file to get
4515  * @param {string} alternate  Used to set 'alt' and 'title' attributes of the image
4516  * @param {object} attributes An associative array of other attributes
4518  * @return {object} The requested image, this object has two methods:
4519  *                  .toString()        - Returns the IMG tag for the requested image
4520  *                  .attr(name)        - Returns a particular attribute of the IMG
4521  *                                       tag given it's name
4522  *                  .attr(name, value) - Sets a particular attribute of the IMG
4523  *                                       tag to the given value
4524  */
4525 Functions.getImage = function (image, alternate, attributes) {
4526     var alt = alternate;
4527     var attr = attributes;
4528     // custom image object, it will eventually be returned by this functions
4529     var retval = {
4530         data: {
4531             // this is private
4532             alt: '',
4533             title: '',
4534             src: 'themes/dot.gif',
4535         },
4536         attr: function (name, value) {
4537             if (value === undefined) {
4538                 if (this.data[name] === undefined) {
4539                     return '';
4540                 } else {
4541                     return this.data[name];
4542                 }
4543             } else {
4544                 this.data[name] = value;
4545             }
4546         },
4547         toString: function () {
4548             var retval = '<' + 'img';
4549             for (var i in this.data) {
4550                 retval += ' ' + i + '="' + this.data[i] + '"';
4551             }
4552             retval += ' /' + '>';
4553             return retval;
4554         }
4555     };
4556     // initialise missing parameters
4557     if (attr === undefined) {
4558         attr = {};
4559     }
4560     if (alt === undefined) {
4561         alt = '';
4562     }
4563     // set alt
4564     if (attr.alt !== undefined) {
4565         retval.attr('alt', Functions.escapeHtml(attr.alt));
4566     } else {
4567         retval.attr('alt', Functions.escapeHtml(alt));
4568     }
4569     // set title
4570     if (attr.title !== undefined) {
4571         retval.attr('title', Functions.escapeHtml(attr.title));
4572     } else {
4573         retval.attr('title', Functions.escapeHtml(alt));
4574     }
4575     // set css classes
4576     retval.attr('class', 'icon ic_' + image);
4577     // set all other attributes
4578     for (var i in attr) {
4579         if (i === 'src') {
4580             // do not allow to override the 'src' attribute
4581             continue;
4582         }
4584         retval.attr(i, attr[i]);
4585     }
4587     return retval;
4591  * Sets a configuration value.
4593  * A configuration value may be set in both browser's local storage and
4594  * remotely in server's configuration table.
4596  * NOTE: Depending on server's configuration, the configuration table may be or
4597  * not persistent.
4599  * @param {string}     key         Configuration key.
4600  * @param {object}     value       Configuration value.
4601  */
4602 Functions.configSet = function (key, value) {
4603     // Updating value in local storage.
4604     var serialized = JSON.stringify(value);
4605     localStorage.setItem(key, serialized);
4607     $.ajax({
4608         url: 'index.php?route=/config/set',
4609         type: 'POST',
4610         dataType: 'json',
4611         data: {
4612             'ajax_request': true,
4613             key: key,
4614             server: CommonParams.get('server'),
4615             value: serialized,
4616         },
4617         success: function (data) {
4618             if (data.success !== true) {
4619                 // Try to find a message to display
4620                 if (data.error || data.message || false) {
4621                     Functions.ajaxShowMessage(data.error || data.message);
4622                 }
4623             }
4624         }
4625     });
4629  * Gets a configuration value. A configuration value will be searched in
4630  * browser's local storage first and if not found, a call to the server will be
4631  * made.
4633  * If value should not be cached and the up-to-date configuration value from
4634  * right from the server is required, the third parameter should be `false`.
4636  * @param {string}     key             Configuration key.
4637  * @param {boolean}    cached          Configuration type.
4638  * @param {Function}   successCallback The callback to call after the value is successfully received
4639  * @param {Function}   failureCallback The callback to call when the value can not be received
4641  * @return {void}
4642  */
4643 Functions.configGet = function (key, cached, successCallback, failureCallback) {
4644     var isCached = (typeof cached !== 'undefined') ? cached : true;
4645     var value = localStorage.getItem(key);
4646     if (isCached && value !== undefined && value !== null) {
4647         return JSON.parse(value);
4648     }
4650     // Result not found in local storage or ignored.
4651     // Hitting the server.
4652     $.ajax({
4653         url: 'index.php?route=/config/get',
4654         type: 'POST',
4655         dataType: 'json',
4656         data: {
4657             'ajax_request': true,
4658             server: CommonParams.get('server'),
4659             key: key
4660         },
4661         success: function (data) {
4662             if (data.success !== true) {
4663                 // Try to find a message to display
4664                 if (data.error || data.message || false) {
4665                     Functions.ajaxShowMessage(data.error || data.message);
4666                 }
4668                 // Call the callback if it is defined
4669                 if (typeof failureCallback === 'function') {
4670                     failureCallback();
4671                 }
4673                 // return here, exit non success mode
4674                 return;
4675             }
4677             // Updating value in local storage.
4678             localStorage.setItem(key, JSON.stringify(data.value));
4679             // Call the callback if it is defined
4680             if (typeof successCallback === 'function') {
4681                 // Feed it the value previously saved like on async mode
4682                 successCallback(JSON.parse(localStorage.getItem(key)));
4683             }
4684         }
4685     });
4689  * Return POST data as stored by Generator::linkOrButton
4691  * @return {string}
4692  */
4693 Functions.getPostData = function () {
4694     var dataPost = this.attr('data-post');
4695     // Strip possible leading ?
4696     if (dataPost !== undefined && dataPost.substring(0,1) === '?') {
4697         dataPost = dataPost.substr(1);
4698     }
4699     return dataPost;
4701 jQuery.fn.getPostData = Functions.getPostData;