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
12 * general function, usually for data manipulation pages
13 * @test-module Functions
18 * @var sqlBoxLocked lock for the sqlbox textarea in the querybox
20 // eslint-disable-next-line no-unused-vars
21 var sqlBoxLocked = false;
24 * @var {array}, holds elements which content should only selected once
26 var onlyOnceElements = [];
29 * @var {number} ajaxMessageCount Number of AJAX messages shown since page load
31 var ajaxMessageCount = 0;
34 * @var codeMirrorEditor object containing CodeMirror editor of the query editor in SQL tab
36 var codeMirrorEditor = false;
39 * @var codeMirrorInlineEditor object containing CodeMirror editor of the inline query editor
41 var codeMirrorInlineEditor = false;
44 * @var {boolean} sqlAutoCompleteInProgress shows if Table/Column name autocomplete AJAX is in progress
46 var sqlAutoCompleteInProgress = false;
49 * @var sqlAutoComplete object containing list of columns in each table
51 var sqlAutoComplete = false;
54 * @var {string} sqlAutoCompleteDefaultTable string containing default table to autocomplete columns
56 var sqlAutoCompleteDefaultTable = '';
59 * @var {array} centralColumnList array to hold the columns in central list per db.
61 var centralColumnList = [];
64 * @var {array} primaryIndexes array to hold 'Primary' index columns.
66 // eslint-disable-next-line no-unused-vars
67 var primaryIndexes = [];
70 * @var {array} uniqueIndexes array to hold 'Unique' index columns.
72 // eslint-disable-next-line no-unused-vars
73 var uniqueIndexes = [];
76 * @var {array} indexes array to hold 'Index' columns.
78 // eslint-disable-next-line no-unused-vars
82 * @var {array} fulltextIndexes array to hold 'Fulltext' columns.
84 // eslint-disable-next-line no-unused-vars
85 var fulltextIndexes = [];
88 * @var {array} spatialIndexes array to hold 'Spatial' columns.
90 // eslint-disable-next-line no-unused-vars
91 var spatialIndexes = [];
94 * Make sure that ajax requests will not be cached
95 * by appending a random variable to their parameters
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') });
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
113 Functions.addDatepicker = function ($thisElement, type, options) {
114 if (type !== 'date' && type !== 'time' && type !== 'datetime' && type !== 'timestamp') {
118 var showTimepicker = true;
119 if (type === 'date') {
120 showTimepicker = false;
123 // Getting the current Date and time
124 var currentDateTime = new Date();
126 var defaultOptions = {
128 hour: currentDateTime.getHours(),
129 minute: currentDateTime.getMinutes(),
130 second: currentDateTime.getSeconds(),
132 buttonImage: themeImagePath + 'b_calendar.png',
133 buttonImageOnly: true,
139 showTimepicker: showTimepicker,
140 showButtonPanel: false,
142 dateFormat: 'yy-mm-dd', // yy means year with four digits
143 timeFormat: 'HH:mm:ss.lc',
144 constrainInput: false,
145 altFieldTimeOnly: false,
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 () {
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') {
167 var $note = $('<p class="note"></div>');
168 $note.text(tooltip.option('content'));
169 $('div.ui-datepicker').append($note);
173 onSelect: function () {
174 $thisElement.data('datepicker').inline = true;
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;
182 var tooltip = $thisElement.uiTooltip('instance');
183 if (typeof tooltip !== 'undefined') {
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);
193 $thisElement.datetimepicker($.extend(defaultOptions, options));
198 * Add a date/time picker to each element that needs it
199 * (only when jquery-ui-timepicker-addon.js is loaded)
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';
211 // check for decimal places of seconds
212 if (decimals > 0 && type.indexOf('time') !== -1) {
216 timeFormat = 'HH:mm:ss.lc';
219 timeFormat = 'HH:mm:ss.l';
222 if (type === 'time') {
225 Functions.addDatepicker($(this), type, {
226 showMillisec: showMillisec,
227 showMicrosec: showMicrosec,
228 timeFormat: timeFormat,
230 firstDay: firstDayOfCalendar
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);
244 * Handle redirect and reload flags sent as part of AJAX requests
246 * @param data ajax response data
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';
254 window.location.href += CommonParams.get('arg_separator') + 'session_expired=1';
256 window.location.reload();
257 } else if (parseInt(data.reload_flag) === 1) {
258 window.location.reload();
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}
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
279 extraKeys: { 'Ctrl-Space': 'autocomplete' },
280 hintOptions: { 'completeSingle': false, 'completeOnSingleClick': true },
282 mode: 'text/x-mysql',
286 if (CodeMirror.sqlLint) {
288 gutters: ['CodeMirror-lint-markers'],
290 'getAnnotations': CodeMirror.sqlLint,
292 'lintOptions': lintOptions
297 $.extend(true, defaults, options);
299 // create CodeMirror editor
300 var codemirrorEditor = CodeMirror.fromTextArea($textarea[0], defaults);
303 resizeType = 'vertical';
306 if (resizeType === 'vertical') {
309 if (resizeType === 'both') {
312 if (resizeType === 'horizontal') {
315 $(codemirrorEditor.getWrapperElement())
316 .css('resize', resizeType)
319 resize: function () {
320 codemirrorEditor.setSize($(this).width(), $(this).height());
323 // enable autocomplete
324 codemirrorEditor.on('inputRead', Functions.codeMirrorAutoCompleteOnInputRead);
327 codemirrorEditor.on('change', function (e) {
330 content: codemirrorEditor.isClean(),
332 AJAX.lockPageHandler(e);
335 return codemirrorEditor;
341 * Clear text selection
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();
351 if (sel.removeAllRanges) {
352 sel.removeAllRanges();
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
367 Functions.tooltip = function ($elements, item, myContent, additionalOptions) {
368 if ($('#no_hint').length > 0) {
372 var defaultOptions = {
375 tooltipClass: 'tooltip',
381 $elements.uiTooltip($.extend(true, defaultOptions, additionalOptions));
387 * @param {any} unsafe
388 * @return {string | false}
390 Functions.escapeHtml = function (unsafe) {
391 if (typeof(unsafe) !== 'undefined') {
394 .replace(/&/g, '&')
395 .replace(/</g, '<')
396 .replace(/>/g, '>')
397 .replace(/"/g, '"')
398 .replace(/'/g, ''');
405 * JavaScript escaping
407 * @param {any} unsafe
408 * @return {string | false}
410 Functions.escapeJsString = function (unsafe) {
411 if (typeof(unsafe) !== 'undefined') {
415 .replace('\\', '\\\\')
416 .replace('\'', '\\\'')
417 .replace(''', '\\'')
419 .replace('"', '\\"')
422 .replace(/<\/script/gi, '</\' + \'script');
432 Functions.escapeBacktick = function (s) {
433 return s.replace('`', '``');
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
454 Functions.hideShowDefaultValue = function ($defaultType) {
455 if ($defaultType.val() === 'USER_DEFINED') {
456 $defaultType.siblings('.default_value').show().trigger('focus');
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);
467 * Hides/shows the input field for column expression based on whether
468 * VIRTUAL/PERSISTENT is selected
470 * @param $virtuality virtuality dropdown
472 Functions.hideShowExpression = function ($virtuality) {
473 if ($virtuality.val() === '') {
474 $virtuality.siblings('.expression').hide();
476 $virtuality.siblings('.expression').show();
481 * Show notices for ENUM columns; add/hide the default value
484 Functions.verifyColumnsProperties = function () {
485 $('select.column_type').each(function () {
486 Functions.showNoticeForEnum($(this));
487 Functions.showWarningForIntTypes();
489 $('select.default_type').each(function () {
490 Functions.hideShowDefaultValue($(this));
492 $('select.virtuality').each(function () {
493 Functions.hideShowExpression($(this));
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
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">');
509 Functions.checkPasswordStrength = function (value, meterObject, meterObjectLabel, username) {
510 // List of words we don't want to appear in the password
520 customDict.push(username);
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);
529 case 0: meterObjectLabel.html(Messages.strExtrWeak);
531 case 1: meterObjectLabel.html(Messages.strVeryWeak);
533 case 2: meterObjectLabel.html(Messages.strWeak);
535 case 3: meterObjectLabel.html(Messages.strGood);
537 case 4: meterObjectLabel.html(Messages.strStrong);
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
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);
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);
571 // Fallback to Math.random
572 for (i = 0; i < passwordlength; i++) {
573 randomWords[i] = Math.floor(Math.random() * pwchars.length);
577 for (i = 0; i < passwordlength; i++) {
578 passwd.value += pwchars.charAt(Math.abs(randomWords[i]) % pwchars.length);
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();
588 if (passwordForm.elements.username) {
589 username = passwordForm.elements.username.value;
591 Functions.checkPasswordStrength(passwd.value, meterObj, meterObjLabel, username);
596 * for PhpMyAdmin\Display\ChangePassword and /user-password
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);
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);
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);
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
637 Functions.selectContent = function (element, lock, onlyOnce) {
638 if (onlyOnce && onlyOnceElements[element.name]) {
642 onlyOnceElements[element.name] = true;
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
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') {
667 var isConfirmed = confirm(Functions.sprintf(Messages.strDoYouReally, theSqlQuery));
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';
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()
691 Functions.confirmQuery = function (theForm1, sqlQuery1) {
692 // Confirmation is not required in the configuration file
693 if (Messages.strDoYouReally === '') {
697 // Confirms a "DROP/DELETE/ALTER/TRUNCATE" statement
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)) {
716 if (sqlQuery1.length > 100) {
717 message = sqlQuery1.substr(0, 100) + '\n ...';
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
726 theForm1.elements.is_js_confirmed.value = 1;
729 // statement is rejected -> do not submit the form
732 } // end if (handle confirm box result)
733 } // end if (display confirm box)
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()
748 Functions.checkSqlQuery = function (theForm) {
749 // get the textarea element containing the query
751 if (codeMirrorEditor) {
752 codeMirrorEditor.save();
753 sqlQuery = codeMirrorEditor.getValue();
755 sqlQuery = theForm.elements.sql_query.value;
757 var spaceRegExp = new RegExp('\\s+');
758 if (typeof(theForm.elements.sql_file) !== 'undefined' &&
759 theForm.elements.sql_file.value.replace(spaceRegExp, '') !== '') {
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) {
768 // Checks for "DROP/DELETE/ALTER" statements
769 if (sqlQuery.replace(spaceRegExp, '') !== '') {
770 result = Functions.confirmQuery(theForm, sqlQuery);
772 alert(Messages.strFormEmpty);
775 if (codeMirrorEditor) {
776 codeMirrorEditor.focus();
777 } else if (codeMirrorInlineEditor) {
778 codeMirrorInlineEditor.focus();
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
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
809 Functions.checkFormElementInRange = function (theForm, theFieldName, message, minimum, maximum) {
810 var theField = theForm.elements[theFieldName];
811 var val = parseInt(theField.value, 10);
813 var max = Number.MAX_VALUE;
815 if (typeof(minimum) !== 'undefined') {
818 if (typeof(maximum) !== 'undefined' && maximum !== null) {
824 alert(Messages.strEnterValidNumber);
827 } else if (val < min || val > max) {
829 alert(Functions.sprintf(message, val));
833 theField.value = val;
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;
850 for (i = 0; i < fieldsCnt; i++) {
851 id = '#field_' + i + '_2';
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() !== '') {
860 alert(Messages.strEnterValidLength);
866 if (atLeastOneField === 0) {
867 id = 'field_' + i + '_1';
868 if (!Functions.emptyCheckTheField(theForm, id)) {
873 if (atLeastOneField === 0) {
874 var theField = theForm.elements.field_0_1;
875 alert(Messages.strFormEmpty);
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');
892 * True if last click is to check a row.
894 var lastClickChecked = false;
897 * Zero-based index of last clicked row.
898 * Used to handle the shift + click event in the code above.
900 var lastClickedRow = -1;
903 * Zero-based index of last shift clicked row.
905 var lastShiftClickedRow = -1;
907 var idleSecondsCounter = 0;
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;
920 $(document).on('mousemove',function () {
921 idleSecondsCounter = 0;
923 document.onkeypress = function () {
924 idleSecondsCounter = 0;
928 return Math.floor((1 + Math.random()) * 0x10000)
932 return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
933 s4() + '-' + s4() + s4() + s4();
936 function SetIdleTime () {
937 idleSecondsCounter++;
939 function UpdateIdleTime () {
940 var href = 'index.php?route=/';
941 var guid = 'default';
942 if (isStorageSupported('sessionStorage')) {
943 guid = window.sessionStorage.guid;
946 'ajax_request' : true,
947 'server' : CommonParams.get('server'),
948 'db' : CommonParams.get('db'),
950 'access_time': idleSecondsCounter,
957 success: function (data) {
959 if (CommonParams.get('LoginCookieValidity') - idleSecondsCounter < 0) {
960 /* There is other active window, let's reset counter */
961 idleSecondsCounter = 0;
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')
971 // max value for setInterval() function
972 interval = Math.min((remaining - 1) * 1000, Math.pow(2, 31) - 1);
974 updateTimeout = window.setTimeout(UpdateIdleTime, interval);
975 } else { // timeout occurred
976 clearInterval(incInterval);
977 if (isStorageSupported('sessionStorage')) {
978 window.sessionStorage.clear();
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');
987 $('#input_username').trigger('focus');
989 CommonParams.set('token', data.new_token);
990 $('input[name=token]').val(data.new_token);
992 idleSecondsCounter = 0;
993 Functions.handleRedirectAndReload(data);
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')
1004 if (isStorageSupported('sessionStorage')) {
1005 window.sessionStorage.setItem('guid', guid());
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;
1011 updateTimeout = window.setTimeout(UpdateIdleTime, interval);
1015 * Unbind all event handlers before tearing down a page
1017 AJAX.registerTeardown('functions.js', function () {
1018 $(document).off('click', 'input:checkbox.checkall');
1021 AJAX.registerOnload('functions.js', function () {
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
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) {
1036 var $checkbox = $tr.find(':checkbox.checkall');
1037 var checked = $this.prop('checked');
1038 $checkbox.prop('checked', checked).trigger('change');
1040 $tr.addClass('marked table-active');
1042 $tr.removeClass('marked table-active');
1044 lastClickChecked = checked;
1046 // remember the last clicked row
1047 lastClickedRow = lastClickChecked ? $table.find('tbody tr:not(.noclick)').index($tr) : -1;
1048 lastShiftClickedRow = -1;
1050 // handle the shift click
1051 Functions.clearSelection();
1055 // clear last shift click result
1056 if (lastShiftClickedRow >= 0) {
1057 if (lastShiftClickedRow >= lastClickedRow) {
1058 start = lastClickedRow;
1059 end = lastShiftClickedRow;
1061 start = lastShiftClickedRow;
1062 end = lastClickedRow;
1064 $tr.parent().find('tr:not(.noclick)')
1065 .slice(start, end + 1)
1066 .removeClass('marked table-active')
1068 .prop('checked', false)
1072 // handle new shift click
1073 var currRow = $table.find('tbody tr:not(.noclick)').index($tr);
1074 if (currRow >= lastClickedRow) {
1075 start = lastClickedRow;
1079 end = lastClickedRow;
1081 $tr.parent().find('tr:not(.noclick)')
1082 .slice(start, end + 1)
1083 .addClass('marked table-active')
1085 .prop('checked', true)
1088 // remember the last shift clicked row
1089 lastShiftClickedRow = currRow;
1093 Functions.addDateTimePicker();
1096 * Add attribute to text boxes for iOS devices (based on bugID: 3508912)
1098 if (navigator.userAgent.match(/(iphone|ipod|ipad)/i)) {
1099 $('input[type=text]').attr('autocapitalize', 'off').attr('autocorrect', 'off');
1104 * Checks/unchecks all options of a <select> element
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
1110 * @return {boolean} always true
1112 Functions.setSelectOptions = function (theForm, theSelect, doCheck) {
1113 $('form[name=\'' + theForm + '\'] select[name=\'' + theSelect + '\']').find('option').prop('selected', doCheck);
1118 * Sets current value for query box.
1119 * @param {string} query
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();
1133 * Handles 'Simulate query' button on SQL query box.
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 +
1153 if ($simulateDml.length) {
1154 $simulateDml.remove();
1160 * Create quick sql statements.
1162 * @param {'clear'|'format'|'saved'|'selectall'|'select'|'insert'|'update'|'delete'} queryType
1165 Functions.insertQuery = function (queryType) {
1167 if (queryType === 'clear') {
1168 Functions.setQuery('');
1170 } else if (queryType === 'format') {
1171 if (codeMirrorEditor) {
1172 $('#querymessage').html(Messages.strFormatting +
1173 ' <img class="ajaxIcon" src="' +
1174 themeImagePath + 'ajax_clock_small.gif" alt="">');
1176 'ajax_request': true,
1177 'sql': codeMirrorEditor.getValue(),
1178 'server': CommonParams.get('server')
1182 url: 'index.php?route=/database/sql/format',
1184 success: function (data) {
1186 codeMirrorEditor.setValue(data.sql);
1188 $('#querymessage').html('');
1190 error: function () {
1191 $('#querymessage').html('');
1196 } else if (queryType === 'saved') {
1197 var db = $('input[name="db"]').val();
1198 table = $('input[name="table"]').val();
1200 if (table !== undefined) {
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));
1210 Functions.ajaxShowMessage(Messages.strNoAutoSavedQuery);
1216 var myListBox = document.sqlform.dummy;
1217 table = document.sqlform.table.value;
1219 if (myListBox.options.length > 0) {
1220 sqlBoxLocked = true;
1221 var columnsList = '';
1225 for (var i = 0; i < myListBox.options.length; i++) {
1228 columnsList += ', ';
1232 columnsList += myListBox.options[i].value;
1233 valDis += '\'[value-' + NbSelect + ']\'';
1234 editDis += myListBox.options[i].value + '=\'[value-' + NbSelect + ']\'';
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';
1247 Functions.setQuery(query);
1248 sqlBoxLocked = false;
1253 * Inserts multiple fields.
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 = '';
1264 for (var i = 0; i < myListBox.options.length; i++) {
1265 if (myListBox.options[i].selected) {
1268 columnsList += ', ';
1270 columnsList += myListBox.options[i].value;
1274 /* CodeMirror support */
1275 if (codeMirrorEditor) {
1276 codeMirrorEditor.replaceSelection(columnsList);
1277 codeMirrorEditor.focus();
1279 } else if (document.selection) {
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);
1292 myQuery.value += columnsList;
1295 // eslint-disable-next-line no-unused-vars
1296 sqlBoxLocked = false;
1301 * Updates the input fields for the parameters based on the query
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);
1317 $('#parametersDiv').text(Messages.strNoParam);
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);
1333 $('#parametersDiv').append($param);
1336 $('#parametersDiv').empty();
1341 * Get checkbox for foreign key checks
1345 Functions.getForeignKeyCheckboxLoader = function () {
1347 html += '<div class="mt-1 mb-2">';
1348 html += '<div class="load-default-fk-check-value">';
1349 html += Functions.getImage('ajax_clock_small');
1355 Functions.loadForeignKeyCheckbox = function () {
1356 // Load default foreign key check value
1358 'ajax_request': true,
1359 'server': CommonParams.get('server'),
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);
1370 Functions.getJsConfirmCommonParam = function (elem, parameters) {
1371 var $elem = $(elem);
1372 var params = parameters;
1373 var sep = CommonParams.get('arg_separator');
1375 // Strip possible leading ?
1376 if (params.substring(0,1) === '?') {
1377 params = params.substr(1);
1383 params += 'is_js_confirmed=1' + sep + 'ajax_request=true' + sep + 'fk_checks=' + ($elem.find('#fk_checks').is(':checked') ? 1 : 0);
1388 * Unbind all event handlers before tearing down a page
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');
1398 $(document).off('blur', '#sqlquery');
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;
1412 if (codeMirrorEditor) {
1413 $(codeMirrorEditor.getWrapperElement()).off('keydown');
1418 * Jquery Coding for inline editing SQL_QUERY
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
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);
1444 $editorArea.html(newContent);
1445 Functions.loadForeignKeyCheckbox();
1448 Functions.bindCodeMirrorToInlineEditor();
1452 $(document).on('click', 'input#sql_query_edit_save', function () {
1453 // hide already existing success message
1455 if (codeMirrorInlineEditor) {
1456 codeMirrorInlineEditor.save();
1457 sqlQuery = codeMirrorInlineEditor.getValue();
1459 sqlQuery = $(this).parent().find('#sql_query_edit').val();
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])) {
1473 $('.alert-success').hide();
1474 $fakeForm.appendTo($('body')).trigger('submit');
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();
1483 $(document).on('click', 'input.sqlbutton', function (evt) {
1484 Functions.insertQuery(evt.target.id);
1485 Functions.handleSimulateQueryButton();
1489 $(document).on('change', '#parameterized', Functions.updateQueryParameters);
1491 var $inputUsername = $('#input_username');
1492 if ($inputUsername) {
1493 if ($inputUsername.val() === '') {
1494 $inputUsername.trigger('focus');
1496 $('#input_password').trigger('focus');
1502 * "inputRead" event handler for CodeMirror SQL query editors for autocompletion
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;
1516 'ajax_request': true,
1517 'server': CommonParams.get('server'),
1518 'db': CommonParams.get('db'),
1522 var columnHintRender = function (elem, self, data) {
1523 $('<div class="autocomplete-column-name">')
1524 .text(data.columnName)
1526 $('<div class="autocomplete-column-hint">')
1527 .text(data.columnHint)
1533 url: 'index.php?route=/database/sql/autocomplete',
1535 success: function (data) {
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];
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';
1555 table.columns.push({
1557 displayText: column + ' | ' + displayText,
1559 columnHint: displayText,
1560 render: columnHintRender
1565 sqlAutoComplete.push(table);
1567 instance.options.hintOptions.tables = sqlAutoComplete;
1568 instance.options.hintOptions.defaultTable = sqlAutoCompleteDefaultTable;
1571 complete: function () {
1572 sqlAutoCompleteInProgress = false;
1576 instance.options.hintOptions.tables = sqlAutoComplete;
1577 instance.options.hintOptions.defaultTable = sqlAutoCompleteDefaultTable;
1580 if (instance.state.completionActive) {
1583 var cur = instance.getCursor();
1584 var token = instance.getTokenAt(cur);
1586 if (token.string.match(/^[.`\w@]\w*$/)) {
1587 string = token.string;
1589 if (string.length > 0) {
1590 CodeMirror.commands.autocomplete(instance);
1595 * Remove autocomplete information before tearing down a page
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.
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);
1619 .on('keydown', Functions.catchKeypressesFromSqlInlineEdit);
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');
1632 * Adds doc link to single highlighted SQL element
1637 Functions.documentationAdd = function ($elm, params) {
1638 if (typeof mysqlDocTemplate === 'undefined') {
1642 var url = Functions.sprintf(
1643 decodeURIComponent(mysqlDocTemplate),
1646 if (params.length > 1) {
1647 // The # needs to be escaped to be part of the destination URL
1648 url += encodeURIComponent('#') + params[1];
1650 var content = $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
1661 Functions.documentationKeyword = function (idx, elm) {
1663 /* Skip already processed ones */
1664 if ($elm.find('a').length > 0) {
1667 var keyword = $elm.text().toUpperCase();
1668 var $next = $elm.next('.cm-keyword');
1670 var nextKeyword = $next.text().toUpperCase();
1671 var full = keyword + ' ' + nextKeyword;
1673 var $next2 = $next.next('.cm-keyword');
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]);
1684 if (full in mysqlDocKeyword) {
1685 Functions.documentationAdd($elm, mysqlDocKeyword[full]);
1686 Functions.documentationAdd($next, mysqlDocKeyword[full]);
1690 if (keyword in mysqlDocKeyword) {
1691 Functions.documentationAdd($elm, mysqlDocKeyword[keyword]);
1696 * Generates doc links for builtins inside highlighted SQL
1701 Functions.documentationBuiltin = function (idx, elm) {
1703 var builtin = $elm.text().toUpperCase();
1704 if (builtin in mysqlDocBuiltin) {
1705 Functions.documentationAdd($elm, mysqlDocBuiltin[builtin]);
1710 * Higlights SQL using CodeMirror.
1714 Functions.highlightSql = function ($base) {
1715 var $elm = $base.find('code.sql');
1716 $elm.each(function () {
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]);
1726 $highlight.find('.cm-keyword').each(Functions.documentationKeyword);
1727 $highlight.find('.cm-builtin').each(Functions.documentationBuiltin);
1734 * Updates an element containing code.
1736 * @param {JQuery} $base base element which contains the raw and the
1739 * @param {string} htmlValue code in HTML format, displayed if code cannot be
1742 * @param {string} rawValue raw code, used as a parameter for highlighter
1744 * @return {boolean} whether content was updated or not
1746 Functions.updateCode = function ($base, htmlValue, rawValue) {
1747 var $code = $base.find('code');
1748 if ($code.length === 0) {
1752 // Determines the type of the content and appropriate CodeMirror mode.
1755 if ($code.hasClass('json')) {
1757 mode = 'application/json';
1758 } else if ($code.hasClass('sql')) {
1760 mode = 'text/x-mysql';
1761 } else if ($code.hasClass('xml')) {
1763 mode = 'application/xml';
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]);
1778 $code.html('').append($notHighlighted);
1785 * Show a message on the top of the page for an Ajax request
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
1822 Functions.ajaxShowMessage = function (message, timeout, type) {
1824 var newTimeOut = timeout;
1826 * @var self_closing Whether the notification will automatically disappear
1828 var selfClosing = true;
1830 * @var dismissable Whether the user will be able to remove
1831 * the notification by clicking on it
1833 var dismissable = true;
1834 // Handle the case when a empty data.message is passed.
1835 // We don't want the empty message
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;
1848 // Figure out whether (or after how long) to remove the notification
1849 if (newTimeOut === undefined || newTimeOut === null) {
1851 } else if (newTimeOut === false) {
1852 selfClosing = false;
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>';
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');
1865 // Update message count to create distinct message elements every time
1867 // Remove all old messages, if any
1868 $('span.ajax_notification[id^=ajax_message_num]').remove();
1870 * @var $retval a jQuery object containing the reference
1871 * to the created AJAX message
1874 '<span class="ajax_notification" id="ajax_message_num_' +
1879 .appendTo('#loading_parent')
1882 // If the notification is self-closing we should create a callback to remove it
1886 .fadeOut('medium', function () {
1887 if ($(this).is(':data(tooltip)')) {
1888 $(this).uiTooltip('destroy');
1890 // Remove the notification
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
1897 $retval.addClass('dismissable').css('cursor', 'pointer');
1899 * Add a tooltip to the notification to let the user know that they
1900 * can dismiss the ajax notification by clicking on it.
1908 // Hide spinner if this is not a loading message
1909 if (msg !== Messages.strLoading) {
1910 $retval.css('background-image', 'none');
1912 Functions.highlightSql($retval);
1918 * Removes the message shown for an Ajax operation when it's completed
1920 * @param {JQuery} $thisMessageBox Element that holds the notification
1924 Functions.ajaxRemoveMessage = function ($thisMessageBox) {
1925 if ($thisMessageBox !== undefined && $thisMessageBox instanceof jQuery) {
1929 if ($thisMessageBox.is(':data(tooltip)')) {
1930 $thisMessageBox.uiTooltip('destroy');
1932 $thisMessageBox.remove();
1938 * Requests SQL for previewing before executing.
1940 * @param {JQuery<HTMLElement>} $form Form containing query data
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();
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'));
1966 Functions.ajaxShowMessage(response.message);
1969 error: function () {
1970 Functions.ajaxShowMessage(Messages.strErrorProcessingRequest);
1976 * Callback called when submit/"OK" is clicked on sql preview/confirm modal
1978 * @callback onSubmitCallback
1979 * @param {string} url The url
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
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'));
1997 $('#previewSQLConfirmOkButton').on('click', function () {
1999 $('#previewSqlConfirmModal').modal('hide');
2004 * check for reserved keyword column name
2006 * @param {JQuery} $form Form
2010 Functions.checkReservedWordColumns = function ($form) {
2011 var isConfirmed = true;
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);
2026 // This event only need to be fired once after the initial page load
2029 * Allows the user to dismiss a notification
2030 * created with Functions.ajaxShowMessage()
2032 var holdStarter = null;
2033 $(document).on('mousedown', 'span.ajax_notification.dismissable', function () {
2034 holdStarter = setTimeout(function () {
2039 $(document).on('mouseup', 'span.ajax_notification.dismissable', function (event) {
2040 if (holdStarter && event.which === 1) {
2041 clearTimeout(holdStarter);
2042 Functions.ajaxRemoveMessage($(this));
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
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');
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');
2061 * Copy text to clipboard
2063 * @param {string | number | string[]} text to copy to clipboard
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');
2073 var res = document.execCommand('copy');
2082 $(document).on('click', 'a.copyQueryBtn', function (event) {
2083 event.preventDefault();
2084 var res = Functions.copyToClipboard($(this).attr('data-text'));
2086 $(this).after('<span id=\'copyStatus\'> (' + Messages.strCopyQueryButtonSuccess + ')</span>');
2088 $(this).after('<span id=\'copyStatus\'> (' + Messages.strCopyQueryButtonFailure + ')</span>');
2090 setTimeout(function () {
2091 $('#copyStatus').remove();
2097 * Hides/shows the "Open in ENUM/SET editor" message, depending on the data type of the column currently selected
2099 * @param selectElement
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();
2108 $('p#enum_notice_' + enumNoticeId).hide();
2113 * Hides/shows a warning message when LENGTH is used with inappropriate integer type
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');
2121 var restricationFound = lengthRestrictions.some(restriction => Number(restriction) === 1);
2123 if (restricationFound) {
2124 $('div#length_not_allowed').show();
2126 $('div#length_not_allowed').hide();
2132 * Creates a Profiling Chart. Used in sql.js
2133 * and in server/status/monitor.js
2140 Functions.createProfilingChart = function (target, data) {
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();
2153 if (windowWidth > 768) {
2157 // draw the chart and return the chart object
2158 chart.draw(dataTable, {
2161 showDataLabels: true
2165 tooltipLocation: 'se',
2167 tooltipAxes: 'pieref',
2168 formatString: '%s, %.9Ps'
2177 // from https://web.archive.org/web/20190321233412/http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines
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
2209 Functions.prettyProfilingNum = function (number, accuracy) {
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';
2221 num = Math.round(acc * num) / acc;
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
2233 Functions.sqlPrettyPrint = function (string) {
2234 if (typeof CodeMirror === 'undefined') {
2238 var mode = CodeMirror.getMode({}, 'text/x-mysql');
2239 var stream = new CodeMirror.StringStream(string);
2240 var state = mode.startState();
2244 var tabs = function (cnt) {
2246 for (var i = 0; i < 4 * cnt; i++) {
2252 // "root-level" statements
2254 'select': ['select', 'from', 'on', 'where', 'having', 'limit', 'order by', 'group by'],
2255 'update': ['update', 'set', 'where'],
2256 'insert into': ['insert into', 'values']
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()]);
2272 var currentStatement = tokens[0][1];
2274 if (! statements[currentStatement]) {
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
2282 // How much to indent in the current line
2283 var indentLevel = 0;
2284 // Holds the "root-level" statements
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');
2299 blockStack.unshift(newBlock = 'generic');
2305 // Block end => pop from stack
2306 if (tokens[i][1] === ')') {
2307 endBlock = blockStack[0];
2313 // A subquery is starting
2314 if (i > 0 && newBlock === 'statement') {
2316 output += '\n' + tabs(indentLevel) + tokens[i][1] + ' ' + tokens[i + 1][1].toUpperCase() + '\n' + tabs(indentLevel + 1);
2317 currentStatement = tokens[i + 1][1];
2322 // A subquery is ending
2323 if (endBlock === 'statement' && indentLevel > 0) {
2324 output += '\n' + tabs(indentLevel);
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) {
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
2339 if (! spaceExceptionsBefore[tokens[i][1]] &&
2340 ! (i > 0 && spaceExceptionsAfter[tokens[i - 1][1]]) &&
2341 output.charAt(output.length - 1) !== ' ') {
2344 if (tokens[i][0] === 'keyword') {
2345 output += tokens[i][1].toUpperCase();
2347 output += tokens[i][1];
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);
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);
2363 // Todo: Also split and or blocks in newlines & indentation++
2364 // if (blockStack[0] === 'generic')
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
2378 * @param {Function} callbackFn callback to execute after user clicks on OK
2379 * @param {Function} openCallback optional callback to run when dialog is shown
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);
2392 if (Messages.strDoYouReally === '') {
2397 * @var button_options Object that stores the options passed to jQueryUI
2400 var buttonOptions = [
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);
2412 text: Messages.strCancel,
2413 'class': 'btn btn-secondary submitCancel',
2414 click: function () {
2415 $(this).dialog('close');
2420 $('<div></div>', { 'id': 'confirm_dialog', 'title': Messages.strConfirm })
2424 'ui-dialog-titlebar-close': 'btn-close'
2426 buttons: buttonOptions,
2427 close: function () {
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
2443 Functions.sortTable = function (textSelector) {
2444 return this.each(function () {
2446 * @var table_body Object referring to the table's <tbody> element
2448 var tableBody = $(this);
2450 * @var rows Object referring to the collection of rows in {@link tableBody}
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();
2459 // get the sorted order
2460 rows.sort(function (a, b) {
2461 if (a.sortKey < b.sortKey) {
2464 if (a.sortKey > b.sortKey) {
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);
2477 jQuery.fn.sortTable = Functions.sortTable;
2480 * Unbind all event handlers before tearing down a page
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
2496 AJAX.registerOnload('functions.js', function () {
2498 * Attach event handler for submission of create table form (save)
2500 $(document).on('submit', 'form.create_table_form.ajax', function (event) {
2501 event.preventDefault();
2504 * @var the_form object referring to the create table form
2506 var $form = $(this);
2509 * First validate the form; if there is a problem, avoid submitting it
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)
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')
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();
2532 $('#tableslistcontainer').before(data.formatted_sql);
2535 * @var tables_table Object referring to the <tbody> element that holds the list of tables
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')
2545 * @var curr_last_row Object referring to the last <tr> element in {@link tablesTable}
2547 var currLastRow = $(tablesTable).find('tr').last();
2549 * @var curr_last_row_index_string String containing the index of {@link currLastRow}
2551 var currLastRowIndexString = $(currLastRow).find('input:checkbox').attr('id').match(/\d+/)[0];
2553 * @var curr_last_row_index Index of {@link currLastRow}
2555 var currLastRowIndex = parseFloat(currLastRowIndexString);
2557 * @var new_last_row_index Index of the new row to be appended to {@link tablesTable}
2559 var newLastRowIndex = currLastRowIndex + 1;
2561 * @var new_last_row_id String containing the id of the row to be appended to {@link tablesTable}
2563 var newLastRowId = 'checkbox_tbl_' + newLastRowIndex;
2565 data.newTableString = data.newTableString.replace(/checkbox_tbl_/, newLastRowId);
2567 $(data.newTableString)
2568 .appendTo(tablesTable);
2571 $(tablesTable).sortTable('th');
2573 // Adjust summary row
2574 DatabaseStructure.adjustTotals();
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);
2587 Functions.ajaxShowMessage(
2588 '<div class="alert alert-danger" role="alert">' + data.error + '</div>',
2595 }); // end create table form (save)
2598 * Submits the intermediate changes in the table creation form
2599 * to refresh the UI accordingly
2601 * @param actionParam
2603 function submitChangesInCreateTableForm (actionParam) {
2605 * @var the_form object referring to the create table form
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);
2622 Functions.ajaxShowMessage(data.error);
2628 * Attach event handler for create table form (add fields)
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();
2641 .find('input[name=submit_num_fields]')
2647 * Attach event handler to manage changes in number of partitions and subpartitions
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');
2655 $form.trigger('submit');
2659 $(document).on('change', 'input[value=AUTO_INCREMENT]', function () {
2661 var col = /\d/.exec($(this).attr('name'));
2663 var $selectFieldKey = $('select[name="field_key[' + col + ']"]');
2664 if ($selectFieldKey.val() === 'none_' + col) {
2665 $selectFieldKey.val('primary_' + col).trigger('change', [false]);
2670 .off('click', 'input.preview_sql')
2671 .on('click', 'input.preview_sql', function () {
2672 var $form = $(this).closest('form');
2673 Functions.previewSql($form);
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
2686 Functions.checkPassword = function ($theForm) {
2687 // Did the user select 'no password'?
2688 if ($theForm.find('#nopass_1').is(':checked')) {
2691 var $pred = $theForm.find('#select_pred_password');
2692 if ($pred.length && ($pred.val() === 'none' || $pred.val() === 'keep')) {
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;
2708 alert(alertMessage);
2710 $passwordRepeat.val('');
2711 $password.trigger('focus');
2718 * Attach Ajax event handlers for 'Change Password' on index.php
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') {
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);
2737 /* Handler for editing hostname */
2738 $(document).on('change', '#pma_hostname', function () {
2739 $('#select_pred_hostname').val('userdefined');
2740 $('#pma_hostname').prop('required', true);
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);
2753 /* Handler for editing username */
2754 $(document).on('change', '#pma_username', function () {
2755 $('#select_pred_username').val('userdefined');
2756 $('#pma_username').prop('required', true);
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');
2768 $('#text_pma_pw2').prop('required', false);
2769 $('#text_pma_pw').prop('required', false);
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);
2781 * Unbind all event handlers before tearing down a page
2783 $(document).off('click', '#change_password_anchor.ajax');
2786 * Attach Ajax event handler on the change password anchor
2789 $(document).on('click', '#change_password_anchor.ajax', function (event) {
2790 event.preventDefault();
2792 var $msgbox = Functions.ajaxShowMessage();
2795 * @var buttonOptions Object containing options to be passed to jQueryUI's dialog
2797 var buttonOptions = {
2799 text: Messages.strGo,
2800 'class': 'btn btn-primary',
2802 [Messages.strCancel]: {
2803 text: Messages.strCancel,
2804 'class': 'btn btn-secondary',
2808 buttonOptions[Messages.strGo].click = function () {
2809 event.preventDefault();
2812 * @var $the_form Object referring to the change password form
2814 var $theForm = $('#change_password_form');
2816 if (! Functions.checkPassword($theForm)) {
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
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);
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);
2845 buttonOptions[Messages.strCancel].click = function () {
2846 $(this).dialog('close');
2848 $.get($(this).attr('href'), { 'ajax_request': true }, function (data) {
2849 if (typeof data === 'undefined' || !data.success) {
2850 Functions.ajaxShowMessage(data.error, false);
2855 AJAX.scriptHandler.load(data.scripts);
2858 $('<div id="change_password_dialog"></div>')
2861 'ui-dialog-titlebar-close': 'btn-close'
2863 title: Messages.strChangePassword,
2865 close: function () {
2868 buttons: buttonOptions,
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) {
2883 .closest('.ui-dialog')
2884 .find('.ui-dialog-buttonpane .ui-button')
2889 }); // end handler for change password anchor
2890 }); // end $() for Change Password
2893 * Unbind all event handlers before tearing down a page
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
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();
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();
2916 $(document).on('change', 'select.default_type', function () {
2917 Functions.hideShowDefaultValue($(this));
2919 $(document).on('change', 'select.virtuality', function () {
2920 Functions.hideShowExpression($(this));
2922 $(document).on('change', 'input.allow_null', function () {
2923 Functions.validateDefaultValue($(this));
2925 $(document).on('change', '.create_table_form select[name=tbl_storage_engine]', function () {
2926 Functions.hideShowConnection($(this));
2931 * If the chosen storage engine is FEDERATED show connection field. Hide otherwise
2933 * @param $engineSelector storage engine selector
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') {
2940 .prop('disabled', true)
2941 .parent('td').hide();
2945 .prop('disabled', false)
2946 .parent('td').show();
2952 * If the column does not allow NULL values, makes sure that default is not NULL
2954 * @param $nullCheckbox
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');
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
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();
2983 $input3.next().hide();
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';
2991 $input4.val(colDefault);
2992 $input4.siblings('.default_value').hide();
2993 } if (colDefault === '') {
2994 $input4.val('NONE');
2995 $input4.siblings('.default_value').hide();
2997 $input4.val('USER_DEFINED');
2998 $input4.siblings('.default_value').show();
2999 $input4.siblings('.default_value').val(centralColumnList[db + '_' + table][offset].col_default);
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);
3007 if (centralColumnList[db + '_' + table][offset].col_extra.toUpperCase() === 'AUTO_INCREMENT') {
3008 $('#' + newInputId + '9').prop('checked',true).trigger('change');
3010 $('#' + newInputId + '9').prop('checked',false);
3012 if (centralColumnList[db + '_' + table][offset].col_isNull !== '0') {
3013 $('#' + newInputId + '7').prop('checked',true);
3015 $('#' + newInputId + '7').prop('checked',false);
3020 * Unbind all event handlers before tearing down a page
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
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();
3038 // And use it to make up a title for the page
3039 if (colname.length < 1) {
3040 title = Messages.enum_newColumnVals;
3042 title = Messages.enum_columnVals.replace(
3044 '"' + Functions.escapeHtml(decodeURIComponent(colname)) + '"'
3047 // Get the values as a string
3048 var inputstring = $(this)
3052 // Escape html entities
3053 inputstring = $('<div></div>')
3056 // Parse the values, escaping quotes and
3057 // slashes on the fly, into an array
3059 var inString = false;
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 === '\'') {
3068 } else if (inString && curr === '\\' && next === '\\') {
3071 } else if (inString && next === '\'' && (curr === '\'' || curr === '\\')) {
3074 } else if (inString && curr === '\'') {
3076 values.push(buffer);
3078 } else if (inString) {
3082 if (buffer.length > 0) {
3083 // The leftovers in the buffer are the last value (if any)
3084 values.push(buffer);
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('', '', '', '');
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\'>' +
3102 * @var dialog HTML code for the ENUM/SET dialog
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>' +
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') +
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 + '\'');
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(','));
3136 var width = parseInt(
3137 (parseInt($('html').css('font-size'), 10) / 13) * 340,
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({
3152 slide: function (event, ui) {
3153 $(this).closest('table').find('input[type=submit]').val(
3154 Functions.sprintf(Messages.enum_addValue, ui.value)
3158 // Focus the slider, otherwise it looks nearly transparent
3159 $('a.ui-slider-handle').addClass('ui-state-focus');
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) {
3173 'ajax_request' : true,
3174 'server' : CommonParams.get('server'),
3175 'db' : CommonParams.get('db'),
3176 'cur_table' : CommonParams.get('table'),
3177 'getColumnList':true
3179 var colid = $(this).closest('td').find('input').attr('id');
3181 if (! (db + '_' + table in centralColumnList)) {
3182 centralColumnList.push(db + '_' + table);
3187 success: function (data) {
3188 centralColumnList[db + '_' + table] = data.message;
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) + ') ';
3204 if (centralColumnList[db + '_' + table][i].col_length !== '') {
3205 fields += '(' + Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_length) + ') ';
3207 fields += Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_extra) + '</span>' +
3210 fields += '<td><input class="btn btn-secondary pick w-100" type="submit" value="' +
3211 Messages.pickColumn + '" onclick="Functions.autoPopulate(\'' + colid + '\',' + i + ')"></td>';
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) + '\'');
3222 if (listSize > maxRows) {
3223 seeMore = '<fieldset class="pma-fieldset tblFooters text-center fw-bold">' +
3224 '<a href=\'#\' id=\'seeMore\'>' + Messages.seeMore + '</a></fieldset>';
3226 var centralColumnsDialog = '<div class=\'max_height_400\'>' +
3227 '<fieldset class="pma-fieldset">' +
3229 '<table id="col_list" class="table table-borderless values">' + fields + '</table>' +
3234 var width = parseInt(
3235 (parseInt($('html').css('font-size'), 10) / 13) * 500,
3241 var buttonOptions = {};
3242 var $centralColumnsDialog = $(centralColumnsDialog).dialog({
3244 'ui-dialog-titlebar-close': 'btn-close'
3249 title: Messages.pickColumnTitle,
3250 buttons: buttonOptions,
3252 $('#col_list').on('click', '.pick', function () {
3253 $centralColumnsDialog.remove();
3255 $('.filter_rows').on('keyup', function () {
3256 $.uiTableFilter($('#col_list'), $(this).val());
3258 $('#seeMore').on('click', function () {
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 + ') ';
3270 if (centralColumnList[db + '_' + table][i].col_length !== '') {
3271 fields += '(' + centralColumnList[db + '_' + table][i].col_length + ') ';
3273 fields += centralColumnList[db + '_' + table][i].col_extra + '</span>' +
3276 fields += '<td><input class="btn btn-secondary pick w-100" type="submit" value="' +
3277 Messages.pickColumn + '" onclick="Functions.autoPopulate(\'' + colid + '\',' + i + ')"></td>';
3281 $('#col_list').append(fields);
3283 if (resultPointer === listSize) {
3284 $('#seeMore').hide();
3288 $(this).closest('.ui-dialog').find('.ui-dialog-buttonpane button').first().trigger('focus');
3290 close: function () {
3291 $('#col_list').off('click', '.pick');
3292 $('.filter_rows').off('keyup');
3299 // $(document).on('click', 'a.show_central_list',function(e) {
3302 // When "add a new value" is clicked, append an empty text field
3303 $(document).on('click', 'input.add_value', function (e) {
3305 var numNewRows = $('#enumEditorModal').find('div.slider').slider('value');
3306 while (numNewRows--) {
3307 $('#enumEditorModal').find('.values')
3309 '<tr class=\'hide\'><td>' +
3310 '<input type=\'text\'>' +
3311 '</td><td class=\'drop\'>' +
3312 Functions.getImage('b_drop') +
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 () {
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
3333 * @return {boolean} false if there is no index form, true else
3335 Functions.checkIndexName = function (formId) {
3336 if ($('#' + formId).length === 0) {
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);
3349 if ($theIdxName.val() === 'PRIMARY') {
3350 $theIdxName.val('');
3352 $theIdxName.prop('disabled', false);
3358 AJAX.registerTeardown('functions.js', function () {
3359 $(document).off('click', '#index_frm input[type=submit]');
3361 AJAX.registerOnload('functions.js', function () {
3363 * Handler for adding more columns to an index in the editor
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')
3374 var tempEmptyVal = function () {
3378 var tempSetFocus = function () {
3379 if ($(this).find('option:selected').val() === '') {
3382 $(this).closest('tr').find('input').trigger('focus');
3385 while (rowsToAdd--) {
3386 var $indexColumns = $('#index_columns');
3387 var $newrow = $indexColumns
3388 .find('tbody > tr').first()
3391 $indexColumns.find('tbody')
3393 $newrow.find(':input').each(tempEmptyVal);
3394 // focus index size input on column picked
3395 $newrow.find('select').on('change', tempSetFocus);
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';
3417 success: response => {
3418 if (! response.success) {
3419 modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' + Messages.strErrorProcessingRequest + '</div>';
3423 modalBody.innerHTML = response.sql_data;
3424 Functions.highlightSql($('#indexDialogPreviewModal'));
3427 modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' + Messages.strErrorProcessingRequest + '</div>';
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>';
3436 // Remove previous click listeners from other modal openings (issue: #17892)
3437 $('#indexDialogModalGoButton').off('click');
3438 $('#indexDialogModalGoButton').on('click', function () {
3440 * @var the_form object referring to the export form
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();
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');
3465 $('div.no_indexes_defined').hide();
3466 if (callbackSuccess) {
3467 callbackSuccess(data);
3469 Navigation.reload();
3471 var $tempDiv = $('<div id=\'temp_div\'><div>').append(data.error);
3473 if ($tempDiv.find('.error code').length !== 0) {
3474 $error = $tempDiv.find('.error code').addClass('error');
3478 if (callbackFailure) {
3481 Functions.ajaxShowMessage($error, false);
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);
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);
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({
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);
3538 Functions.showHints($outer);
3539 // Add a slider for selecting how many columns to add to the index
3540 $outer.find('.slider').slider({
3545 slide: function (event, ui) {
3546 $(this).closest('fieldset').find('input[type=submit]').val(
3547 Functions.sprintf(Messages.strAddToIndex, ui.value)
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() === '') {
3557 $(this).closest('tr').find('input').trigger('focus');
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');
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
3577 Functions.showHints = function ($div) {
3579 if ($newDiv === undefined || !($newDiv instanceof jQuery) || $newDiv.length === 0) {
3580 $newDiv = $('body');
3582 $newDiv.find('.pma_hint').each(function () {
3584 $(this).children('img'),
3586 $(this).children('span').html()
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
3603 // Initialise the menu resize plugin
3604 $('#topmenu').menuResizer(Functions.mainMenuResizerCallback);
3605 // register resize event
3606 $(window).on('resize', function () {
3607 $('#topmenu').menuResizer('resize');
3612 * var toggleButton This is a function that creates a toggle
3613 * sliding button given a jQuery reference
3614 * to the correct DOM element
3618 Functions.toggleButton = function ($obj) {
3619 // In rtl mode the toggle switch is flipped horizontally
3620 // so we need to take that into account
3622 if ($('span.text_direction', $obj).text() === 'ltr') {
3628 * @var h Height of the button, used to scale the
3629 * background image and position the layers
3631 var h = $obj.height();
3632 $('img', $obj).height(h);
3633 $('table', $obj).css('bottom', h - 1);
3635 * @var on Width of the "ON" part of the toggle switch
3636 * @var off Width of the "OFF" part of the toggle switch
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);
3645 * @var w Width of the central part of the switch
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);
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
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);
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
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);
3672 * @var move How many pixels to move the
3673 * switch by when toggling
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);
3682 $('div.toggle-container', $obj).animate({ 'left': '+=' + move + 'px' }, 0);
3685 // Attach an 'onclick' event to the switch
3686 $('div.toggle-container', $obj).on('click', function () {
3687 if ($(this).hasClass('isActive')) {
3690 $(this).addClass('isActive');
3692 var $msg = Functions.ajaxShowMessage();
3693 var $container = $(this);
3694 var callback = $('span.callback', this).text();
3699 // Perform the actual toggle
3700 if ($(this).hasClass('on')) {
3701 if (right === 'right') {
3706 url = $(this).find('td.toggleOff > span').text();
3710 if (right === 'right') {
3715 url = $(this).find('td.toggleOn > span').text();
3716 removeClass = 'off';
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);
3725 .removeClass(removeClass)
3727 .animate({ 'left': operator + move + 'px' }, function () {
3728 $container.removeClass('isActive');
3730 // eslint-disable-next-line no-eval
3733 Functions.ajaxShowMessage(data.error, false);
3734 $container.removeClass('isActive');
3741 * Unbind all event handlers before tearing down a page
3743 AJAX.registerTeardown('functions.js', function () {
3744 $('div.toggle-container').off('click');
3747 * Initialise all toggle buttons
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);
3756 $(this).on('load', function () {
3757 Functions.toggleButton($button);
3765 * Unbind all event handlers before tearing down a page
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 () {
3775 * Autosubmit page selector
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');
3784 // but for the navigation we need to manually replace the content
3785 Navigation.treePagination($(this));
3789 var $updateRecentTables = $('#update_recent_tables');
3790 if ($updateRecentTables.length) {
3792 $updateRecentTables.attr('href'),
3793 { 'no_debug': true },
3795 if (typeof data !== 'undefined' && data.success === true) {
3796 $('#pma_recent_list').html(data.list);
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
3815 url: $('#sync_favorite_tables').attr('href'),
3819 'favoriteTables': favoriteTables,
3820 'server': CommonParams.get('server'),
3823 success: function (data) {
3824 // Update localStorage.
3825 if (isStorageSupported('localStorage')) {
3826 window.localStorage.favoriteTables = data.favoriteTables;
3828 $('#pma_favorite_list').html(data.list);
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
3846 Functions.slidingMessage = function (msg, $object) {
3848 if (msg === undefined || msg.length === 0) {
3849 // Don't show an empty message
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>'
3861 $obj = $('#PMA_slidingMessage');
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
3869 .fadeOut(function () {
3874 .append('<div>' + msg + '</div>');
3875 // highlight any sql before taking height;
3876 Functions.highlightSql($obj);
3882 height: $obj.find('div').first().height()
3889 // Object does not already have a message
3890 // inside it, so we simply slide it down
3892 .html('<div>' + msg + '</div>');
3893 // highlight any sql before taking height;
3894 Functions.highlightSql($obj);
3908 // Set the height of the parent
3909 // to the height of the child
3923 * Attach CodeMirror2 editor to SQL edit area.
3925 AJAX.registerOnload('functions.js', function () {
3926 var $elm = $('#sqlquery');
3927 if ($elm.siblings().filter('.CodeMirror').length > 0) {
3930 if ($elm.length > 0) {
3931 if (typeof CodeMirror !== 'undefined') {
3932 codeMirrorEditor = Functions.getSqlEditor($elm);
3933 codeMirrorEditor.focus();
3934 codeMirrorEditor.on('blur', Functions.updateQueryParameters);
3936 // without codemirror
3937 $elm.trigger('focus').on('blur', Functions.updateQueryParameters);
3940 Functions.highlightSql($('body'));
3942 AJAX.registerTeardown('functions.js', function () {
3943 if (codeMirrorEditor) {
3944 $('#sqlquery').text(codeMirrorEditor.getValue());
3945 codeMirrorEditor.toTextArea();
3946 codeMirrorEditor = false;
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()));
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')));
3973 * jQuery plugin to correctly filter input fields by value, needed
3974 * because some nasty values may break selector syntax
3977 $.fn.filterByValue = function (value) {
3978 return this.filter(function () {
3979 return $(this).val() === value;
3985 * Return value of a cell in a table.
3987 * @param {string} td
3990 Functions.getCellValue = function (td) {
3992 if ($td.is('.null')) {
3994 } else if ((! $td.is('.to_be_saved')
3996 && $td.data('original_data')
3998 return $td.data('original_data');
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.
4012 Functions.stringifyJSON = function (json, replacer = null, space = 0) {
4014 return JSON.stringify(JSON.parse(json), replacer, space);
4021 * Unbind all event handlers before tearing down a page
4023 AJAX.registerTeardown('functions.js', function () {
4024 $(document).off('change', '.autosubmit');
4027 AJAX.registerOnload('functions.js', function () {
4029 * Automatic form submission on change.
4031 $(document).on('change', '.autosubmit', function () {
4032 $(this).closest('form').trigger('submit');
4037 * @implements EventListener
4040 handleEvent: () => {
4046 * Unbind all event handlers before tearing down a page
4048 AJAX.registerTeardown('functions.js', function () {
4049 document.querySelectorAll('.jsPrintButton').forEach(item => {
4050 item.removeEventListener('click', PrintPage);
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);
4063 $('.logout').on('click', function () {
4065 '<form method="POST" action="' + $(this).attr('href') + '" class="disableAjax">' +
4066 '<input type="hidden" name="token" value="' + Functions.escapeHtml(CommonParams.get('token')) + '">' +
4069 $('body').append(form);
4071 sessionStorage.clear();
4075 * Ajaxification for the "Create View" action
4077 $(document).on('click', 'a.create_view.ajax', function (e) {
4079 Functions.createViewModal($(this));
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.
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
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');
4096 }); // end $(document).on()
4099 if ($('textarea[name="view[as]"]').length !== 0) {
4100 codeMirrorEditor = Functions.getSqlEditor($('textarea[name="view[as]"]'));
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();
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();
4124 Functions.ajaxShowMessage(data.error);
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');
4135 $('#createViewModal').modal('show');
4137 Functions.ajaxShowMessage(data.error);
4143 * Makes the breadcrumbs and the menu bar float at the top of the viewport
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())
4152 'position': 'fixed',
4157 .append($('#server-breadcrumb'))
4158 .append($('#topmenucontainer'));
4159 // Allow the DOM to render, then adjust the padding on the body
4160 setTimeout(function () {
4163 $('#floating_menubar').outerHeight(true)
4165 $('#topmenu').menuResizer('resize');
4171 * Scrolls the page to the top if clicking the server-breadcrumb bar
4174 $(document).on('click', '#server-breadcrumb, #goto_pagetop', function (event) {
4175 event.preventDefault();
4176 $('html, body').animate({ scrollTop: 0 }, 'fast');
4180 var checkboxesSel = 'input.checkall:checkbox:enabled';
4181 Functions.checkboxesSel = checkboxesSel;
4184 * Watches checkboxes in a form to set the checkall box accordingly
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 });
4198 $checkall.prop({ checked: false, indeterminate: false });
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);
4220 * Watches checkboxes in a sub form to set the sub checkall box accordingly
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 });
4234 $checkall.prop({ checked: false, indeterminate: false });
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);
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
4256 $(document).on('keyup', '#filterText', function () {
4257 var filterInput = $(this).val().toUpperCase().replace(/ /g, '_');
4259 $('[data-filter-row]').each(function () {
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) {
4265 $row.find('input.checkall').removeClass('row-hidden');
4268 $row.find('input.checkall').addClass('row-hidden').prop('checked', false);
4269 $row.removeClass('marked');
4272 setTimeout(function () {
4273 $(checkboxesSel).trigger('change');
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');
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
4294 Functions.formatBytes = function (bytesToFormat, subDecimals, pointChar) {
4295 var bytes = bytesToFormat;
4296 var decimals = subDecimals;
4297 var point = pointChar;
4304 var units = ['B', 'KiB', 'MiB', 'GiB'];
4305 for (var i = 0; bytes > 1024 && i < units.length; i++) {
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 () {
4316 * Reveal the login form to users with JS enabled
4317 * and focus the appropriate input field
4319 var $loginform = $('#loginform');
4320 if ($loginform.length) {
4321 $loginform.find('.js-show').show();
4322 if ($('#input_username').val()) {
4323 $('#input_password').trigger('focus');
4325 $('#input_username').trigger('focus');
4328 var $httpsWarning = $('#js-https-mismatch');
4329 if ($httpsWarning.length) {
4330 if ((window.location.protocol === 'https:') !== CommonParams.get('is_https')) {
4331 $httpsWarning.show();
4337 * Formats timestamp for display
4339 * @param {string} date
4340 * @param {bool} seconds
4343 Functions.formatDateTime = function (date, seconds) {
4344 var result = $.datepicker.formatDate('yy-mm-dd', date);
4345 var timefmt = 'HH:mm';
4347 timefmt = 'HH:mm:ss';
4349 return result + ' ' + $.datepicker.formatTime(
4351 hour: date.getHours(),
4352 minute: date.getMinutes(),
4353 second: date.getSeconds()
4359 * Check than forms have less fields than max allowed by PHP.
4362 Functions.checkNumberOfFields = function () {
4363 if (typeof maxInputVars === 'undefined') {
4366 if (false === maxInputVars) {
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);
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
4390 Functions.ignorePhpErrors = function (clearPrevErrors) {
4391 var clearPrevious = clearPrevErrors;
4392 if (typeof(clearPrevious) === 'undefined' ||
4393 clearPrevious === null
4395 clearPrevious = false;
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');
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)
4417 * @param $inputField
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');
4436 $inputField.datepicker('show');
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
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
4453 AJAX.registerTeardown('functions.js', function () {
4454 $(document).off('keydown', 'form input, form textarea, form select');
4457 AJAX.registerOnload('functions.js', function () {
4459 * Handle 'Ctrl/Alt + Enter' form submits
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')
4470 $form.trigger('submit');
4477 * Unbind all event handlers before tearing down a page
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 () {
4487 * Display warning regarding SSL when sha256_password
4488 * method is selected
4489 * Used in /user-password (Change Password link on index.php)
4491 $(document).on('change', 'select#select_authentication_plugin_cp', function () {
4492 if (this.value === 'sha256_password') {
4493 $('#ssl_reqd_warning_cp').show();
4495 $('#ssl_reqd_warning_cp').hide();
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();
4505 $(document).on('mouseout', '.sortlink', function () {
4506 $(this).find('.soimg').toggle();
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
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
4534 src: 'themes/dot.gif',
4536 attr: function (name, value) {
4537 if (value === undefined) {
4538 if (this.data[name] === undefined) {
4541 return this.data[name];
4544 this.data[name] = value;
4547 toString: function () {
4548 var retval = '<' + 'img';
4549 for (var i in this.data) {
4550 retval += ' ' + i + '="' + this.data[i] + '"';
4552 retval += ' /' + '>';
4556 // initialise missing parameters
4557 if (attr === undefined) {
4560 if (alt === undefined) {
4564 if (attr.alt !== undefined) {
4565 retval.attr('alt', Functions.escapeHtml(attr.alt));
4567 retval.attr('alt', Functions.escapeHtml(alt));
4570 if (attr.title !== undefined) {
4571 retval.attr('title', Functions.escapeHtml(attr.title));
4573 retval.attr('title', Functions.escapeHtml(alt));
4576 retval.attr('class', 'icon ic_' + image);
4577 // set all other attributes
4578 for (var i in attr) {
4580 // do not allow to override the 'src' attribute
4584 retval.attr(i, attr[i]);
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
4599 * @param {string} key Configuration key.
4600 * @param {object} value Configuration value.
4602 Functions.configSet = function (key, value) {
4603 // Updating value in local storage.
4604 var serialized = JSON.stringify(value);
4605 localStorage.setItem(key, serialized);
4608 url: 'index.php?route=/config/set',
4612 'ajax_request': true,
4614 server: CommonParams.get('server'),
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);
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
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
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);
4650 // Result not found in local storage or ignored.
4651 // Hitting the server.
4653 url: 'index.php?route=/config/get',
4657 'ajax_request': true,
4658 server: CommonParams.get('server'),
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);
4668 // Call the callback if it is defined
4669 if (typeof failureCallback === 'function') {
4673 // return here, exit non success mode
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)));
4689 * Return POST data as stored by Generator::linkOrButton
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);
4701 jQuery.fn.getPostData = Functions.getPostData;