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, pmaThemeImage */ // templates/javascript/variables.twig
8 /* global MicroHistory */ // js/microhistory.js
9 /* global sprintf */ // js/vendor/sprintf.js
10 /* global Int32Array */ // ES6
11 /* global zxcvbn */ // js/vendor/zxcvbn.js
14 * general function, usually for data manipulation pages
20 * @var sqlBoxLocked lock for the sqlbox textarea in the querybox
22 // eslint-disable-next-line no-unused-vars
23 var sqlBoxLocked = false;
26 * @var {array} holds elements which content should only selected once
28 var onlyOnceElements = [];
31 * @var {int} ajaxMessageCount Number of AJAX messages shown since page load
33 var ajaxMessageCount = 0;
36 * @var codeMirrorEditor object containing CodeMirror editor of the query editor in SQL tab
38 var codeMirrorEditor = false;
41 * @var codeMirrorInlineEditor object containing CodeMirror editor of the inline query editor
43 var codeMirrorInlineEditor = false;
46 * @var {boolean} sqlAutoCompleteInProgress shows if Table/Column name autocomplete AJAX is in progress
48 var sqlAutoCompleteInProgress = false;
51 * @var sqlAutoComplete object containing list of columns in each table
53 var sqlAutoComplete = false;
56 * @var {string} sqlAutoCompleteDefaultTable string containing default table to autocomplete columns
58 var sqlAutoCompleteDefaultTable = '';
61 * @var {array} centralColumnList array to hold the columns in central list per db.
63 var centralColumnList = [];
66 * @var {array} primaryIndexes array to hold 'Primary' index columns.
68 // eslint-disable-next-line no-unused-vars
69 var primaryIndexes = [];
72 * @var {array} uniqueIndexes array to hold 'Unique' index columns.
74 // eslint-disable-next-line no-unused-vars
75 var uniqueIndexes = [];
78 * @var {array} indexes array to hold 'Index' columns.
80 // eslint-disable-next-line no-unused-vars
84 * @var {array} fulltextIndexes array to hold 'Fulltext' columns.
86 // eslint-disable-next-line no-unused-vars
87 var fulltextIndexes = [];
90 * @var {array} spatialIndexes array to hold 'Spatial' columns.
92 // eslint-disable-next-line no-unused-vars
93 var spatialIndexes = [];
96 * Make sure that ajax requests will not be cached
97 * by appending a random variable to their parameters
99 $.ajaxPrefilter(function (options, originalOptions) {
100 var nocache = new Date().getTime() + '' + Math.floor(Math.random() * 1000000);
101 if (typeof options.data === 'string') {
102 options.data += '&_nocache=' + nocache + '&token=' + encodeURIComponent(CommonParams.get('token'));
103 } else if (typeof options.data === 'object') {
104 options.data = $.extend(originalOptions.data, { '_nocache' : nocache, 'token': CommonParams.get('token') });
109 * Adds a date/time picker to an element
111 * @param {object} $thisElement a jQuery object pointing to the element
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: pmaThemeImage + 'b_calendar.png',
133 buttonImageOnly: true,
139 showTimepicker: showTimepicker,
140 showButtonPanel: false,
141 dateFormat: 'yy-mm-dd', // yy means year with four digits
142 timeFormat: 'HH:mm:ss.lc',
143 constrainInput: false,
144 altFieldTimeOnly: false,
146 beforeShow: function (input, inst) {
147 // Remember that we came from the datepicker; this is used
148 // in table/change.js by verificationsAfterFieldChange()
149 $thisElement.data('comes_from', 'datepicker');
150 if ($(input).closest('.cEdit').length > 0) {
151 setTimeout(function () {
159 setTimeout(function () {
160 // Fix wrong timepicker z-index, doesn't work without timeout
161 $('#ui-timepicker-div').css('z-index', $('#ui-datepicker-div').css('z-index'));
162 // Integrate tooltip text into dialog
163 var tooltip = $thisElement.tooltip('instance');
164 if (typeof tooltip !== 'undefined') {
166 var $note = $('<p class="note"></div>');
167 $note.text(tooltip.option('content'));
168 $('div.ui-datepicker').append($note);
172 onSelect: function () {
173 $thisElement.data('datepicker').inline = true;
175 onClose: function () {
176 // The value is no more from the date picker
177 $thisElement.data('comes_from', '');
178 if (typeof $thisElement.data('datepicker') !== 'undefined') {
179 $thisElement.data('datepicker').inline = false;
181 var tooltip = $thisElement.tooltip('instance');
182 if (typeof tooltip !== 'undefined') {
187 if (type === 'time') {
188 $thisElement.timepicker($.extend(defaultOptions, options));
189 // Add a tip regarding entering MySQL allowed-values for TIME data-type
190 Functions.tooltip($thisElement, 'input', Messages.strMysqlAllowedValuesTipTime);
192 $thisElement.datetimepicker($.extend(defaultOptions, options));
197 * Add a date/time picker to each element that needs it
198 * (only when jquery-ui-timepicker-addon.js is loaded)
200 Functions.addDateTimePicker = function () {
201 if ($.timepicker !== undefined) {
202 $('input.timefield, input.datefield, input.datetimefield').each(function () {
203 var decimals = $(this).parent().attr('data-decimals');
204 var type = $(this).parent().attr('data-type');
206 var showMillisec = false;
207 var showMicrosec = false;
208 var timeFormat = 'HH:mm:ss';
210 // check for decimal places of seconds
211 if (decimals > 0 && type.indexOf('time') !== -1) {
215 timeFormat = 'HH:mm:ss.lc';
218 timeFormat = 'HH:mm:ss.l';
221 if (type === 'time') {
224 Functions.addDatepicker($(this), type, {
225 showMillisec: showMillisec,
226 showMicrosec: showMicrosec,
227 timeFormat: timeFormat,
229 firstDay: firstDayOfCalendar
231 // Add a tip regarding entering MySQL allowed-values
232 // for TIME and DATE data-type
233 if ($(this).hasClass('timefield')) {
234 Functions.tooltip($(this), 'input', Messages.strMysqlAllowedValuesTipTime);
235 } else if ($(this).hasClass('datefield')) {
236 Functions.tooltip($(this), 'input', Messages.strMysqlAllowedValuesTipDate);
243 * Handle redirect and reload flags sent as part of AJAX requests
245 * @param data ajax response data
247 Functions.handleRedirectAndReload = function (data) {
248 if (parseInt(data.redirect_flag) === 1) {
249 // add one more GET param to display session expiry msg
250 if (window.location.href.indexOf('?') === -1) {
251 window.location.href += '?session_expired=1';
253 window.location.href += CommonParams.get('arg_separator') + 'session_expired=1';
255 window.location.reload();
256 } else if (parseInt(data.reload_flag) === 1) {
257 window.location.reload();
262 * Creates an SQL editor which supports auto completing etc.
264 * @param $textarea jQuery object wrapping the textarea to be made the editor
265 * @param options optional options for CodeMirror
266 * @param resize optional resizing ('vertical', 'horizontal', 'both')
267 * @param lintOptions additional options for lint
269 Functions.getSqlEditor = function ($textarea, options, resize, lintOptions) {
270 var resizeType = resize;
271 if ($textarea.length > 0 && typeof CodeMirror !== 'undefined') {
272 // merge options for CodeMirror
276 extraKeys: { 'Ctrl-Space': 'autocomplete' },
277 hintOptions: { 'completeSingle': false, 'completeOnSingleClick': true },
279 mode: 'text/x-mysql',
283 if (CodeMirror.sqlLint) {
285 gutters: ['CodeMirror-lint-markers'],
287 'getAnnotations': CodeMirror.sqlLint,
289 'lintOptions': lintOptions
294 $.extend(true, defaults, options);
296 // create CodeMirror editor
297 var codemirrorEditor = CodeMirror.fromTextArea($textarea[0], defaults);
300 resizeType = 'vertical';
303 if (resizeType === 'vertical') {
306 if (resizeType === 'both') {
309 if (resizeType === 'horizontal') {
312 $(codemirrorEditor.getWrapperElement())
313 .css('resize', resizeType)
316 resize: function () {
317 codemirrorEditor.setSize($(this).width(), $(this).height());
320 // enable autocomplete
321 codemirrorEditor.on('inputRead', Functions.codeMirrorAutoCompleteOnInputRead);
324 codemirrorEditor.on('change', function (e) {
327 content: codemirrorEditor.isClean(),
329 AJAX.lockPageHandler(e);
332 return codemirrorEditor;
338 * Clear text selection
340 Functions.clearSelection = function () {
341 if (document.selection && document.selection.empty) {
342 document.selection.empty();
343 } else if (window.getSelection) {
344 var sel = window.getSelection();
348 if (sel.removeAllRanges) {
349 sel.removeAllRanges();
355 * Create a jQuery UI tooltip
357 * @param $elements jQuery object representing the elements
358 * @param item the item
359 * (see https://api.jqueryui.com/tooltip/#option-items)
360 * @param myContent content of the tooltip
361 * @param additionalOptions to override the default options
364 Functions.tooltip = function ($elements, item, myContent, additionalOptions) {
365 if ($('#no_hint').length > 0) {
369 var defaultOptions = {
372 tooltipClass: 'tooltip',
378 $elements.tooltip($.extend(true, defaultOptions, additionalOptions));
384 Functions.escapeHtml = function (unsafe) {
385 if (typeof(unsafe) !== 'undefined') {
388 .replace(/&/g, '&')
389 .replace(/</g, '<')
390 .replace(/>/g, '>')
391 .replace(/"/g, '"')
392 .replace(/'/g, ''');
398 Functions.escapeJsString = function (unsafe) {
399 if (typeof(unsafe) !== 'undefined') {
403 .replace('\\', '\\\\')
404 .replace('\'', '\\\'')
405 .replace(''', '\\'')
407 .replace('"', '\\"')
410 .replace(/<\/script/gi, '</\' + \'script');
416 Functions.escapeBacktick = function (s) {
417 return s.replace('`', '``');
420 Functions.escapeSingleQuote = function (s) {
421 return s.replace('\\', '\\\\').replace('\'', '\\\'');
424 Functions.sprintf = function () {
425 return sprintf.apply(this, arguments);
429 * Hides/shows the default value input field, depending on the default type
430 * Ticks the NULL checkbox if NULL is chosen as default value.
432 Functions.hideShowDefaultValue = function ($defaultType) {
433 if ($defaultType.val() === 'USER_DEFINED') {
434 $defaultType.siblings('.default_value').show().trigger('focus');
436 $defaultType.siblings('.default_value').hide();
437 if ($defaultType.val() === 'NULL') {
438 var $nullCheckbox = $defaultType.closest('tr').find('.allow_null');
439 $nullCheckbox.prop('checked', true);
445 * Hides/shows the input field for column expression based on whether
446 * VIRTUAL/PERSISTENT is selected
448 * @param $virtuality virtuality dropdown
450 Functions.hideShowExpression = function ($virtuality) {
451 if ($virtuality.val() === '') {
452 $virtuality.siblings('.expression').hide();
454 $virtuality.siblings('.expression').show();
459 * Show notices for ENUM columns; add/hide the default value
462 Functions.verifyColumnsProperties = function () {
463 $('select.column_type').each(function () {
464 Functions.showNoticeForEnum($(this));
466 $('select.default_type').each(function () {
467 Functions.hideShowDefaultValue($(this));
469 $('select.virtuality').each(function () {
470 Functions.hideShowExpression($(this));
475 * Add a hidden field to the form to indicate that this will be an
476 * Ajax request (only if this hidden field does not exist)
478 * @param $form object the form
480 Functions.prepareForAjaxRequest = function ($form) {
481 if (! $form.find('input:hidden').is('#ajax_request_hidden')) {
482 $form.append('<input type="hidden" id="ajax_request_hidden" name="ajax_request" value="true">');
486 Functions.checkPasswordStrength = function (value, meterObject, meterObjectLabel, username) {
487 // List of words we don't want to appear in the password
496 if (username !== null) {
497 customDict.push(username);
499 var zxcvbnObject = zxcvbn(value, customDict);
500 var strength = zxcvbnObject.score;
501 strength = parseInt(strength);
502 meterObject.val(strength);
504 case 0: meterObjectLabel.html(Messages.strExtrWeak);
506 case 1: meterObjectLabel.html(Messages.strVeryWeak);
508 case 2: meterObjectLabel.html(Messages.strWeak);
510 case 3: meterObjectLabel.html(Messages.strGood);
512 case 4: meterObjectLabel.html(Messages.strStrong);
517 * Generate a new password and copy it to the password input areas
519 * @param {object} passwordForm the form that holds the password fields
521 * @return {boolean} always true
523 Functions.suggestPassword = function (passwordForm) {
524 // restrict the password to just letters and numbers to avoid problems:
525 // "editors and viewers regard the password as multiple words and
526 // things like double click no longer work"
527 var pwchars = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWYXZ@!_.*/()[]-';
528 var passwordlength = 16; // do we want that to be dynamic? no, keep it simple :)
529 var passwd = passwordForm.generated_pw;
530 // eslint-disable-next-line compat/compat
531 var randomWords = new Int32Array(passwordlength);
537 // First we're going to try to use a built-in CSPRNG
538 // eslint-disable-next-line compat/compat
539 if (window.crypto && window.crypto.getRandomValues) {
540 // eslint-disable-next-line compat/compat
541 window.crypto.getRandomValues(randomWords);
542 } else if (window.msCrypto && window.msCrypto.getRandomValues) {
543 // Because of course IE calls it msCrypto instead of being standard
544 window.msCrypto.getRandomValues(randomWords);
546 // Fallback to Math.random
547 for (i = 0; i < passwordlength; i++) {
548 randomWords[i] = Math.floor(Math.random() * pwchars.length);
552 for (i = 0; i < passwordlength; i++) {
553 passwd.value += pwchars.charAt(Math.abs(randomWords[i]) % pwchars.length);
556 var $jQueryPasswordForm = $(passwordForm);
558 passwordForm.elements.pma_pw.value = passwd.value;
559 passwordForm.elements.pma_pw2.value = passwd.value;
560 var meterObj = $jQueryPasswordForm.find('meter[name="pw_meter"]').first();
561 var meterObjLabel = $jQueryPasswordForm.find('span[name="pw_strength"]').first();
562 Functions.checkPasswordStrength(passwd.value, meterObj, meterObjLabel);
567 * Version string to integer conversion.
569 Functions.parseVersionString = function (str) {
570 if (typeof(str) !== 'string') {
574 // Parse possible alpha/beta/rc/
575 var state = str.split('-');
576 if (state.length >= 2) {
577 if (state[1].substr(0, 2) === 'rc') {
578 add = - 20 - parseInt(state[1].substr(2), 10);
579 } else if (state[1].substr(0, 4) === 'beta') {
580 add = - 40 - parseInt(state[1].substr(4), 10);
581 } else if (state[1].substr(0, 5) === 'alpha') {
582 add = - 60 - parseInt(state[1].substr(5), 10);
583 } else if (state[1].substr(0, 3) === 'dev') {
584 /* We don't handle dev, it's git snapshot */
589 var x = str.split('.');
590 // Use 0 for non existing parts
591 var maj = parseInt(x[0], 10) || 0;
592 var min = parseInt(x[1], 10) || 0;
593 var pat = parseInt(x[2], 10) || 0;
594 var hotfix = parseInt(x[3], 10) || 0;
595 return maj * 100000000 + min * 1000000 + pat * 10000 + hotfix * 100 + add;
599 * Indicates current available version on main page.
601 Functions.currentVersion = function (data) {
602 if (data && data.version && data.date) {
603 var current = Functions.parseVersionString($('span.version').text());
604 var latest = Functions.parseVersionString(data.version);
605 var url = 'https://www.phpmyadmin.net/files/' + Functions.escapeHtml(encodeURIComponent(data.version)) + '/';
606 var versionInformationMessage = document.createElement('span');
607 versionInformationMessage.className = 'latest';
608 var versionInformationMessageLink = document.createElement('a');
609 versionInformationMessageLink.href = url;
610 versionInformationMessageLink.className = 'disableAjax';
611 var versionInformationMessageLinkText = document.createTextNode(data.version);
612 versionInformationMessageLink.appendChild(versionInformationMessageLinkText);
613 var prefixMessage = document.createTextNode(Messages.strLatestAvailable + ' ');
614 versionInformationMessage.appendChild(prefixMessage);
615 versionInformationMessage.appendChild(versionInformationMessageLink);
616 if (latest > current) {
617 var message = Functions.sprintf(
618 Messages.strNewerVersion,
619 Functions.escapeHtml(data.version),
620 Functions.escapeHtml(data.date)
622 var htmlClass = 'alert alert-primary';
623 if (Math.floor(latest / 10000) === Math.floor(current / 10000)) {
624 /* Security update */
625 htmlClass = 'alert alert-danger';
627 $('#newer_version_notice').remove();
628 var mainContainerDiv = document.createElement('div');
629 mainContainerDiv.id = 'newer_version_notice';
630 mainContainerDiv.className = htmlClass;
631 var mainContainerDivLink = document.createElement('a');
632 mainContainerDivLink.href = url;
633 mainContainerDivLink.className = 'disableAjax';
634 var mainContainerDivLinkText = document.createTextNode(message);
635 mainContainerDivLink.appendChild(mainContainerDivLinkText);
636 mainContainerDiv.appendChild(mainContainerDivLink);
637 $('#maincontainer').append($(mainContainerDiv));
639 if (latest === current) {
640 versionInformationMessage = document.createTextNode(' (' + Messages.strUpToDate + ')');
642 /* Remove extra whitespace */
643 var versionInfo = $('#li_pma_version').contents().get(2);
644 if (typeof versionInfo !== 'undefined') {
645 versionInfo.textContent = $.trim(versionInfo.textContent);
647 var $liPmaVersion = $('#li_pma_version');
648 $liPmaVersion.find('span.latest').remove();
649 $liPmaVersion.append($(versionInformationMessage));
654 * Loads Git revision data from ajax for index.php
656 Functions.displayGitRevision = function () {
657 $('#is_git_revision').remove();
658 $('#li_pma_version_git').remove();
660 'index.php?route=/git-revision',
662 'server': CommonParams.get('server'),
663 'ajax_request': true,
667 if (typeof data !== 'undefined' && data.success === true) {
668 $(data.message).insertAfter('#li_pma_version');
675 * for PhpMyAdmin\Display\ChangePassword and /user-password
677 Functions.displayPasswordGenerateButton = function () {
678 var generatePwdRow = $('<tr></tr>').addClass('vmiddle');
679 $('<td></td>').html(Messages.strGeneratePassword).appendTo(generatePwdRow);
680 var pwdCell = $('<td></td>').appendTo(generatePwdRow);
681 var pwdButton = $('<input>')
682 .attr({ type: 'button', id: 'button_generate_password', value: Messages.strGenerate })
683 .addClass('btn btn-secondary button')
684 .on('click', function () {
685 Functions.suggestPassword(this.form);
687 var pwdTextbox = $('<input>')
688 .attr({ type: 'text', name: 'generated_pw', id: 'generated_pw' });
689 pwdCell.append(pwdButton).append(pwdTextbox);
691 if (document.getElementById('button_generate_password') === null) {
692 $('#tr_element_before_generate_password').parent().append(generatePwdRow);
695 var generatePwdDiv = $('<div></div>').addClass('item');
696 $('<label></label>').attr({ for: 'button_generate_password' })
697 .html(Messages.strGeneratePassword + ':')
698 .appendTo(generatePwdDiv);
699 var optionsSpan = $('<span></span>').addClass('options')
700 .appendTo(generatePwdDiv);
701 pwdButton.clone(true).appendTo(optionsSpan);
702 pwdTextbox.clone(true).appendTo(generatePwdDiv);
704 if (document.getElementById('button_generate_password') === null) {
705 $('#div_element_before_generate_password').parent().append(generatePwdDiv);
710 * selects the content of a given object, f.e. a textarea
712 * @param {object} element element of which the content will be selected
713 * @param {var} lock variable which holds the lock for this element or true, if no lock exists
714 * @param {boolean} onlyOnce boolean if true this is only done once f.e. only on first focus
716 Functions.selectContent = function (element, lock, onlyOnce) {
717 if (onlyOnce && onlyOnceElements[element.name]) {
721 onlyOnceElements[element.name] = true;
731 * Displays a confirmation box before submitting a "DROP/DELETE/ALTER" query.
732 * This function is called while clicking links
734 * @param theLink object the link
735 * @param theSqlQuery object the sql query to submit
737 * @return boolean whether to run the query or not
739 Functions.confirmLink = function (theLink, theSqlQuery) {
740 // Confirmation is not required in the configuration file
741 // or browser is Opera (crappy js implementation)
742 if (Messages.strDoYouReally === '' || typeof(window.opera) !== 'undefined') {
746 var isConfirmed = confirm(Functions.sprintf(Messages.strDoYouReally, theSqlQuery));
748 if (typeof(theLink.href) !== 'undefined') {
749 theLink.href += CommonParams.get('arg_separator') + 'is_js_confirmed=1';
750 } else if (typeof(theLink.form) !== 'undefined') {
751 theLink.form.action += '?is_js_confirmed=1';
759 * Confirms a "DROP/DELETE/ALTER" query before
760 * submitting it if required.
761 * This function is called by the 'Functions.checkSqlQuery()' js function.
763 * @param theForm1 object the form
764 * @param sqlQuery1 string the sql query string
766 * @return boolean whether to run the query or not
768 * @see Functions.checkSqlQuery()
770 Functions.confirmQuery = function (theForm1, sqlQuery1) {
771 // Confirmation is not required in the configuration file
772 if (Messages.strDoYouReally === '') {
776 // Confirms a "DROP/DELETE/ALTER/TRUNCATE" statement
778 // TODO: find a way (if possible) to use the parser-analyser
779 // for this kind of verification
780 // For now, I just added a ^ to check for the statement at
781 // beginning of expression
783 var doConfirmRegExp0 = new RegExp('^\\s*DROP\\s+(IF EXISTS\\s+)?(TABLE|PROCEDURE)\\s', 'i');
784 var doConfirmRegExp1 = new RegExp('^\\s*ALTER\\s+TABLE\\s+((`[^`]+`)|([A-Za-z0-9_$]+))\\s+DROP\\s', 'i');
785 var doConfirmRegExp2 = new RegExp('^\\s*DELETE\\s+FROM\\s', 'i');
786 var doConfirmRegExp3 = new RegExp('^\\s*TRUNCATE\\s', 'i');
787 var doConfirmRegExp4 = new RegExp('^(?=.*UPDATE\\b)^((?!WHERE).)*$', 'i');
789 if (doConfirmRegExp0.test(sqlQuery1) ||
790 doConfirmRegExp1.test(sqlQuery1) ||
791 doConfirmRegExp2.test(sqlQuery1) ||
792 doConfirmRegExp3.test(sqlQuery1) ||
793 doConfirmRegExp4.test(sqlQuery1)) {
795 if (sqlQuery1.length > 100) {
796 message = sqlQuery1.substr(0, 100) + '\n ...';
800 var isConfirmed = confirm(Functions.sprintf(Messages.strDoYouReally, message));
801 // statement is confirmed -> update the
802 // "is_js_confirmed" form field so the confirm test won't be
803 // run on the server side and allows to submit the form
805 theForm1.elements.is_js_confirmed.value = 1;
808 // statement is rejected -> do not submit the form
811 } // end if (handle confirm box result)
812 } // end if (display confirm box)
818 * Displays an error message if the user submitted the sql query form with no
819 * sql query, else checks for "DROP/DELETE/ALTER" statements
821 * @param theForm object the form
823 * @return boolean always false
825 * @see Functions.confirmQuery()
827 Functions.checkSqlQuery = function (theForm) {
828 // get the textarea element containing the query
830 if (codeMirrorEditor) {
831 codeMirrorEditor.save();
832 sqlQuery = codeMirrorEditor.getValue();
834 sqlQuery = theForm.elements.sql_query.value;
836 var spaceRegExp = new RegExp('\\s+');
837 if (typeof(theForm.elements.sql_file) !== 'undefined' &&
838 theForm.elements.sql_file.value.replace(spaceRegExp, '') !== '') {
841 if (typeof(theForm.elements.id_bookmark) !== 'undefined' &&
842 (theForm.elements.id_bookmark.value !== null || theForm.elements.id_bookmark.value !== '') &&
843 theForm.elements.id_bookmark.selectedIndex !== 0) {
847 // Checks for "DROP/DELETE/ALTER" statements
848 if (sqlQuery.replace(spaceRegExp, '') !== '') {
849 result = Functions.confirmQuery(theForm, sqlQuery);
851 alert(Messages.strFormEmpty);
854 if (codeMirrorEditor) {
855 codeMirrorEditor.focus();
856 } else if (codeMirrorInlineEditor) {
857 codeMirrorInlineEditor.focus();
863 * Check if a form's element is empty.
864 * An element containing only spaces is also considered empty
866 * @param {object} theForm the form
867 * @param {string} theFieldName the name of the form field to put the focus on
869 * @return {boolean} whether the form field is empty or not
871 Functions.emptyCheckTheField = function (theForm, theFieldName) {
872 var theField = theForm.elements[theFieldName];
873 var spaceRegExp = new RegExp('\\s+');
874 return theField.value.replace(spaceRegExp, '') === '';
878 * Ensures a value submitted in a form is numeric and is in a range
880 * @param object the form
881 * @param string the name of the form field to check
882 * @param integer the minimum authorized value
883 * @param integer the maximum authorized value
885 * @return boolean whether a valid number has been submitted or not
887 Functions.checkFormElementInRange = function (theForm, theFieldName, message, minimum, maximum) {
888 var theField = theForm.elements[theFieldName];
889 var val = parseInt(theField.value, 10);
891 var max = Number.MAX_VALUE;
893 if (typeof(minimum) !== 'undefined') {
896 if (typeof(maximum) !== 'undefined' && maximum !== null) {
902 alert(Messages.strEnterValidNumber);
905 } else if (val < min || val > max) {
907 alert(Functions.sprintf(message, val));
911 theField.value = val;
916 Functions.checkTableEditForm = function (theForm, fieldsCnt) {
917 // TODO: avoid sending a message if user just wants to add a line
918 // on the form but has not completed at least one field name
920 var atLeastOneField = 0;
928 for (i = 0; i < fieldsCnt; i++) {
929 id = '#field_' + i + '_2';
932 if (val === 'VARCHAR' || val === 'CHAR' || val === 'BIT' || val === 'VARBINARY' || val === 'BINARY') {
933 elm2 = $('#field_' + i + '_3');
934 val = parseInt(elm2.val(), 10);
935 elm3 = $('#field_' + i + '_1');
936 if (isNaN(val) && elm3.val() !== '') {
938 alert(Messages.strEnterValidLength);
944 if (atLeastOneField === 0) {
945 id = 'field_' + i + '_1';
946 if (!Functions.emptyCheckTheField(theForm, id)) {
951 if (atLeastOneField === 0) {
952 var theField = theForm.elements.field_0_1;
953 alert(Messages.strFormEmpty);
958 // at least this section is under jQuery
959 var $input = $('input.textfield[name=\'table\']');
960 if ($input.val() === '') {
961 alert(Messages.strFormEmpty);
962 $input.trigger('focus');
970 * True if last click is to check a row.
972 var lastClickChecked = false;
975 * Zero-based index of last clicked row.
976 * Used to handle the shift + click event in the code above.
978 var lastClickedRow = -1;
981 * Zero-based index of last shift clicked row.
983 var lastShiftClickedRow = -1;
985 var idleSecondsCounter = 0;
988 AJAX.registerTeardown('functions.js', function () {
989 clearTimeout(updateTimeout);
990 clearInterval(incInterval);
991 $(document).off('mousemove');
994 AJAX.registerOnload('functions.js', function () {
995 document.onclick = function () {
996 idleSecondsCounter = 0;
998 $(document).on('mousemove',function () {
999 idleSecondsCounter = 0;
1001 document.onkeypress = function () {
1002 idleSecondsCounter = 0;
1006 return Math.floor((1 + Math.random()) * 0x10000)
1010 return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
1011 s4() + '-' + s4() + s4() + s4();
1014 function SetIdleTime () {
1015 idleSecondsCounter++;
1017 function UpdateIdleTime () {
1018 var href = 'index.php?route=/';
1019 var guid = 'default';
1020 if (isStorageSupported('sessionStorage')) {
1021 guid = window.sessionStorage.guid;
1024 'ajax_request' : true,
1025 'server' : CommonParams.get('server'),
1026 'db' : CommonParams.get('db'),
1028 'access_time': idleSecondsCounter,
1035 success: function (data) {
1037 if (CommonParams.get('LoginCookieValidity') - idleSecondsCounter < 0) {
1038 /* There is other active window, let's reset counter */
1039 idleSecondsCounter = 0;
1041 var remaining = Math.min(
1042 /* Remaining login validity */
1043 CommonParams.get('LoginCookieValidity') - idleSecondsCounter,
1044 /* Remaining time till session GC */
1045 CommonParams.get('session_gc_maxlifetime')
1047 var interval = 1000;
1048 if (remaining > 5) {
1049 // max value for setInterval() function
1050 interval = Math.min((remaining - 1) * 1000, Math.pow(2, 31) - 1);
1052 updateTimeout = window.setTimeout(UpdateIdleTime, interval);
1053 } else { // timeout occurred
1054 clearInterval(incInterval);
1055 if (isStorageSupported('sessionStorage')) {
1056 window.sessionStorage.clear();
1058 // append the login form on the page, disable all the forms which were not disabled already, close all the open jqueryui modal boxes
1059 if (!$('#modalOverlay').length) {
1060 $('fieldset').not(':disabled').attr('disabled', 'disabled').addClass('disabled_for_expiration');
1061 $('body').append(data.error);
1062 $('.ui-dialog').each(function () {
1063 $('#' + $(this).attr('aria-describedby')).dialog('close');
1065 $('#input_username').trigger('focus');
1067 CommonParams.set('token', data.new_token);
1068 $('input[name=token]').val(data.new_token);
1070 idleSecondsCounter = 0;
1071 Functions.handleRedirectAndReload(data);
1076 if (CommonParams.get('logged_in')) {
1077 incInterval = window.setInterval(SetIdleTime, 1000);
1078 var sessionTimeout = Math.min(
1079 CommonParams.get('LoginCookieValidity'),
1080 CommonParams.get('session_gc_maxlifetime')
1082 if (isStorageSupported('sessionStorage')) {
1083 window.sessionStorage.setItem('guid', guid());
1085 var interval = (sessionTimeout - 5) * 1000;
1086 if (interval > Math.pow(2, 31) - 1) { // max value for setInterval() function
1087 interval = Math.pow(2, 31) - 1;
1089 updateTimeout = window.setTimeout(UpdateIdleTime, interval);
1093 * Unbind all event handlers before tearing down a page
1095 AJAX.registerTeardown('functions.js', function () {
1096 $(document).off('click', 'input:checkbox.checkall');
1099 AJAX.registerOnload('functions.js', function () {
1101 * Row marking in horizontal mode (use "on" so that it works also for
1102 * next pages reached via AJAX); a tr may have the class noclick to remove
1106 $(document).on('click', 'input:checkbox.checkall', function (e) {
1107 var $this = $(this);
1108 var $tr = $this.closest('tr');
1109 var $table = $this.closest('table');
1111 if (!e.shiftKey || lastClickedRow === -1) {
1114 var $checkbox = $tr.find(':checkbox.checkall');
1115 var checked = $this.prop('checked');
1116 $checkbox.prop('checked', checked).trigger('change');
1118 $tr.addClass('marked');
1120 $tr.removeClass('marked');
1122 lastClickChecked = checked;
1124 // remember the last clicked row
1125 lastClickedRow = lastClickChecked ? $table.find('tbody tr:not(.noclick)').index($tr) : -1;
1126 lastShiftClickedRow = -1;
1128 // handle the shift click
1129 Functions.clearSelection();
1133 // clear last shift click result
1134 if (lastShiftClickedRow >= 0) {
1135 if (lastShiftClickedRow >= lastClickedRow) {
1136 start = lastClickedRow;
1137 end = lastShiftClickedRow;
1139 start = lastShiftClickedRow;
1140 end = lastClickedRow;
1142 $tr.parent().find('tr:not(.noclick)')
1143 .slice(start, end + 1)
1144 .removeClass('marked')
1146 .prop('checked', false)
1150 // handle new shift click
1151 var currRow = $table.find('tbody tr:not(.noclick)').index($tr);
1152 if (currRow >= lastClickedRow) {
1153 start = lastClickedRow;
1157 end = lastClickedRow;
1159 $tr.parent().find('tr:not(.noclick)')
1160 .slice(start, end + 1)
1163 .prop('checked', true)
1166 // remember the last shift clicked row
1167 lastShiftClickedRow = currRow;
1171 Functions.addDateTimePicker();
1174 * Add attribute to text boxes for iOS devices (based on bugID: 3508912)
1176 if (navigator.userAgent.match(/(iphone|ipod|ipad)/i)) {
1177 $('input[type=text]').attr('autocapitalize', 'off').attr('autocorrect', 'off');
1182 * Checks/unchecks all options of a <select> element
1184 * @param string the form name
1185 * @param string the element name
1186 * @param boolean whether to check or to uncheck options
1188 * @return boolean always true
1190 Functions.setSelectOptions = function (theForm, theSelect, doCheck) {
1191 $('form[name=\'' + theForm + '\'] select[name=\'' + theSelect + '\']').find('option').prop('selected', doCheck);
1196 * Sets current value for query box.
1198 Functions.setQuery = function (query) {
1199 if (codeMirrorEditor) {
1200 codeMirrorEditor.setValue(query);
1201 codeMirrorEditor.focus();
1202 } else if (document.sqlform) {
1203 document.sqlform.sql_query.value = query;
1204 document.sqlform.sql_query.focus();
1209 * Handles 'Simulate query' button on SQL query box.
1213 Functions.handleSimulateQueryButton = function () {
1214 var updateRegExp = new RegExp('^\\s*UPDATE\\s+((`[^`]+`)|([A-Za-z0-9_$]+))\\s+SET\\s', 'i');
1215 var deleteRegExp = new RegExp('^\\s*DELETE\\s+FROM\\s', 'i');
1218 if (codeMirrorEditor) {
1219 query = codeMirrorEditor.getValue();
1221 query = $('#sqlquery').val();
1224 var $simulateDml = $('#simulate_dml');
1225 if (updateRegExp.test(query) || deleteRegExp.test(query)) {
1226 if (! $simulateDml.length) {
1227 $('#button_submit_query')
1228 .before('<input type="button" id="simulate_dml"' +
1229 'tabindex="199" class="btn btn-primary" value="' +
1230 Messages.strSimulateDML +
1234 if ($simulateDml.length) {
1235 $simulateDml.remove();
1241 * Create quick sql statements.
1244 Functions.insertQuery = function (queryType) {
1245 if (queryType === 'clear') {
1246 Functions.setQuery('');
1248 } else if (queryType === 'format') {
1249 if (codeMirrorEditor) {
1250 $('#querymessage').html(Messages.strFormatting +
1251 ' <img class="ajaxIcon" src="' +
1252 pmaThemeImage + 'ajax_clock_small.gif" alt="">');
1254 'ajax_request': true,
1255 'sql': codeMirrorEditor.getValue(),
1256 'server': CommonParams.get('server')
1260 url: 'index.php?route=/database/sql/format',
1262 success: function (data) {
1264 codeMirrorEditor.setValue(data.sql);
1266 $('#querymessage').html('');
1271 } else if (queryType === 'saved') {
1272 if (isStorageSupported('localStorage') && typeof window.localStorage.autoSavedSql !== 'undefined') {
1273 Functions.setQuery(window.localStorage.autoSavedSql);
1274 } else if (Cookies.get('autoSavedSql')) {
1275 Functions.setQuery(Cookies.get('autoSavedSql'));
1277 Functions.ajaxShowMessage(Messages.strNoAutoSavedQuery);
1283 var myListBox = document.sqlform.dummy;
1284 var table = document.sqlform.table.value;
1286 if (myListBox.options.length > 0) {
1287 sqlBoxLocked = true;
1288 var columnsList = '';
1292 for (var i = 0; i < myListBox.options.length; i++) {
1295 columnsList += ', ';
1299 columnsList += myListBox.options[i].value;
1300 valDis += '[value-' + NbSelect + ']';
1301 editDis += myListBox.options[i].value + '=[value-' + NbSelect + ']';
1303 if (queryType === 'selectall') {
1304 query = 'SELECT * FROM `' + table + '` WHERE 1';
1305 } else if (queryType === 'select') {
1306 query = 'SELECT ' + columnsList + ' FROM `' + table + '` WHERE 1';
1307 } else if (queryType === 'insert') {
1308 query = 'INSERT INTO `' + table + '`(' + columnsList + ') VALUES (' + valDis + ')';
1309 } else if (queryType === 'update') {
1310 query = 'UPDATE `' + table + '` SET ' + editDis + ' WHERE 1';
1311 } else if (queryType === 'delete') {
1312 query = 'DELETE FROM `' + table + '` WHERE 0';
1314 Functions.setQuery(query);
1315 sqlBoxLocked = false;
1320 * Inserts multiple fields.
1323 Functions.insertValueQuery = function () {
1324 var myQuery = document.sqlform.sql_query;
1325 var myListBox = document.sqlform.dummy;
1327 if (myListBox.options.length > 0) {
1328 sqlBoxLocked = true;
1329 var columnsList = '';
1331 for (var i = 0; i < myListBox.options.length; i++) {
1332 if (myListBox.options[i].selected) {
1335 columnsList += ', ';
1337 columnsList += myListBox.options[i].value;
1341 /* CodeMirror support */
1342 if (codeMirrorEditor) {
1343 codeMirrorEditor.replaceSelection(columnsList);
1344 codeMirrorEditor.focus();
1346 } else if (document.selection) {
1348 var sel = document.selection.createRange();
1349 sel.text = columnsList;
1350 // MOZILLA/NETSCAPE support
1351 } else if (document.sqlform.sql_query.selectionStart || document.sqlform.sql_query.selectionStart === '0') {
1352 var startPos = document.sqlform.sql_query.selectionStart;
1353 var endPos = document.sqlform.sql_query.selectionEnd;
1354 var SqlString = document.sqlform.sql_query.value;
1356 myQuery.value = SqlString.substring(0, startPos) + columnsList + SqlString.substring(endPos, SqlString.length);
1359 myQuery.value += columnsList;
1361 sqlBoxLocked = false;
1366 * Updates the input fields for the parameters based on the query
1368 Functions.updateQueryParameters = function () {
1369 if ($('#parameterized').is(':checked')) {
1370 var query = codeMirrorEditor ? codeMirrorEditor.getValue() : $('#sqlquery').val();
1372 var allParameters = query.match(/:[a-zA-Z0-9_]+/g);
1373 var parameters = [];
1374 // get unique parameters
1375 if (allParameters) {
1376 $.each(allParameters, function (i, parameter) {
1377 if ($.inArray(parameter, parameters) === -1) {
1378 parameters.push(parameter);
1382 $('#parametersDiv').text(Messages.strNoParam);
1386 var $temp = $('<div></div>');
1387 $temp.append($('#parametersDiv').children());
1388 $('#parametersDiv').empty();
1390 $.each(parameters, function (i, parameter) {
1391 var paramName = parameter.substring(1);
1392 var $param = $temp.find('#paramSpan_' + paramName);
1393 if (! $param.length) {
1394 $param = $('<span class="parameter" id="paramSpan_' + paramName + '"></span>');
1395 $('<label for="param_' + paramName + '"></label>').text(parameter).appendTo($param);
1396 $('<input type="text" name="parameters[' + parameter + ']" id="param_' + paramName + '">').appendTo($param);
1398 $('#parametersDiv').append($param);
1401 $('#parametersDiv').empty();
1406 * Refresh/resize the WYSIWYG scratchboard
1408 Functions.refreshLayout = function () {
1409 var $elm = $('#pdflayout');
1410 var orientation = $('#orientation_opt').val();
1412 var $paperOpt = $('#paper_opt');
1413 if ($paperOpt.length === 1) {
1414 paper = $paperOpt.val();
1418 if (orientation === 'P') {
1422 $elm.css('width', Functions.pdfPaperSize(paper, posa) + 'px');
1423 $elm.css('height', Functions.pdfPaperSize(paper, posb) + 'px');
1427 * Initializes positions of elements.
1429 Functions.tableDragInit = function () {
1430 $('.pdflayout_table').each(function () {
1431 var $this = $(this);
1432 var number = $this.data('number');
1433 var x = $('#c_table_' + number + '_x').val();
1434 var y = $('#c_table_' + number + '_y').val();
1435 $this.css('left', x + 'px');
1436 $this.css('top', y + 'px');
1437 /* Make elements draggable */
1439 containment: 'parent',
1440 drag: function (evt, ui) {
1441 var number = $this.data('number');
1442 $('#c_table_' + number + '_x').val(parseInt(ui.position.left, 10));
1443 $('#c_table_' + number + '_y').val(parseInt(ui.position.top, 10));
1450 * Resets drag and drop positions.
1452 Functions.resetDrag = function () {
1453 $('.pdflayout_table').each(function () {
1454 var $this = $(this);
1455 var x = $this.data('x');
1456 var y = $this.data('y');
1457 $this.css('left', x + 'px');
1458 $this.css('top', y + 'px');
1463 * User schema handlers.
1466 /* Move in scratchboard on manual change */
1467 $(document).on('change', '.position-change', function () {
1468 var $this = $(this);
1469 var $elm = $('#table_' + $this.data('number'));
1470 $elm.css($this.data('axis'), $this.val() + 'px');
1472 /* Refresh on paper size/orientation change */
1473 $(document).on('change', '.paper-change', function () {
1474 var $elm = $('#pdflayout');
1475 if ($elm.css('visibility') === 'visible') {
1476 Functions.refreshLayout();
1477 Functions.tableDragInit();
1480 /* Show/hide the WYSIWYG scratchboard */
1481 $(document).on('click', '#toggle-dragdrop', function () {
1482 var $elm = $('#pdflayout');
1483 if ($elm.css('visibility') === 'hidden') {
1484 Functions.refreshLayout();
1485 Functions.tableDragInit();
1486 $elm.css('visibility', 'visible');
1487 $elm.css('display', 'block');
1488 $('#showwysiwyg').val('1');
1490 $elm.css('visibility', 'hidden');
1491 $elm.css('display', 'none');
1492 $('#showwysiwyg').val('0');
1495 /* Reset scratchboard */
1496 $(document).on('click', '#reset-dragdrop', function () {
1497 Functions.resetDrag();
1502 * Returns paper sizes for a given format
1504 Functions.pdfPaperSize = function (format, axis) {
1505 switch (format.toUpperCase()) {
1756 * Get checkbox for foreign key checks
1760 Functions.getForeignKeyCheckboxLoader = function () {
1763 html += '<div class="load-default-fk-check-value">';
1764 html += Functions.getImage('ajax_clock_small');
1770 Functions.loadForeignKeyCheckbox = function () {
1771 // Load default foreign key check value
1773 'ajax_request': true,
1774 'server': CommonParams.get('server'),
1776 $.get('index.php?route=/sql/get-default-fk-check-value', params, function (data) {
1777 var html = '<input type="hidden" name="fk_checks" value="0">' +
1778 '<input type="checkbox" name="fk_checks" id="fk_checks"' +
1779 (data.default_fk_check_value ? ' checked="checked"' : '') + '>' +
1780 '<label for="fk_checks">' + Messages.strForeignKeyCheck + '</label>';
1781 $('.load-default-fk-check-value').replaceWith(html);
1785 Functions.getJsConfirmCommonParam = function (elem, parameters) {
1786 var $elem = $(elem);
1787 var params = parameters;
1788 var sep = CommonParams.get('arg_separator');
1790 // Strip possible leading ?
1791 if (params.substring(0,1) === '?') {
1792 params = params.substr(1);
1798 params += 'is_js_confirmed=1' + sep + 'ajax_request=true' + sep + 'fk_checks=' + ($elem.find('#fk_checks').is(':checked') ? 1 : 0);
1803 * Unbind all event handlers before tearing down a page
1805 AJAX.registerTeardown('functions.js', function () {
1806 $(document).off('click', 'a.inline_edit_sql');
1807 $(document).off('click', 'input#sql_query_edit_save');
1808 $(document).off('click', 'input#sql_query_edit_discard');
1809 $('input.sqlbutton').off('click');
1810 if (codeMirrorEditor) {
1811 codeMirrorEditor.off('blur');
1813 $(document).off('blur', '#sqlquery');
1815 $(document).off('change', '#parameterized');
1816 $(document).off('click', 'input.sqlbutton');
1817 $('#sqlquery').off('keydown');
1818 $('#sql_query_edit').off('keydown');
1820 if (codeMirrorInlineEditor) {
1821 // Copy the sql query to the text area to preserve it.
1822 $('#sql_query_edit').text(codeMirrorInlineEditor.getValue());
1823 $(codeMirrorInlineEditor.getWrapperElement()).off('keydown');
1824 codeMirrorInlineEditor.toTextArea();
1825 codeMirrorInlineEditor = false;
1827 if (codeMirrorEditor) {
1828 $(codeMirrorEditor.getWrapperElement()).off('keydown');
1833 * Jquery Coding for inline editing SQL_QUERY
1835 AJAX.registerOnload('functions.js', function () {
1836 // If we are coming back to the page by clicking forward button
1837 // of the browser, bind the code mirror to inline query editor.
1838 Functions.bindCodeMirrorToInlineEditor();
1839 $(document).on('click', 'a.inline_edit_sql', function () {
1840 if ($('#sql_query_edit').length) {
1841 // An inline query editor is already open,
1842 // we don't want another copy of it
1846 var $form = $(this).prev('form');
1847 var sqlQuery = $form.find('input[name=\'sql_query\']').val().trim();
1848 var $innerSql = $(this).parent().prev().find('code.sql');
1850 var newContent = '<textarea name="sql_query_edit" id="sql_query_edit">' + Functions.escapeHtml(sqlQuery) + '</textarea>\n';
1851 newContent += Functions.getForeignKeyCheckboxLoader();
1852 newContent += '<input type="submit" id="sql_query_edit_save" class="btn btn-secondary button btnSave" value="' + Messages.strGo + '">\n';
1853 newContent += '<input type="button" id="sql_query_edit_discard" class="btn btn-secondary button btnDiscard" value="' + Messages.strCancel + '">\n';
1854 var $editorArea = $('div#inline_editor');
1855 if ($editorArea.length === 0) {
1856 $editorArea = $('<div id="inline_editor_outer"></div>');
1857 $editorArea.insertBefore($innerSql);
1859 $editorArea.html(newContent);
1860 Functions.loadForeignKeyCheckbox();
1863 Functions.bindCodeMirrorToInlineEditor();
1867 $(document).on('click', 'input#sql_query_edit_save', function () {
1868 // hide already existing success message
1870 if (codeMirrorInlineEditor) {
1871 codeMirrorInlineEditor.save();
1872 sqlQuery = codeMirrorInlineEditor.getValue();
1874 sqlQuery = $(this).parent().find('#sql_query_edit').val();
1876 var fkCheck = $(this).parent().find('#fk_checks').is(':checked');
1878 var $form = $('a.inline_edit_sql').prev('form');
1879 var $fakeForm = $('<form>', { action: 'index.php?route=/import', method: 'post' })
1880 .append($form.find('input[name=server], input[name=db], input[name=table], input[name=token]').clone())
1881 .append($('<input>', { type: 'hidden', name: 'show_query', value: 1 }))
1882 .append($('<input>', { type: 'hidden', name: 'is_js_confirmed', value: 0 }))
1883 .append($('<input>', { type: 'hidden', name: 'sql_query', value: sqlQuery }))
1884 .append($('<input>', { type: 'hidden', name: 'fk_checks', value: fkCheck ? 1 : 0 }));
1885 if (! Functions.checkSqlQuery($fakeForm[0])) {
1888 $('.alert-success').hide();
1889 $fakeForm.appendTo($('body')).trigger('submit');
1892 $(document).on('click', 'input#sql_query_edit_discard', function () {
1893 var $divEditor = $('div#inline_editor_outer');
1894 $divEditor.siblings('code.sql').show();
1895 $divEditor.remove();
1898 $(document).on('click', 'input.sqlbutton', function (evt) {
1899 Functions.insertQuery(evt.target.id);
1900 Functions.handleSimulateQueryButton();
1904 $(document).on('change', '#parameterized', Functions.updateQueryParameters);
1906 var $inputUsername = $('#input_username');
1907 if ($inputUsername) {
1908 if ($inputUsername.val() === '') {
1909 $inputUsername.trigger('focus');
1911 $('#input_password').trigger('focus');
1917 * "inputRead" event handler for CodeMirror SQL query editors for autocompletion
1919 Functions.codeMirrorAutoCompleteOnInputRead = function (instance) {
1920 if (!sqlAutoCompleteInProgress
1921 && (!instance.options.hintOptions.tables || !sqlAutoComplete)) {
1922 if (!sqlAutoComplete) {
1923 // Reset after teardown
1924 instance.options.hintOptions.tables = false;
1925 instance.options.hintOptions.defaultTable = '';
1927 sqlAutoCompleteInProgress = true;
1930 'ajax_request': true,
1931 'server': CommonParams.get('server'),
1932 'db': CommonParams.get('db'),
1936 var columnHintRender = function (elem, self, data) {
1937 $('<div class="autocomplete-column-name">')
1938 .text(data.columnName)
1940 $('<div class="autocomplete-column-hint">')
1941 .text(data.columnHint)
1947 url: 'index.php?route=/database/sql/autocomplete',
1949 success: function (data) {
1951 var tables = JSON.parse(data.tables);
1952 sqlAutoCompleteDefaultTable = CommonParams.get('table');
1953 sqlAutoComplete = [];
1954 for (var table in tables) {
1955 if (tables.hasOwnProperty(table)) {
1956 var columns = tables[table];
1961 for (var column in columns) {
1962 if (columns.hasOwnProperty(column)) {
1963 var displayText = columns[column].Type;
1964 if (columns[column].Key === 'PRI') {
1965 displayText += ' | Primary';
1966 } else if (columns[column].Key === 'UNI') {
1967 displayText += ' | Unique';
1969 table.columns.push({
1971 displayText: column + ' | ' + displayText,
1973 columnHint: displayText,
1974 render: columnHintRender
1979 sqlAutoComplete.push(table);
1981 instance.options.hintOptions.tables = sqlAutoComplete;
1982 instance.options.hintOptions.defaultTable = sqlAutoCompleteDefaultTable;
1985 complete: function () {
1986 sqlAutoCompleteInProgress = false;
1990 instance.options.hintOptions.tables = sqlAutoComplete;
1991 instance.options.hintOptions.defaultTable = sqlAutoCompleteDefaultTable;
1994 if (instance.state.completionActive) {
1997 var cur = instance.getCursor();
1998 var token = instance.getTokenAt(cur);
2000 if (token.string.match(/^[.`\w@]\w*$/)) {
2001 string = token.string;
2003 if (string.length > 0) {
2004 CodeMirror.commands.autocomplete(instance);
2009 * Remove autocomplete information before tearing down a page
2011 AJAX.registerTeardown('functions.js', function () {
2012 sqlAutoComplete = false;
2013 sqlAutoCompleteDefaultTable = '';
2017 * Binds the CodeMirror to the text area used to inline edit a query.
2019 Functions.bindCodeMirrorToInlineEditor = function () {
2020 var $inlineEditor = $('#sql_query_edit');
2021 if ($inlineEditor.length > 0) {
2022 if (typeof CodeMirror !== 'undefined') {
2023 var height = $inlineEditor.css('height');
2024 codeMirrorInlineEditor = Functions.getSqlEditor($inlineEditor);
2025 codeMirrorInlineEditor.getWrapperElement().style.height = height;
2026 codeMirrorInlineEditor.refresh();
2027 codeMirrorInlineEditor.focus();
2028 $(codeMirrorInlineEditor.getWrapperElement())
2029 .on('keydown', Functions.catchKeypressesFromSqlInlineEdit);
2033 .on('keydown', Functions.catchKeypressesFromSqlInlineEdit);
2038 Functions.catchKeypressesFromSqlInlineEdit = function (event) {
2039 // ctrl-enter is 10 in chrome and ie, but 13 in ff
2040 if ((event.ctrlKey || event.metaKey) && (event.keyCode === 13 || event.keyCode === 10)) {
2041 $('#sql_query_edit_save').trigger('click');
2046 * Adds doc link to single highlighted SQL element
2048 Functions.documentationAdd = function ($elm, params) {
2049 if (typeof mysqlDocTemplate === 'undefined') {
2053 var url = Functions.sprintf(
2054 decodeURIComponent(mysqlDocTemplate),
2057 if (params.length > 1) {
2058 url += '#' + params[1];
2060 var content = $elm.text();
2062 $elm.append('<a target="mysql_doc" class="cm-sql-doc" href="' + url + '">' + content + '</a>');
2066 * Generates doc links for keywords inside highlighted SQL
2068 Functions.documentationKeyword = function (idx, elm) {
2070 /* Skip already processed ones */
2071 if ($elm.find('a').length > 0) {
2074 var keyword = $elm.text().toUpperCase();
2075 var $next = $elm.next('.cm-keyword');
2077 var nextKeyword = $next.text().toUpperCase();
2078 var full = keyword + ' ' + nextKeyword;
2080 var $next2 = $next.next('.cm-keyword');
2082 var next2Keyword = $next2.text().toUpperCase();
2083 var full2 = full + ' ' + next2Keyword;
2084 if (full2 in mysqlDocKeyword) {
2085 Functions.documentationAdd($elm, mysqlDocKeyword[full2]);
2086 Functions.documentationAdd($next, mysqlDocKeyword[full2]);
2087 Functions.documentationAdd($next2, mysqlDocKeyword[full2]);
2091 if (full in mysqlDocKeyword) {
2092 Functions.documentationAdd($elm, mysqlDocKeyword[full]);
2093 Functions.documentationAdd($next, mysqlDocKeyword[full]);
2097 if (keyword in mysqlDocKeyword) {
2098 Functions.documentationAdd($elm, mysqlDocKeyword[keyword]);
2103 * Generates doc links for builtins inside highlighted SQL
2105 Functions.documentationBuiltin = function (idx, elm) {
2107 var builtin = $elm.text().toUpperCase();
2108 if (builtin in mysqlDocBuiltin) {
2109 Functions.documentationAdd($elm, mysqlDocBuiltin[builtin]);
2114 * Higlights SQL using CodeMirror.
2116 Functions.highlightSql = function ($base) {
2117 var $elm = $base.find('code.sql');
2118 $elm.each(function () {
2120 var $pre = $sql.find('pre');
2121 /* We only care about visible elements to avoid double processing */
2122 if ($pre.is(':visible')) {
2123 var $highlight = $('<div class="sql-highlight cm-s-default"></div>');
2124 $sql.append($highlight);
2125 if (typeof CodeMirror !== 'undefined') {
2126 CodeMirror.runMode($sql.text(), 'text/x-mysql', $highlight[0]);
2128 $highlight.find('.cm-keyword').each(Functions.documentationKeyword);
2129 $highlight.find('.cm-builtin').each(Functions.documentationBuiltin);
2136 * Updates an element containing code.
2138 * @param jQuery Object $base base element which contains the raw and the
2141 * @param string htmlValue code in HTML format, displayed if code cannot be
2144 * @param string rawValue raw code, used as a parameter for highlighter
2146 * @return bool whether content was updated or not
2148 Functions.updateCode = function ($base, htmlValue, rawValue) {
2149 var $code = $base.find('code');
2150 if ($code.length === 0) {
2154 // Determines the type of the content and appropriate CodeMirror mode.
2157 if ($code.hasClass('json')) {
2159 mode = 'application/json';
2160 } else if ($code.hasClass('sql')) {
2162 mode = 'text/x-mysql';
2163 } else if ($code.hasClass('xml')) {
2165 mode = 'application/xml';
2170 // Element used to display unhighlighted code.
2171 var $notHighlighted = $('<pre>' + htmlValue + '</pre>');
2173 // Tries to highlight code using CodeMirror.
2174 if (typeof CodeMirror !== 'undefined') {
2175 var $highlighted = $('<div class="' + type + '-highlight cm-s-default"></div>');
2176 CodeMirror.runMode(rawValue, mode, $highlighted[0]);
2177 $notHighlighted.hide();
2178 $code.html('').append($notHighlighted, $highlighted[0]);
2180 $code.html('').append($notHighlighted);
2187 * Show a message on the top of the page for an Ajax request
2191 * 1) var $msg = Functions.ajaxShowMessage();
2192 * This will show a message that reads "Loading...". Such a message will not
2193 * disappear automatically and cannot be dismissed by the user. To remove this
2194 * message either the Functions.ajaxRemoveMessage($msg) function must be called or
2195 * another message must be show with Functions.ajaxShowMessage() function.
2197 * 2) var $msg = Functions.ajaxShowMessage(Messages.strProcessingRequest);
2198 * This is a special case. The behaviour is same as above,
2199 * just with a different message
2201 * 3) var $msg = Functions.ajaxShowMessage('The operation was successful');
2202 * This will show a message that will disappear automatically and it can also
2203 * be dismissed by the user.
2205 * 4) var $msg = Functions.ajaxShowMessage('Some error', false);
2206 * This will show a message that will not disappear automatically, but it
2207 * can be dismissed by the user after they have finished reading it.
2209 * @param string message string containing the message to be shown.
2210 * optional, defaults to 'Loading...'
2211 * @param mixed timeout number of milliseconds for the message to be visible
2212 * optional, defaults to 5000. If set to 'false', the
2213 * notification will never disappear
2214 * @param string type string to dictate the type of message shown.
2215 * optional, defaults to normal notification.
2216 * If set to 'error', the notification will show message
2217 * with red background.
2218 * If set to 'success', the notification will show with
2219 * a green background.
2220 * @return jQuery object jQuery Element that holds the message div
2221 * this object can be passed to Functions.ajaxRemoveMessage()
2222 * to remove the notification
2224 Functions.ajaxShowMessage = function (message, timeout, type) {
2226 var newTimeOut = timeout;
2228 * @var self_closing Whether the notification will automatically disappear
2230 var selfClosing = true;
2232 * @var dismissable Whether the user will be able to remove
2233 * the notification by clicking on it
2235 var dismissable = true;
2236 // Handle the case when a empty data.message is passed.
2237 // We don't want the empty message
2241 // If the message is undefined, show the default
2242 msg = Messages.strLoading;
2243 dismissable = false;
2244 selfClosing = false;
2245 } else if (msg === Messages.strProcessingRequest) {
2246 // This is another case where the message should not disappear
2247 dismissable = false;
2248 selfClosing = false;
2250 // Figure out whether (or after how long) to remove the notification
2251 if (newTimeOut === undefined) {
2253 } else if (newTimeOut === false) {
2254 selfClosing = false;
2256 // Determine type of message, add styling as required
2257 if (type === 'error') {
2258 msg = '<div class="alert alert-danger" role="alert">' + msg + '</div>';
2259 } else if (type === 'success') {
2260 msg = '<div class="alert alert-success" role="alert">' + msg + '</div>';
2262 // Create a parent element for the AJAX messages, if necessary
2263 if ($('#loading_parent').length === 0) {
2264 $('<div id="loading_parent"></div>')
2265 .prependTo('#page_content');
2267 // Update message count to create distinct message elements every time
2269 // Remove all old messages, if any
2270 $('span.ajax_notification[id^=ajax_message_num]').remove();
2272 * @var $retval a jQuery object containing the reference
2273 * to the created AJAX message
2276 '<span class="ajax_notification" id="ajax_message_num_' +
2281 .appendTo('#loading_parent')
2284 // If the notification is self-closing we should create a callback to remove it
2288 .fadeOut('medium', function () {
2289 if ($(this).is(':data(tooltip)')) {
2290 $(this).tooltip('destroy');
2292 // Remove the notification
2296 // If the notification is dismissable we need to add the relevant class to it
2297 // and add a tooltip so that the users know that it can be removed
2299 $retval.addClass('dismissable').css('cursor', 'pointer');
2301 * Add a tooltip to the notification to let the user know that they
2302 * can dismiss the ajax notification by clicking on it.
2310 // Hide spinner if this is not a loading message
2311 if (msg !== Messages.strLoading) {
2312 $retval.css('background-image', 'none');
2314 Functions.highlightSql($retval);
2320 * Removes the message shown for an Ajax operation when it's completed
2322 * @param jQuery object jQuery Element that holds the notification
2326 Functions.ajaxRemoveMessage = function ($thisMessageBox) {
2327 if ($thisMessageBox !== undefined && $thisMessageBox instanceof jQuery) {
2331 if ($thisMessageBox.is(':data(tooltip)')) {
2332 $thisMessageBox.tooltip('destroy');
2334 $thisMessageBox.remove();
2340 * Requests SQL for previewing before executing.
2342 * @param jQuery Object $form Form containing query data
2346 Functions.previewSql = function ($form) {
2347 var formUrl = $form.attr('action');
2348 var sep = CommonParams.get('arg_separator');
2349 var formData = $form.serialize() +
2350 sep + 'do_save_data=1' +
2351 sep + 'preview_sql=1' +
2352 sep + 'ajax_request=1';
2353 var $messageBox = Functions.ajaxShowMessage();
2358 success: function (response) {
2359 Functions.ajaxRemoveMessage($messageBox);
2360 if (response.success) {
2361 var $dialogContent = $('<div></div>')
2362 .append(response.sql_data);
2363 var buttonOptions = {};
2364 buttonOptions[Messages.strClose] = function () {
2365 $(this).dialog('close');
2367 $dialogContent.dialog({
2371 buttons: buttonOptions,
2372 title: Messages.strPreviewSQL,
2373 close: function () {
2377 // Pretty SQL printing.
2378 Functions.highlightSql($(this));
2382 Functions.ajaxShowMessage(response.message);
2385 error: function () {
2386 Functions.ajaxShowMessage(Messages.strErrorProcessingRequest);
2392 * Callback called when submit/"OK" is clicked on sql preview/confirm modal
2394 * @callback onSubmitCallback
2395 * @param {string} url The url
2400 * @param {string} sqlData Sql query to preview
2401 * @param {string} url Url to be sent to callback
2402 * @param {onSubmitCallback} callback On submit callback function
2406 Functions.confirmPreviewSql = function (sqlData, url, callback) {
2407 var $dialogContent = $('<div class="preview_sql"><code class="sql"><pre>'
2409 + '</pre></code></div>'
2411 var buttonOptions = [
2413 text: Messages.strOK,
2415 click: function () {
2420 text: Messages.strCancel,
2421 class: 'submitCancel',
2422 click: function () {
2423 $(this).dialog('close');
2427 $dialogContent.dialog({
2431 buttons: buttonOptions,
2432 title: Messages.strPreviewSQL,
2433 close: function () {
2437 // Pretty SQL printing.
2438 Functions.highlightSql($(this));
2444 * check for reserved keyword column name
2446 * @param jQuery Object $form Form
2448 * @returns true|false
2450 Functions.checkReservedWordColumns = function ($form) {
2451 var isConfirmed = true;
2454 url: 'index.php?route=/table/structure/reserved-word-check',
2455 data: $form.serialize(),
2456 success: function (data) {
2457 if (typeof data.success !== 'undefined' && data.success === true) {
2458 isConfirmed = confirm(data.message);
2466 // This event only need to be fired once after the initial page load
2469 * Allows the user to dismiss a notification
2470 * created with Functions.ajaxShowMessage()
2472 var holdStarter = null;
2473 $(document).on('mousedown', 'span.ajax_notification.dismissable', function () {
2474 holdStarter = setTimeout(function () {
2479 $(document).on('mouseup', 'span.ajax_notification.dismissable', function () {
2480 if (holdStarter && event.which === 1) {
2481 clearTimeout(holdStarter);
2482 Functions.ajaxRemoveMessage($(this));
2486 * The below two functions hide the "Dismiss notification" tooltip when a user
2487 * is hovering a link or button that is inside an ajax message
2489 $(document).on('mouseover', 'span.ajax_notification a, span.ajax_notification button, span.ajax_notification input', function () {
2490 if ($(this).parents('span.ajax_notification').is(':data(tooltip)')) {
2491 $(this).parents('span.ajax_notification').tooltip('disable');
2494 $(document).on('mouseout', 'span.ajax_notification a, span.ajax_notification button, span.ajax_notification input', function () {
2495 if ($(this).parents('span.ajax_notification').is(':data(tooltip)')) {
2496 $(this).parents('span.ajax_notification').tooltip('enable');
2501 * Copy text to clipboard
2503 * @param text to copy to clipboard
2505 * @returns bool true|false
2507 function copyToClipboard (text) {
2508 var $temp = $('<input>');
2509 $temp.css({ 'position': 'fixed', 'width': '2em', 'border': 0, 'top': 0, 'left': 0, 'padding': 0, 'background': 'transparent' });
2510 $('body').append($temp);
2511 $temp.val(text).trigger('select');
2513 var res = document.execCommand('copy');
2522 $(document).on('click', 'a.copyQueryBtn', function (event) {
2523 event.preventDefault();
2524 var res = copyToClipboard($(this).attr('data-text'));
2526 $(this).after('<span id=\'copyStatus\'> (' + Messages.strCopyQueryButtonSuccess + ')</span>');
2528 $(this).after('<span id=\'copyStatus\'> (' + Messages.strCopyQueryButtonFailure + ')</span>');
2530 setTimeout(function () {
2531 $('#copyStatus').remove();
2537 * Hides/shows the "Open in ENUM/SET editor" message, depending on the data type of the column currently selected
2539 Functions.showNoticeForEnum = function (selectElement) {
2540 var enumNoticeId = selectElement.attr('id').split('_')[1];
2541 enumNoticeId += '_' + (parseInt(selectElement.attr('id').split('_')[2], 10) + 1);
2542 var selectedType = selectElement.val();
2543 if (selectedType === 'ENUM' || selectedType === 'SET') {
2544 $('p#enum_notice_' + enumNoticeId).show();
2546 $('p#enum_notice_' + enumNoticeId).hide();
2551 * Creates a Profiling Chart. Used in sql.js
2552 * and in server/status/monitor.js
2554 Functions.createProfilingChart = function (target, data) {
2556 var factory = new JQPlotChartFactory();
2557 var chart = factory.createChart(ChartType.PIE, target);
2559 // create the data table and add columns
2560 var dataTable = new DataTable();
2561 dataTable.addColumn(ColumnType.STRING, '');
2562 dataTable.addColumn(ColumnType.NUMBER, '');
2563 dataTable.setData(data);
2565 var windowWidth = $(window).width();
2567 if (windowWidth > 768) {
2571 // draw the chart and return the chart object
2572 chart.draw(dataTable, {
2575 showDataLabels: true
2579 tooltipLocation: 'se',
2581 tooltipAxes: 'pieref',
2582 formatString: '%s, %.9Ps'
2591 // from http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines#Color_Palette
2616 * Formats a profiling duration nicely (in us and ms time).
2617 * Used in server/status/monitor.js
2619 * @param integer Number to be formatted, should be in the range of microsecond to second
2620 * @param integer Accuracy, how many numbers right to the comma should be
2621 * @return string The formatted number
2623 Functions.prettyProfilingNum = function (number, accuracy) {
2629 acc = Math.pow(10, acc);
2630 if (num * 1000 < 0.1) {
2631 num = Math.round(acc * (num * 1000 * 1000)) / acc + 'µ';
2632 } else if (num < 0.1) {
2633 num = Math.round(acc * (num * 1000)) / acc + 'm';
2635 num = Math.round(acc * num) / acc;
2642 * Formats a SQL Query nicely with newlines and indentation. Depends on Codemirror and MySQL Mode!
2644 * @param string Query to be formatted
2645 * @return string The formatted query
2647 Functions.sqlPrettyPrint = function (string) {
2648 if (typeof CodeMirror === 'undefined') {
2652 var mode = CodeMirror.getMode({}, 'text/x-mysql');
2653 var stream = new CodeMirror.StringStream(string);
2654 var state = mode.startState();
2658 var tabs = function (cnt) {
2660 for (var i = 0; i < 4 * cnt; i++) {
2666 // "root-level" statements
2668 'select': ['select', 'from', 'on', 'where', 'having', 'limit', 'order by', 'group by'],
2669 'update': ['update', 'set', 'where'],
2670 'insert into': ['insert into', 'values']
2672 // don't put spaces before these tokens
2673 var spaceExceptionsBefore = { ';': true, ',': true, '.': true, '(': true };
2674 // don't put spaces after these tokens
2675 var spaceExceptionsAfter = { '.': true };
2677 // Populate tokens array
2678 while (! stream.eol()) {
2679 stream.start = stream.pos;
2680 token = mode.token(stream, state);
2681 if (token !== null) {
2682 tokens.push([token, stream.current().toLowerCase()]);
2686 var currentStatement = tokens[0][1];
2688 if (! statements[currentStatement]) {
2691 // Holds all currently opened code blocks (statement, function or generic)
2692 var blockStack = [];
2693 // If a new code block is found, newBlock contains its type for one iteration and vice versa for endBlock
2696 // How much to indent in the current line
2697 var indentLevel = 0;
2698 // Holds the "root-level" statements
2700 var lastStatementPart = statements[currentStatement][0];
2702 blockStack.unshift('statement');
2704 // Iterate through every token and format accordingly
2705 for (var i = 0; i < tokens.length; i++) {
2706 // New block => push to stack
2707 if (tokens[i][1] === '(') {
2708 if (i < tokens.length - 1 && tokens[i + 1][0] === 'statement-verb') {
2709 blockStack.unshift(newBlock = 'statement');
2710 } else if (i > 0 && tokens[i - 1][0] === 'builtin') {
2711 blockStack.unshift(newBlock = 'function');
2713 blockStack.unshift(newBlock = 'generic');
2719 // Block end => pop from stack
2720 if (tokens[i][1] === ')') {
2721 endBlock = blockStack[0];
2727 // A subquery is starting
2728 if (i > 0 && newBlock === 'statement') {
2730 output += '\n' + tabs(indentLevel) + tokens[i][1] + ' ' + tokens[i + 1][1].toUpperCase() + '\n' + tabs(indentLevel + 1);
2731 currentStatement = tokens[i + 1][1];
2736 // A subquery is ending
2737 if (endBlock === 'statement' && indentLevel > 0) {
2738 output += '\n' + tabs(indentLevel);
2742 // One less indentation for statement parts (from, where, order by, etc.) and a newline
2743 statementPart = statements[currentStatement].indexOf(tokens[i][1]);
2744 if (statementPart !== -1) {
2748 output += tabs(indentLevel) + tokens[i][1].toUpperCase();
2749 output += '\n' + tabs(indentLevel + 1);
2750 lastStatementPart = tokens[i][1];
2751 // Normal indentation and spaces for everything else
2753 if (! spaceExceptionsBefore[tokens[i][1]] &&
2754 ! (i > 0 && spaceExceptionsAfter[tokens[i - 1][1]]) &&
2755 output.charAt(output.length - 1) !== ' ') {
2758 if (tokens[i][0] === 'keyword') {
2759 output += tokens[i][1].toUpperCase();
2761 output += tokens[i][1];
2765 // split columns in select and 'update set' clauses, but only inside statements blocks
2766 if ((lastStatementPart === 'select' || lastStatementPart === 'where' || lastStatementPart === 'set') &&
2767 tokens[i][1] === ',' && blockStack[0] === 'statement') {
2768 output += '\n' + tabs(indentLevel + 1);
2771 // split conditions in where clauses, but only inside statements blocks
2772 if (lastStatementPart === 'where' &&
2773 (tokens[i][1] === 'and' || tokens[i][1] === 'or' || tokens[i][1] === 'xor')) {
2774 if (blockStack[0] === 'statement') {
2775 output += '\n' + tabs(indentLevel + 1);
2777 // Todo: Also split and or blocks in newlines & indentation++
2778 // if (blockStack[0] === 'generic')
2786 * jQuery function that uses jQueryUI's dialogs to confirm with user. Does not
2787 * return a jQuery object yet and hence cannot be chained
2789 * @param string question
2790 * @param string url URL to be passed to the callbackFn to make
2792 * @param function callbackFn callback to execute after user clicks on OK
2793 * @param function openCallback optional callback to run when dialog is shown
2795 Functions.confirm = function (question, url, callbackFn, openCallback) {
2796 var confirmState = CommonParams.get('confirm');
2797 if (! confirmState) {
2798 // user does not want to confirm
2799 if (typeof callbackFn === 'function') {
2800 callbackFn.call(this, url);
2804 if (Messages.strDoYouReally === '') {
2809 * @var button_options Object that stores the options passed to jQueryUI
2812 var buttonOptions = [
2814 text: Messages.strOK,
2815 'class': 'submitOK',
2816 click: function () {
2817 $(this).dialog('close');
2818 if (typeof callbackFn === 'function') {
2819 callbackFn.call(this, url);
2824 text: Messages.strCancel,
2825 'class': 'submitCancel',
2826 click: function () {
2827 $(this).dialog('close');
2832 $('<div></div>', { 'id': 'confirm_dialog', 'title': Messages.strConfirm })
2835 buttons: buttonOptions,
2836 close: function () {
2843 jQuery.fn.confirm = Functions.confirm;
2846 * jQuery function to sort a table's body after a new row has been appended to it.
2848 * @param string text_selector string to select the sortKey's text
2850 * @return jQuery Object for chaining purposes
2852 Functions.sortTable = function (textSelector) {
2853 return this.each(function () {
2855 * @var table_body Object referring to the table's <tbody> element
2857 var tableBody = $(this);
2859 * @var rows Object referring to the collection of rows in {@link tableBody}
2861 var rows = $(this).find('tr').get();
2863 // get the text of the field that we will sort by
2864 $.each(rows, function (index, row) {
2865 row.sortKey = $.trim($(row).find(textSelector).text().toLowerCase());
2868 // get the sorted order
2869 rows.sort(function (a, b) {
2870 if (a.sortKey < b.sortKey) {
2873 if (a.sortKey > b.sortKey) {
2879 // pull out each row from the table and then append it according to it's order
2880 $.each(rows, function (index, row) {
2881 $(tableBody).append(row);
2886 jQuery.fn.sortTable = Functions.sortTable;
2889 * Unbind all event handlers before tearing down a page
2891 AJAX.registerTeardown('functions.js', function () {
2892 $(document).off('submit', '#create_table_form_minimal.ajax');
2893 $(document).off('submit', 'form.create_table_form.ajax');
2894 $(document).off('click', 'form.create_table_form.ajax input[name=submit_num_fields]');
2895 $(document).off('keyup', 'form.create_table_form.ajax input');
2896 $(document).off('change', 'input[name=partition_count],input[name=subpartition_count],select[name=partition_by]');
2900 * jQuery coding for 'Create Table'. Used on /database/operations,
2901 * /database/structure and /database/tracking (i.e., wherever
2902 * PhpMyAdmin\Display\CreateTable is used)
2904 * Attach Ajax Event handlers for Create Table
2906 AJAX.registerOnload('functions.js', function () {
2908 * Attach event handler for submission of create table form (save)
2910 $(document).on('submit', 'form.create_table_form.ajax', function (event) {
2911 event.preventDefault();
2914 * @var the_form object referring to the create table form
2916 var $form = $(this);
2919 * First validate the form; if there is a problem, avoid submitting it
2921 * Functions.checkTableEditForm() needs a pure element and not a jQuery object,
2922 * this is why we pass $form[0] as a parameter (the jQuery object
2923 * is actually an array of DOM elements)
2926 if (Functions.checkTableEditForm($form[0], $form.find('input[name=orig_num_fields]').val())) {
2927 Functions.prepareForAjaxRequest($form);
2928 if (Functions.checkReservedWordColumns($form)) {
2929 Functions.ajaxShowMessage(Messages.strProcessingRequest);
2930 // User wants to submit the form
2931 $.post($form.attr('action'), $form.serialize() + CommonParams.get('arg_separator') + 'do_save_data=1', function (data) {
2932 if (typeof data !== 'undefined' && data.success === true) {
2933 $('#properties_message')
2934 .removeClass('alert-danger')
2936 Functions.ajaxShowMessage(data.message);
2937 // Only if the create table dialog (distinct panel) exists
2938 var $createTableDialog = $('#create_table_dialog');
2939 if ($createTableDialog.length > 0) {
2940 $createTableDialog.dialog('close').remove();
2942 $('#tableslistcontainer').before(data.formatted_sql);
2945 * @var tables_table Object referring to the <tbody> element that holds the list of tables
2947 var tablesTable = $('#tablesForm').find('tbody').not('#tbl_summary_row');
2948 // this is the first table created in this db
2949 if (tablesTable.length === 0) {
2950 CommonActions.refreshMain(
2951 CommonParams.get('opendb_url')
2955 * @var curr_last_row Object referring to the last <tr> element in {@link tablesTable}
2957 var currLastRow = $(tablesTable).find('tr').last();
2959 * @var curr_last_row_index_string String containing the index of {@link currLastRow}
2961 var currLastRowIndexString = $(currLastRow).find('input:checkbox').attr('id').match(/\d+/)[0];
2963 * @var curr_last_row_index Index of {@link currLastRow}
2965 var currLastRowIndex = parseFloat(currLastRowIndexString);
2967 * @var new_last_row_index Index of the new row to be appended to {@link tablesTable}
2969 var newLastRowIndex = currLastRowIndex + 1;
2971 * @var new_last_row_id String containing the id of the row to be appended to {@link tablesTable}
2973 var newLastRowId = 'checkbox_tbl_' + newLastRowIndex;
2975 data.newTableString = data.newTableString.replace(/checkbox_tbl_/, newLastRowId);
2977 $(data.newTableString)
2978 .appendTo(tablesTable);
2981 $(tablesTable).sortTable('th');
2983 // Adjust summary row
2984 DatabaseStructure.adjustTotals();
2987 // Refresh navigation as a new table has been added
2988 Navigation.reload();
2989 // Redirect to table structure page on creation of new table
2990 var argsep = CommonParams.get('arg_separator');
2991 var params12 = 'ajax_request=true' + argsep + 'ajax_page_request=true';
2992 if (! (history && history.pushState)) {
2993 params12 += MicroHistory.menus.getRequestParam();
2995 var tableStructureUrl = 'index.php?route=/table/structure' + argsep + 'server=' + data.params.server +
2996 argsep + 'db=' + data.params.db + argsep + 'token=' + data.params.token +
2997 argsep + 'goto=' + encodeURIComponent('index.php?route=/database/structure') + argsep + 'table=' + data.params.table + '';
2998 $.get(tableStructureUrl, params12, AJAX.responseHandler);
3000 Functions.ajaxShowMessage(
3001 '<div class="alert alert-danger" role="alert">' + data.error + '</div>',
3008 }); // end create table form (save)
3011 * Submits the intermediate changes in the table creation form
3012 * to refresh the UI accordingly
3014 function submitChangesInCreateTableForm (actionParam) {
3016 * @var the_form object referring to the create table form
3018 var $form = $('form.create_table_form.ajax');
3020 var $msgbox = Functions.ajaxShowMessage(Messages.strProcessingRequest);
3021 Functions.prepareForAjaxRequest($form);
3023 // User wants to add more fields to the table
3024 $.post($form.attr('action'), $form.serialize() + '&' + actionParam, function (data) {
3025 if (typeof data !== 'undefined' && data.success) {
3026 var $pageContent = $('#page_content');
3027 $pageContent.html(data.message);
3028 Functions.highlightSql($pageContent);
3029 Functions.verifyColumnsProperties();
3030 Functions.hideShowConnection($('.create_table_form select[name=tbl_storage_engine]'));
3031 Functions.ajaxRemoveMessage($msgbox);
3033 Functions.ajaxShowMessage(data.error);
3039 * Attach event handler for create table form (add fields)
3041 $(document).on('click', 'form.create_table_form.ajax input[name=submit_num_fields]', function (event) {
3042 event.preventDefault();
3043 submitChangesInCreateTableForm('submit_num_fields=1');
3044 }); // end create table form (add fields)
3046 $(document).on('keydown', 'form.create_table_form.ajax input[name=added_fields]', function (event) {
3047 if (event.keyCode === 13) {
3048 event.preventDefault();
3049 event.stopImmediatePropagation();
3052 .find('input[name=submit_num_fields]')
3058 * Attach event handler to manage changes in number of partitions and subpartitions
3060 $(document).on('change', 'input[name=partition_count],input[name=subpartition_count],select[name=partition_by]', function () {
3061 var $this = $(this);
3062 var $form = $this.parents('form');
3063 if ($form.is('.create_table_form.ajax')) {
3064 submitChangesInCreateTableForm('submit_partition_change=1');
3066 $form.trigger('submit');
3070 $(document).on('change', 'input[value=AUTO_INCREMENT]', function () {
3072 var col = /\d/.exec($(this).attr('name'));
3074 var $selectFieldKey = $('select[name="field_key[' + col + ']"]');
3075 if ($selectFieldKey.val() === 'none_' + col) {
3076 $selectFieldKey.val('primary_' + col).trigger('change', [false]);
3081 .off('click', 'input.preview_sql')
3082 .on('click', 'input.preview_sql', function () {
3083 var $form = $(this).closest('form');
3084 Functions.previewSql($form);
3090 * Validates the password field in a form
3092 * @see Messages.strPasswordEmpty
3093 * @see Messages.strPasswordNotSame
3094 * @param {object} $theForm The form to be validated
3097 Functions.checkPassword = function ($theForm) {
3098 // Did the user select 'no password'?
3099 if ($theForm.find('#nopass_1').is(':checked')) {
3102 var $pred = $theForm.find('#select_pred_password');
3103 if ($pred.length && ($pred.val() === 'none' || $pred.val() === 'keep')) {
3108 var $password = $theForm.find('input[name=pma_pw]');
3109 var $passwordRepeat = $theForm.find('input[name=pma_pw2]');
3110 var alertMessage = false;
3112 if ($password.val() === '') {
3113 alertMessage = Messages.strPasswordEmpty;
3114 } else if ($password.val() !== $passwordRepeat.val()) {
3115 alertMessage = Messages.strPasswordNotSame;
3119 alert(alertMessage);
3121 $passwordRepeat.val('');
3122 $password.trigger('focus');
3129 * Attach Ajax event handlers for 'Change Password' on index.php
3131 AJAX.registerOnload('functions.js', function () {
3132 /* Handler for hostname type */
3133 $(document).on('change', '#select_pred_hostname', function () {
3134 var hostname = $('#pma_hostname');
3135 if (this.value === 'any') {
3137 } else if (this.value === 'localhost') {
3138 hostname.val('localhost');
3139 } else if (this.value === 'thishost' && $(this).data('thishost')) {
3140 hostname.val($(this).data('thishost'));
3141 } else if (this.value === 'hosttable') {
3142 hostname.val('').prop('required', false);
3143 } else if (this.value === 'userdefined') {
3144 hostname.trigger('focus').select().prop('required', true);
3148 /* Handler for editing hostname */
3149 $(document).on('change', '#pma_hostname', function () {
3150 $('#select_pred_hostname').val('userdefined');
3151 $('#pma_hostname').prop('required', true);
3154 /* Handler for username type */
3155 $(document).on('change', '#select_pred_username', function () {
3156 if (this.value === 'any') {
3157 $('#pma_username').val('').prop('required', false);
3158 $('#user_exists_warning').css('display', 'none');
3159 } else if (this.value === 'userdefined') {
3160 $('#pma_username').trigger('focus').trigger('select').prop('required', true);
3164 /* Handler for editing username */
3165 $(document).on('change', '#pma_username', function () {
3166 $('#select_pred_username').val('userdefined');
3167 $('#pma_username').prop('required', true);
3170 /* Handler for password type */
3171 $(document).on('change', '#select_pred_password', function () {
3172 if (this.value === 'none') {
3173 $('#text_pma_pw2').prop('required', false).val('');
3174 $('#text_pma_pw').prop('required', false).val('');
3175 } else if (this.value === 'userdefined') {
3176 $('#text_pma_pw2').prop('required', true);
3177 $('#text_pma_pw').prop('required', true).trigger('focus').trigger('select');
3179 $('#text_pma_pw2').prop('required', false);
3180 $('#text_pma_pw').prop('required', false);
3184 /* Handler for editing password */
3185 $(document).on('change', '#text_pma_pw,#text_pma_pw2', function () {
3186 $('#select_pred_password').val('userdefined');
3187 $('#text_pma_pw2').prop('required', true);
3188 $('#text_pma_pw').prop('required', true);
3192 * Unbind all event handlers before tearing down a page
3194 $(document).off('click', '#change_password_anchor.ajax');
3197 * Attach Ajax event handler on the change password anchor
3200 $(document).on('click', '#change_password_anchor.ajax', function (event) {
3201 event.preventDefault();
3203 var $msgbox = Functions.ajaxShowMessage();
3206 * @var button_options Object containing options to be passed to jQueryUI's dialog
3208 var buttonOptions = {};
3209 buttonOptions[Messages.strGo] = function () {
3210 event.preventDefault();
3213 * @var $the_form Object referring to the change password form
3215 var $theForm = $('#change_password_form');
3217 if (! Functions.checkPassword($theForm)) {
3222 * @var this_value String containing the value of the submit button.
3223 * Need to append this for the change password form on Server Privileges
3226 var thisValue = $(this).val();
3228 var $msgbox = Functions.ajaxShowMessage(Messages.strProcessingRequest);
3229 $theForm.append('<input type="hidden" name="ajax_request" value="true">');
3231 $.post($theForm.attr('action'), $theForm.serialize() + CommonParams.get('arg_separator') + 'change_pw=' + thisValue, function (data) {
3232 if (typeof data === 'undefined' || data.success !== true) {
3233 Functions.ajaxShowMessage(data.error, false);
3237 var $pageContent = $('#page_content');
3238 $pageContent.prepend(data.message);
3239 Functions.highlightSql($pageContent);
3240 $('#change_password_dialog').hide().remove();
3241 $('#edit_user_dialog').dialog('close').remove();
3242 Functions.ajaxRemoveMessage($msgbox);
3246 buttonOptions[Messages.strCancel] = function () {
3247 $(this).dialog('close');
3249 $.get($(this).attr('href'), { 'ajax_request': true }, function (data) {
3250 if (typeof data === 'undefined' || !data.success) {
3251 Functions.ajaxShowMessage(data.error, false);
3256 AJAX.scriptHandler.load(data.scripts);
3259 $('<div id="change_password_dialog"></div>')
3261 title: Messages.strChangePassword,
3263 close: function () {
3266 buttons: buttonOptions,
3269 .append(data.message);
3270 // for this dialog, we remove the fieldset wrapping due to double headings
3271 $('fieldset#fieldset_change_password')
3272 .find('legend').remove().end()
3273 .find('table.noclick').unwrap().addClass('some-margin')
3274 .find('input#text_pma_pw').trigger('focus');
3275 $('#fieldset_change_password_footer').hide();
3276 Functions.ajaxRemoveMessage($msgbox);
3277 Functions.displayPasswordGenerateButton();
3278 $('#change_password_form').on('submit', function (e) {
3281 .closest('.ui-dialog')
3282 .find('.ui-dialog-buttonpane .ui-button')
3287 }); // end handler for change password anchor
3288 }); // end $() for Change Password
3291 * Unbind all event handlers before tearing down a page
3293 AJAX.registerTeardown('functions.js', function () {
3294 $(document).off('change', 'select.column_type');
3295 $(document).off('change', 'select.default_type');
3296 $(document).off('change', 'select.virtuality');
3297 $(document).off('change', 'input.allow_null');
3298 $(document).off('change', '.create_table_form select[name=tbl_storage_engine]');
3301 * Toggle the hiding/showing of the "Open in ENUM/SET editor" message when
3302 * the page loads and when the selected data type changes
3304 AJAX.registerOnload('functions.js', function () {
3305 // is called here for normal page loads and also when opening
3306 // the Create table dialog
3307 Functions.verifyColumnsProperties();
3309 // needs on() to work also in the Create Table dialog
3310 $(document).on('change', 'select.column_type', function () {
3311 Functions.showNoticeForEnum($(this));
3313 $(document).on('change', 'select.default_type', function () {
3314 Functions.hideShowDefaultValue($(this));
3316 $(document).on('change', 'select.virtuality', function () {
3317 Functions.hideShowExpression($(this));
3319 $(document).on('change', 'input.allow_null', function () {
3320 Functions.validateDefaultValue($(this));
3322 $(document).on('change', '.create_table_form select[name=tbl_storage_engine]', function () {
3323 Functions.hideShowConnection($(this));
3328 * If the chosen storage engine is FEDERATED show connection field. Hide otherwise
3330 * @param $engineSelector storage engine selector
3332 Functions.hideShowConnection = function ($engineSelector) {
3333 var $connection = $('.create_table_form input[name=connection]');
3334 var index = $connection.parent('td').index() + 1;
3335 var $labelTh = $connection.parents('tr').prev('tr').children(document.querySelectorAll('th:nth-child(' + index + ')'));
3336 if ($engineSelector.val() !== 'FEDERATED') {
3338 .prop('disabled', true)
3339 .parent('td').hide();
3343 .prop('disabled', false)
3344 .parent('td').show();
3350 * If the column does not allow NULL values, makes sure that default is not NULL
3352 Functions.validateDefaultValue = function ($nullCheckbox) {
3353 if (! $nullCheckbox.prop('checked')) {
3354 var $default = $nullCheckbox.closest('tr').find('.default_type');
3355 if ($default.val() === 'NULL') {
3356 $default.val('NONE');
3362 * function to populate the input fields on picking a column from central list
3364 * @param string input_id input id of the name field for the column to be populated
3365 * @param integer offset of the selected column in central list of columns
3367 Functions.autoPopulate = function (inputId, offset) {
3368 var db = CommonParams.get('db');
3369 var table = CommonParams.get('table');
3370 var newInputId = inputId.substring(0, inputId.length - 1);
3371 $('#' + newInputId + '1').val(centralColumnList[db + '_' + table][offset].col_name);
3372 var colType = centralColumnList[db + '_' + table][offset].col_type.toUpperCase();
3373 $('#' + newInputId + '2').val(colType);
3374 var $input3 = $('#' + newInputId + '3');
3375 $input3.val(centralColumnList[db + '_' + table][offset].col_length);
3376 if (colType === 'ENUM' || colType === 'SET') {
3377 $input3.next().show();
3379 $input3.next().hide();
3381 var colDefault = centralColumnList[db + '_' + table][offset].col_default.toUpperCase();
3382 var $input4 = $('#' + newInputId + '4');
3383 if (colDefault !== '' && colDefault !== 'NULL' && colDefault !== 'CURRENT_TIMESTAMP' && colDefault !== 'CURRENT_TIMESTAMP()') {
3384 $input4.val('USER_DEFINED');
3385 $input4.next().next().show();
3386 $input4.next().next().val(centralColumnList[db + '_' + table][offset].col_default);
3388 $input4.val(centralColumnList[db + '_' + table][offset].col_default);
3389 $input4.next().next().hide();
3391 $('#' + newInputId + '5').val(centralColumnList[db + '_' + table][offset].col_collation);
3392 var $input6 = $('#' + newInputId + '6');
3393 $input6.val(centralColumnList[db + '_' + table][offset].col_attribute);
3394 if (centralColumnList[db + '_' + table][offset].col_extra === 'on update CURRENT_TIMESTAMP') {
3395 $input6.val(centralColumnList[db + '_' + table][offset].col_extra);
3397 if (centralColumnList[db + '_' + table][offset].col_extra.toUpperCase() === 'AUTO_INCREMENT') {
3398 $('#' + newInputId + '9').prop('checked',true).trigger('change');
3400 $('#' + newInputId + '9').prop('checked',false);
3402 if (centralColumnList[db + '_' + table][offset].col_isNull !== '0') {
3403 $('#' + newInputId + '7').prop('checked',true);
3405 $('#' + newInputId + '7').prop('checked',false);
3410 * Unbind all event handlers before tearing down a page
3412 AJAX.registerTeardown('functions.js', function () {
3413 $(document).off('click', 'a.open_enum_editor');
3414 $(document).off('click', 'input.add_value');
3415 $(document).off('click', '#enum_editor td.drop');
3416 $(document).off('click', 'a.central_columns_dialog');
3420 * @var $enumEditorDialog An object that points to the jQuery
3421 * dialog of the ENUM/SET editor
3423 var $enumEditorDialog = null;
3426 * Opens the ENUM/SET editor and controls its functions
3428 AJAX.registerOnload('functions.js', function () {
3429 $(document).on('click', 'a.open_enum_editor', function () {
3430 // Get the name of the column that is being edited
3431 var colname = $(this).closest('tr').find('input').first().val();
3434 // And use it to make up a title for the page
3435 if (colname.length < 1) {
3436 title = Messages.enum_newColumnVals;
3438 title = Messages.enum_columnVals.replace(
3440 '"' + Functions.escapeHtml(decodeURIComponent(colname)) + '"'
3443 // Get the values as a string
3444 var inputstring = $(this)
3448 // Escape html entities
3449 inputstring = $('<div></div>')
3452 // Parse the values, escaping quotes and
3453 // slashes on the fly, into an array
3455 var inString = false;
3459 for (i = 0; i < inputstring.length; i++) {
3460 curr = inputstring.charAt(i);
3461 next = i === inputstring.length ? '' : inputstring.charAt(i + 1);
3462 if (! inString && curr === '\'') {
3464 } else if (inString && curr === '\\' && next === '\\') {
3467 } else if (inString && next === '\'' && (curr === '\'' || curr === '\\')) {
3470 } else if (inString && curr === '\'') {
3472 values.push(buffer);
3474 } else if (inString) {
3478 if (buffer.length > 0) {
3479 // The leftovers in the buffer are the last value (if any)
3480 values.push(buffer);
3483 // If there are no values, maybe the user is about to make a
3484 // new list so we add a few for them to get started with.
3485 if (values.length === 0) {
3486 values.push('', '', '', '');
3488 // Add the parsed values to the editor
3489 var dropIcon = Functions.getImage('b_drop');
3490 for (i = 0; i < values.length; i++) {
3491 fields += '<tr><td>' +
3492 '<input type=\'text\' value=\'' + values[i] + '\'>' +
3493 '</td><td class=\'drop\'>' +
3498 * @var dialog HTML code for the ENUM/SET dialog
3500 var dialog = '<div id=\'enum_editor\'>' +
3502 '<legend>' + title + '</legend>' +
3503 '<p>' + Functions.getImage('s_notice') +
3504 Messages.enum_hint + '</p>' +
3505 '<table class=\'values\'>' + fields + '</table>' +
3506 '</fieldset><fieldset class=\'tblFooters\'>' +
3507 '<table class=\'add\'><tr><td>' +
3508 '<div class=\'slider\'></div>' +
3510 '<form><div><input type=\'submit\' class=\'add_value btn btn-primary\' value=\'' +
3511 Functions.sprintf(Messages.enum_addValue, 1) +
3512 '\'></div></form>' +
3513 '</td></tr></table>' +
3514 '<input type=\'hidden\' value=\'' + // So we know which column's data is being edited
3515 $(this).closest('td').find('input').attr('id') +
3520 * @var {object} buttonOptions Defines functions to be called when the buttons in
3521 * the buttonOptions jQuery dialog bar are pressed
3523 var buttonOptions = {};
3524 buttonOptions[Messages.strGo] = function () {
3525 // When the submit button is clicked,
3526 // put the data back into the original form
3527 var valueArray = [];
3528 $(this).find('.values input').each(function (index, elm) {
3529 var val = elm.value.replace(/\\/g, '\\\\').replace(/'/g, '\'\'');
3530 valueArray.push('\'' + val + '\'');
3532 // get the Length/Values text field where this value belongs
3533 var valuesId = $(this).find('input[type=\'hidden\']').val();
3534 $('input#' + valuesId).val(valueArray.join(','));
3535 $(this).dialog('close');
3537 buttonOptions[Messages.strClose] = function () {
3538 $(this).dialog('close');
3541 var width = parseInt(
3542 (parseInt($('html').css('font-size'), 10) / 13) * 340,
3548 $enumEditorDialog = $(dialog).dialog({
3552 title: Messages.enum_editor,
3553 buttons: buttonOptions,
3555 // Focus the "Go" button after opening the dialog
3556 $(this).closest('.ui-dialog').find('.ui-dialog-buttonpane button').first().trigger('focus');
3558 close: function () {
3562 // slider for choosing how many fields to add
3563 $enumEditorDialog.find('.slider').slider({
3569 slide: function (event, ui) {
3570 $(this).closest('table').find('input[type=submit]').val(
3571 Functions.sprintf(Messages.enum_addValue, ui.value)
3575 // Focus the slider, otherwise it looks nearly transparent
3576 $('a.ui-slider-handle').addClass('ui-state-focus');
3580 $(document).on('click', 'a.central_columns_dialog', function () {
3581 var href = 'index.php?route=/database/central-columns';
3582 var db = CommonParams.get('db');
3583 var table = CommonParams.get('table');
3584 var maxRows = $(this).data('maxrows');
3585 var pick = $(this).data('pick');
3586 if (pick !== false) {
3590 'ajax_request' : true,
3591 'server' : CommonParams.get('server'),
3592 'db' : CommonParams.get('db'),
3593 'cur_table' : CommonParams.get('table'),
3594 'getColumnList':true
3596 var colid = $(this).closest('td').find('input').attr('id');
3598 if (! (db + '_' + table in centralColumnList)) {
3599 centralColumnList.push(db + '_' + table);
3604 success: function (data) {
3605 centralColumnList[db + '_' + table] = data.message;
3611 var listSize = centralColumnList[db + '_' + table].length;
3612 var min = (listSize <= maxRows) ? listSize : maxRows;
3613 for (i = 0; i < min; i++) {
3614 fields += '<tr><td><div><span class="font_weight_bold">' +
3615 Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_name) +
3616 '</span><br><span class="color_gray">' + centralColumnList[db + '_' + table][i].col_type;
3618 if (centralColumnList[db + '_' + table][i].col_attribute !== '') {
3619 fields += '(' + Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_attribute) + ') ';
3621 if (centralColumnList[db + '_' + table][i].col_length !== '') {
3622 fields += '(' + Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_length) + ') ';
3624 fields += Functions.escapeHtml(centralColumnList[db + '_' + table][i].col_extra) + '</span>' +
3627 fields += '<td><input class="btn btn-secondary pick w-100" type="submit" value="' +
3628 Messages.pickColumn + '" onclick="Functions.autoPopulate(\'' + colid + '\',' + i + ')"></td>';
3632 var resultPointer = i;
3633 var searchIn = '<input type="text" class="filter_rows" placeholder="' + Messages.searchList + '">';
3634 if (fields === '') {
3635 fields = Functions.sprintf(Messages.strEmptyCentralList, '\'' + Functions.escapeHtml(db) + '\'');
3639 if (listSize > maxRows) {
3640 seeMore = '<fieldset class="tblFooters text-center font_weight_bold">' +
3641 '<a href=\'#\' id=\'seeMore\'>' + Messages.seeMore + '</a></fieldset>';
3643 var centralColumnsDialog = '<div class=\'max_height_400\'>' +
3646 '<table id=\'col_list\' class=\'values w-100\'>' + fields + '</table>' +
3651 var width = parseInt(
3652 (parseInt($('html').css('font-size'), 10) / 13) * 500,
3658 var buttonOptions = {};
3659 var $centralColumnsDialog = $(centralColumnsDialog).dialog({
3663 title: Messages.pickColumnTitle,
3664 buttons: buttonOptions,
3666 $('#col_list').on('click', '.pick', function () {
3667 $centralColumnsDialog.remove();
3669 $('.filter_rows').on('keyup', function () {
3670 $.uiTableFilter($('#col_list'), $(this).val());
3672 $('#seeMore').on('click', function () {
3674 min = (listSize <= maxRows + resultPointer) ? listSize : maxRows + resultPointer;
3675 for (i = resultPointer; i < min; i++) {
3676 fields += '<tr><td><div><span class="font_weight_bold">' +
3677 centralColumnList[db + '_' + table][i].col_name +
3678 '</span><br><span class="color_gray">' +
3679 centralColumnList[db + '_' + table][i].col_type;
3681 if (centralColumnList[db + '_' + table][i].col_attribute !== '') {
3682 fields += '(' + centralColumnList[db + '_' + table][i].col_attribute + ') ';
3684 if (centralColumnList[db + '_' + table][i].col_length !== '') {
3685 fields += '(' + centralColumnList[db + '_' + table][i].col_length + ') ';
3687 fields += centralColumnList[db + '_' + table][i].col_extra + '</span>' +
3690 fields += '<td><input class="btn btn-secondary pick w-100" type="submit" value="' +
3691 Messages.pickColumn + '" onclick="Functions.autoPopulate(\'' + colid + '\',' + i + ')"></td>';
3695 $('#col_list').append(fields);
3697 if (resultPointer === listSize) {
3698 $('#seeMore').hide();
3702 $(this).closest('.ui-dialog').find('.ui-dialog-buttonpane button').first().trigger('focus');
3704 close: function () {
3705 $('#col_list').off('click', '.pick');
3706 $('.filter_rows').off('keyup');
3713 // $(document).on('click', 'a.show_central_list',function(e) {
3716 // When "add a new value" is clicked, append an empty text field
3717 $(document).on('click', 'input.add_value', function (e) {
3719 var numNewRows = $enumEditorDialog.find('div.slider').slider('value');
3720 while (numNewRows--) {
3721 $enumEditorDialog.find('.values')
3723 '<tr class=\'hide\'><td>' +
3724 '<input type=\'text\'>' +
3725 '</td><td class=\'drop\'>' +
3726 Functions.getImage('b_drop') +
3734 // Removes the specified row from the enum editor
3735 $(document).on('click', '#enum_editor td.drop', function () {
3736 $(this).closest('tr').hide('fast', function () {
3743 * Ensures indexes names are valid according to their type and, for a primary
3744 * key, lock index name to 'PRIMARY'
3745 * @param string form_id Variable which parses the form name as
3747 * @return boolean false if there is no index form, true else
3749 Functions.checkIndexName = function (formId) {
3750 if ($('#' + formId).length === 0) {
3754 // Gets the elements pointers
3755 var $theIdxName = $('#input_index_name');
3756 var $theIdxChoice = $('#select_index_choice');
3758 // Index is a primary key
3759 if ($theIdxChoice.find('option:selected').val() === 'PRIMARY') {
3760 $theIdxName.val('PRIMARY');
3761 $theIdxName.prop('disabled', true);
3763 if ($theIdxName.val() === 'PRIMARY') {
3764 $theIdxName.val('');
3766 $theIdxName.prop('disabled', false);
3772 AJAX.registerTeardown('functions.js', function () {
3773 $(document).off('click', '#index_frm input[type=submit]');
3775 AJAX.registerOnload('functions.js', function () {
3777 * Handler for adding more columns to an index in the editor
3779 $(document).on('click', '#index_frm input[type=submit]', function (event) {
3780 event.preventDefault();
3781 var hadAddButtonHidden = $(this).closest('fieldset').find('.add_fields').hasClass('hide');
3782 if (hadAddButtonHidden === false) {
3783 var rowsToAdd = $(this)
3784 .closest('fieldset')
3788 var tempEmptyVal = function () {
3792 var tempSetFocus = function () {
3793 if ($(this).find('option:selected').val() === '') {
3796 $(this).closest('tr').find('input').trigger('focus');
3799 while (rowsToAdd--) {
3800 var $indexColumns = $('#index_columns');
3801 var $newrow = $indexColumns
3802 .find('tbody > tr').first()
3805 $indexColumns.find('tbody')
3807 $newrow.find(':input').each(tempEmptyVal);
3808 // focus index size input on column picked
3809 $newrow.find('select').on('change', tempSetFocus);
3815 Functions.indexEditorDialog = function (url, title, callbackSuccess, callbackFailure) {
3816 /* Remove the hidden dialogs if there are*/
3817 var $editIndexDialog = $('#edit_index_dialog');
3818 if ($editIndexDialog.length !== 0) {
3819 $editIndexDialog.remove();
3821 var $div = $('<div id="edit_index_dialog"></div>');
3824 * @var button_options Object that stores the options
3825 * passed to jQueryUI dialog
3827 var buttonOptions = {};
3828 buttonOptions[Messages.strGo] = function () {
3830 * @var the_form object referring to the export form
3832 var $form = $('#index_frm');
3833 Functions.ajaxShowMessage(Messages.strProcessingRequest);
3834 Functions.prepareForAjaxRequest($form);
3835 // User wants to submit the form
3836 $.post($form.attr('action'), $form.serialize() + CommonParams.get('arg_separator') + 'do_save_data=1', function (data) {
3837 var $sqlqueryresults = $('.sqlqueryresults');
3838 if ($sqlqueryresults.length !== 0) {
3839 $sqlqueryresults.remove();
3841 if (typeof data !== 'undefined' && data.success === true) {
3842 Functions.ajaxShowMessage(data.message);
3843 Functions.highlightSql($('.result_query'));
3844 $('.result_query .alert').remove();
3845 /* Reload the field form*/
3846 $('#table_index').remove();
3847 $('<div id=\'temp_div\'><div>')
3848 .append(data.index_table)
3849 .find('#table_index')
3850 .insertAfter('#index_header');
3851 var $editIndexDialog = $('#edit_index_dialog');
3852 if ($editIndexDialog.length > 0) {
3853 $editIndexDialog.dialog('close');
3855 $('div.no_indexes_defined').hide();
3856 if (callbackSuccess) {
3859 Navigation.reload();
3861 var $tempDiv = $('<div id=\'temp_div\'><div>').append(data.error);
3863 if ($tempDiv.find('.error code').length !== 0) {
3864 $error = $tempDiv.find('.error code').addClass('error');
3868 if (callbackFailure) {
3871 Functions.ajaxShowMessage($error, false);
3875 buttonOptions[Messages.strPreviewSQL] = function () {
3876 // Function for Previewing SQL
3877 var $form = $('#index_frm');
3878 Functions.previewSql($form);
3880 buttonOptions[Messages.strCancel] = function () {
3881 $(this).dialog('close');
3883 var $msgbox = Functions.ajaxShowMessage();
3884 $.post('index.php?route=/table/indexes', url, function (data) {
3885 if (typeof data !== 'undefined' && data.success === false) {
3886 // in the case of an error, show the error message returned.
3887 Functions.ajaxShowMessage(data.error, false);
3889 Functions.ajaxRemoveMessage($msgbox);
3890 // Show dialog if the request was successful
3892 .append(data.message)
3896 open: Functions.verifyColumnsProperties,
3898 buttons: buttonOptions,
3899 close: function () {
3903 $div.find('.tblFooters').remove();
3904 Functions.showIndexEditDialog($div);
3909 Functions.showIndexEditDialog = function ($outer) {
3910 Indexes.checkIndexType();
3911 Functions.checkIndexName('index_frm');
3912 var $indexColumns = $('#index_columns');
3913 $indexColumns.find('td').each(function () {
3914 $(this).css('width', $(this).width() + 'px');
3916 $indexColumns.find('tbody').sortable({
3918 containment: $indexColumns.find('tbody'),
3919 tolerance: 'pointer'
3921 Functions.showHints($outer);
3922 Functions.initSlider();
3923 // Add a slider for selecting how many columns to add to the index
3924 $outer.find('.slider').slider({
3929 slide: function (event, ui) {
3930 $(this).closest('fieldset').find('input[type=submit]').val(
3931 Functions.sprintf(Messages.strAddToIndex, ui.value)
3935 $('div.add_fields').removeClass('hide');
3936 // focus index size input on column picked
3937 $outer.find('table#index_columns select').on('change', function () {
3938 if ($(this).find('option:selected').val() === '') {
3941 $(this).closest('tr').find('input').trigger('focus');
3943 // Focus the slider, otherwise it looks nearly transparent
3944 $('a.ui-slider-handle').addClass('ui-state-focus');
3945 // set focus on index name input, if empty
3946 var input = $outer.find('input#input_index_name');
3947 if (! input.val()) {
3948 input.trigger('focus');
3953 * Function to display tooltips that were
3954 * generated on the PHP side by PhpMyAdmin\Util::showHint()
3956 * @param object $div a div jquery object which specifies the
3957 * domain for searching for tooltips. If we
3958 * omit this parameter the function searches
3961 Functions.showHints = function ($div) {
3963 if ($newDiv === undefined || !($newDiv instanceof jQuery) || $newDiv.length === 0) {
3964 $newDiv = $('body');
3966 $newDiv.find('.pma_hint').each(function () {
3968 $(this).children('img'),
3970 $(this).children('span').html()
3975 AJAX.registerOnload('functions.js', function () {
3976 Functions.showHints();
3979 Functions.mainMenuResizerCallback = function () {
3980 // 5 px margin for jumping menu in Chrome
3981 return $(document.body).width() - 5;
3984 // This must be fired only once after the initial page load
3986 // Initialise the menu resize plugin
3987 $('#topmenu').menuResizer(Functions.mainMenuResizerCallback);
3988 // register resize event
3989 $(window).on('resize', function () {
3990 $('#topmenu').menuResizer('resize');
3995 * Changes status of slider
3997 Functions.setStatusLabel = function ($element) {
3999 if ($element.css('display') === 'none') {
4004 $element.closest('.slide-wrapper').prev().find('span').text(text);
4008 * var toggleButton This is a function that creates a toggle
4009 * sliding button given a jQuery reference
4010 * to the correct DOM element
4012 Functions.toggleButton = function ($obj) {
4013 // In rtl mode the toggle switch is flipped horizontally
4014 // so we need to take that into account
4016 if ($('span.text_direction', $obj).text() === 'ltr') {
4022 * var h Height of the button, used to scale the
4023 * background image and position the layers
4025 var h = $obj.height();
4026 $('img', $obj).height(h);
4027 $('table', $obj).css('bottom', h - 1);
4029 * var on Width of the "ON" part of the toggle switch
4030 * var off Width of the "OFF" part of the toggle switch
4032 var on = $('td.toggleOn', $obj).width();
4033 var off = $('td.toggleOff', $obj).width();
4034 // Make the "ON" and "OFF" parts of the switch the same size
4035 // + 2 pixels to avoid overflowed
4036 $('td.toggleOn > div', $obj).width(Math.max(on, off) + 2);
4037 $('td.toggleOff > div', $obj).width(Math.max(on, off) + 2);
4039 * var w Width of the central part of the switch
4041 var w = parseInt(($('img', $obj).height() / 16) * 22, 10);
4042 // Resize the central part of the switch on the top
4043 // layer to match the background
4044 $(document.querySelectorAll('table td:nth-child(2) > div'), $obj).width(w);
4046 * var imgw Width of the background image
4047 * var tblw Width of the foreground layer
4048 * var offset By how many pixels to move the background
4049 * image, so that it matches the top layer
4051 var imgw = $('img', $obj).width();
4052 var tblw = $('table', $obj).width();
4053 var offset = parseInt(((imgw - tblw) / 2), 10);
4054 // Move the background to match the layout of the top layer
4055 $obj.find('img').css(right, offset);
4057 * var offw Outer width of the "ON" part of the toggle switch
4058 * var btnw Outer width of the central part of the switch
4060 var offw = $('td.toggleOff', $obj).outerWidth();
4061 var btnw = $(document.querySelectorAll('table td:nth-child(2)'), $obj).outerWidth();
4062 // Resize the main div so that exactly one side of
4063 // the switch plus the central part fit into it.
4064 $obj.width(offw + btnw + 2);
4066 * var move How many pixels to move the
4067 * switch by when toggling
4069 var move = $('td.toggleOff', $obj).outerWidth();
4070 // If the switch is initialized to the
4071 // OFF state we need to move it now.
4072 if ($('div.toggle-container', $obj).hasClass('off')) {
4073 if (right === 'right') {
4074 $('div.toggle-container', $obj).animate({ 'left': '-=' + move + 'px' }, 0);
4076 $('div.toggle-container', $obj).animate({ 'left': '+=' + move + 'px' }, 0);
4079 // Attach an 'onclick' event to the switch
4080 $('div.toggle-container', $obj).on('click', function () {
4081 if ($(this).hasClass('isActive')) {
4084 $(this).addClass('isActive');
4086 var $msg = Functions.ajaxShowMessage();
4087 var $container = $(this);
4088 var callback = $('span.callback', this).text();
4093 // Perform the actual toggle
4094 if ($(this).hasClass('on')) {
4095 if (right === 'right') {
4100 url = $(this).find('td.toggleOff > span').text();
4104 if (right === 'right') {
4109 url = $(this).find('td.toggleOn > span').text();
4110 removeClass = 'off';
4114 var parts = url.split('?');
4115 $.post(parts[0], parts[1] + '&ajax_request=true', function (data) {
4116 if (typeof data !== 'undefined' && data.success === true) {
4117 Functions.ajaxRemoveMessage($msg);
4119 .removeClass(removeClass)
4121 .animate({ 'left': operator + move + 'px' }, function () {
4122 $container.removeClass('isActive');
4124 // eslint-disable-next-line no-eval
4127 Functions.ajaxShowMessage(data.error, false);
4128 $container.removeClass('isActive');
4135 * Unbind all event handlers before tearing down a page
4137 AJAX.registerTeardown('functions.js', function () {
4138 $('div.toggle-container').off('click');
4141 * Initialise all toggle buttons
4143 AJAX.registerOnload('functions.js', function () {
4144 $('div.toggleAjax').each(function () {
4145 var $button = $(this).show();
4146 $button.find('img').each(function () {
4147 if (this.complete) {
4148 Functions.toggleButton($button);
4150 $(this).on('load', function () {
4151 Functions.toggleButton($button);
4159 * Unbind all event handlers before tearing down a page
4161 AJAX.registerTeardown('functions.js', function () {
4162 $(document).off('change', 'select.pageselector');
4163 $('#update_recent_tables').off('ready');
4164 $('#sync_favorite_tables').off('ready');
4167 AJAX.registerOnload('functions.js', function () {
4169 * Autosubmit page selector
4171 $(document).on('change', 'select.pageselector', function (event) {
4172 event.stopPropagation();
4173 // Check where to load the new content
4174 if ($(this).closest('#pma_navigation').length === 0) {
4175 // For the main page we don't need to do anything,
4176 $(this).closest('form').trigger('submit');
4178 // but for the navigation we need to manually replace the content
4179 Navigation.treePagination($(this));
4184 * Load version information asynchronously.
4186 if ($('li.jsversioncheck').length > 0) {
4189 url: 'index.php?route=/version-check',
4192 'server': CommonParams.get('server')
4194 success: Functions.currentVersion
4198 if ($('#is_git_revision').length > 0) {
4199 setTimeout(Functions.displayGitRevision, 10);
4205 Functions.initSlider();
4207 var $updateRecentTables = $('#update_recent_tables');
4208 if ($updateRecentTables.length) {
4210 $updateRecentTables.attr('href'),
4211 { 'no_debug': true },
4213 if (typeof data !== 'undefined' && data.success === true) {
4214 $('#pma_recent_list').html(data.list);
4220 // Sync favorite tables from localStorage to pmadb.
4221 if ($('#sync_favorite_tables').length) {
4223 url: $('#sync_favorite_tables').attr('href'),
4227 'favoriteTables': (isStorageSupported('localStorage') && typeof window.localStorage.favoriteTables !== 'undefined')
4228 ? window.localStorage.favoriteTables
4230 'server': CommonParams.get('server'),
4233 success: function (data) {
4234 // Update localStorage.
4235 if (isStorageSupported('localStorage')) {
4236 window.localStorage.favoriteTables = data.favoriteTables;
4238 $('#pma_favorite_list').html(data.list);
4245 * Initializes slider effect.
4247 Functions.initSlider = function () {
4248 $('div.pma_auto_slider').each(function () {
4249 var $this = $(this);
4250 if ($this.data('slider_init_done')) {
4253 var $wrapper = $('<div>', { 'class': 'slide-wrapper' });
4254 $wrapper.toggle($this.is(':visible'));
4255 $('<a>', { href: '#' + this.id, 'class': 'ajax' })
4256 .text($this.attr('title'))
4257 .prepend($('<span>'))
4258 .insertBefore($this)
4259 .on('click', function () {
4260 var $wrapper = $this.closest('.slide-wrapper');
4261 var visible = $this.is(':visible');
4265 $this[visible ? 'hide' : 'show']('blind', function () {
4266 $wrapper.toggle(!visible);
4267 $wrapper.parent().toggleClass('print_ignore', visible);
4268 Functions.setStatusLabel($this);
4272 $this.wrap($wrapper);
4273 $this.removeAttr('title');
4274 Functions.setStatusLabel($this);
4275 $this.data('slider_init_done', 1);
4280 * Initializes slider effect.
4282 AJAX.registerOnload('functions.js', function () {
4283 Functions.initSlider();
4287 * Restores sliders to the state they were in before initialisation.
4289 AJAX.registerTeardown('functions.js', function () {
4290 $('div.pma_auto_slider').each(function () {
4291 var $this = $(this);
4293 $this.parent().replaceWith($this);
4294 $this.parent().children('a').remove();
4299 * Creates a message inside an object with a sliding effect
4301 * @param msg A string containing the text to display
4302 * @param $obj a jQuery object containing the reference
4303 * to the element where to put the message
4304 * This is optional, if no element is
4305 * provided, one will be created below the
4306 * navigation links at the top of the page
4308 * @return bool True on success, false on failure
4310 Functions.slidingMessage = function (msg, $object) {
4312 if (msg === undefined || msg.length === 0) {
4313 // Don't show an empty message
4316 if ($obj === undefined || !($obj instanceof jQuery) || $obj.length === 0) {
4317 // If the second argument was not supplied,
4318 // we might have to create a new DOM node.
4319 if ($('#PMA_slidingMessage').length === 0) {
4320 $('#page_content').prepend(
4321 '<span id="PMA_slidingMessage" ' +
4322 'class="pma_sliding_message"></span>'
4325 $obj = $('#PMA_slidingMessage');
4327 if ($obj.has('div').length > 0) {
4328 // If there already is a message inside the
4329 // target object, we must get rid of it
4333 .fadeOut(function () {
4338 .append('<div>' + msg + '</div>');
4339 // highlight any sql before taking height;
4340 Functions.highlightSql($obj);
4346 height: $obj.find('div').first().height()
4353 // Object does not already have a message
4354 // inside it, so we simply slide it down
4356 .html('<div>' + msg + '</div>');
4357 // highlight any sql before taking height;
4358 Functions.highlightSql($obj);
4372 // Set the height of the parent
4373 // to the height of the child
4387 * Attach CodeMirror2 editor to SQL edit area.
4389 AJAX.registerOnload('functions.js', function () {
4390 var $elm = $('#sqlquery');
4391 if ($elm.siblings().filter('.CodeMirror').length > 0) {
4394 if ($elm.length > 0) {
4395 if (typeof CodeMirror !== 'undefined') {
4396 codeMirrorEditor = Functions.getSqlEditor($elm);
4397 codeMirrorEditor.focus();
4398 codeMirrorEditor.on('blur', Functions.updateQueryParameters);
4400 // without codemirror
4401 $elm.trigger('focus').on('blur', Functions.updateQueryParameters);
4404 Functions.highlightSql($('body'));
4406 AJAX.registerTeardown('functions.js', function () {
4407 if (codeMirrorEditor) {
4408 $('#sqlquery').text(codeMirrorEditor.getValue());
4409 codeMirrorEditor.toTextArea();
4410 codeMirrorEditor = false;
4413 AJAX.registerOnload('functions.js', function () {
4414 // initializes all lock-page elements lock-id and
4415 // val-hash data property
4416 $('#page_content form.lock-page textarea, ' +
4417 '#page_content form.lock-page input[type="text"], ' +
4418 '#page_content form.lock-page input[type="number"], ' +
4419 '#page_content form.lock-page select').each(function (i) {
4420 $(this).data('lock-id', i);
4421 // val-hash is the hash of default value of the field
4422 // so that it can be compared with new value hash
4423 // to check whether field was modified or not.
4424 $(this).data('val-hash', AJAX.hash($(this).val()));
4427 // initializes lock-page elements (input types checkbox and radio buttons)
4428 // lock-id and val-hash data property
4429 $('#page_content form.lock-page input[type="checkbox"], ' +
4430 '#page_content form.lock-page input[type="radio"]').each(function (i) {
4431 $(this).data('lock-id', i);
4432 $(this).data('val-hash', AJAX.hash($(this).is(':checked')));
4437 * jQuery plugin to correctly filter input fields by value, needed
4438 * because some nasty values may break selector syntax
4441 $.fn.filterByValue = function (value) {
4442 return this.filter(function () {
4443 return $(this).val() === value;
4449 * Return value of a cell in a table.
4451 Functions.getCellValue = function (td) {
4453 if ($td.is('.null')) {
4455 } else if ((! $td.is('.to_be_saved')
4457 && $td.data('original_data')
4459 return $td.data('original_data');
4465 $(window).on('popstate', function () {
4466 $('#printcss').attr('media','print');
4471 * Unbind all event handlers before tearing down a page
4473 AJAX.registerTeardown('functions.js', function () {
4474 $(document).off('click', 'a.themeselect');
4475 $(document).off('change', '.autosubmit');
4476 $('a.take_theme').off('click');
4479 AJAX.registerOnload('functions.js', function () {
4483 $(document).on('click', 'a.themeselect', function (e) {
4487 'left=10,top=20,width=510,height=350,scrollbars=yes,status=yes,resizable=yes'
4493 * Automatic form submission on change.
4495 $(document).on('change', '.autosubmit', function () {
4496 $(this).closest('form').trigger('submit');
4502 $('a.take_theme').on('click', function () {
4503 var what = this.name;
4504 /* eslint-disable compat/compat */
4505 if (window.opener && window.opener.document.forms.setTheme.elements.set_theme) {
4506 window.opener.document.forms.setTheme.elements.set_theme.value = what;
4507 window.opener.document.forms.setTheme.submit();
4511 /* eslint-enable compat/compat */
4517 * Produce print preview
4519 Functions.printPreview = function () {
4520 $('#printcss').attr('media','all');
4521 Functions.createPrintAndBackButtons();
4525 * Create print and back buttons in preview page
4527 Functions.createPrintAndBackButtons = function () {
4528 var backButton = $('<input>',{
4530 value: Messages.back,
4531 class: 'btn btn-secondary',
4532 id: 'back_button_print_view'
4534 backButton.on('click', Functions.removePrintAndBackButton);
4535 backButton.appendTo('#page_content');
4536 var printButton = $('<input>',{
4538 value: Messages.print,
4539 class: 'btn btn-primary',
4540 id: 'print_button_print_view'
4542 printButton.on('click', Functions.printPage);
4543 printButton.appendTo('#page_content');
4547 * Remove print and back buttons and revert to normal view
4549 Functions.removePrintAndBackButton = function () {
4550 $('#printcss').attr('media','print');
4551 $('#back_button_print_view').remove();
4552 $('#print_button_print_view').remove();
4558 Functions.printPage = function () {
4559 if (typeof(window.print) !== 'undefined') {
4565 * Unbind all event handlers before tearing down a page
4567 AJAX.registerTeardown('functions.js', function () {
4568 $('input#print').off('click');
4569 $(document).off('click', 'a.create_view.ajax');
4570 $(document).off('keydown', '#createViewDialog input, #createViewDialog select');
4571 $(document).off('change', '#fkc_checkbox');
4574 AJAX.registerOnload('functions.js', function () {
4575 $('input#print').on('click', Functions.printPage);
4576 $('.logout').on('click', function () {
4578 '<form method="POST" action="' + $(this).attr('href') + '" class="disableAjax">' +
4579 '<input type="hidden" name="token" value="' + Functions.escapeHtml(CommonParams.get('token')) + '">' +
4582 $('body').append(form);
4584 sessionStorage.clear();
4588 * Ajaxification for the "Create View" action
4590 $(document).on('click', 'a.create_view.ajax', function (e) {
4592 Functions.createViewDialog($(this));
4595 * Attach Ajax event handlers for input fields in the editor
4596 * and used to submit the Ajax request when the ENTER key is pressed.
4598 if ($('#createViewDialog').length !== 0) {
4599 $(document).on('keydown', '#createViewDialog input, #createViewDialog select', function (e) {
4600 if (e.which === 13) { // 13 is the ENTER key
4603 // with preventing default, selection by <select> tag
4604 // was also prevented in IE
4605 $(this).trigger('blur');
4607 $(this).closest('.ui-dialog').find('.ui-button').first().trigger('click');
4609 }); // end $(document).on()
4612 if ($('textarea[name="view[as]"]').length !== 0) {
4613 codeMirrorEditor = Functions.getSqlEditor($('textarea[name="view[as]"]'));
4617 Functions.createViewDialog = function ($this) {
4618 var $msg = Functions.ajaxShowMessage();
4619 var sep = CommonParams.get('arg_separator');
4620 var params = Functions.getJsConfirmCommonParam(this, $this.getPostData());
4621 params += sep + 'ajax_dialog=1';
4622 $.post($this.attr('href'), params, function (data) {
4623 if (typeof data !== 'undefined' && data.success === true) {
4624 Functions.ajaxRemoveMessage($msg);
4625 var buttonOptions = {};
4626 buttonOptions[Messages.strGo] = function () {
4627 if (typeof CodeMirror !== 'undefined') {
4628 codeMirrorEditor.save();
4630 $msg = Functions.ajaxShowMessage();
4631 $.post('index.php?route=/view/create', $('#createViewDialog').find('form').serialize(), function (data) {
4632 Functions.ajaxRemoveMessage($msg);
4633 if (typeof data !== 'undefined' && data.success === true) {
4634 $('#createViewDialog').dialog('close');
4635 $('.result_query').html(data.message);
4636 Navigation.reload();
4638 Functions.ajaxShowMessage(data.error);
4642 buttonOptions[Messages.strClose] = function () {
4643 $(this).dialog('close');
4645 var $dialog = $('<div></div>').attr('id', 'createViewDialog').append(data.message).dialog({
4649 buttons: buttonOptions,
4650 title: Messages.strCreateView,
4651 close: function () {
4655 // Attach syntax highlighted editor
4656 codeMirrorEditor = Functions.getSqlEditor($dialog.find('textarea'));
4657 $('input:visible[type=text]', $dialog).first().trigger('focus');
4659 Functions.ajaxShowMessage(data.error);
4665 * Makes the breadcrumbs and the menu bar float at the top of the viewport
4668 if ($('#floating_menubar').length && $('#PMA_disable_floating_menubar').length === 0) {
4669 var left = $('html').attr('dir') === 'ltr' ? 'left' : 'right';
4670 $('#floating_menubar')
4671 .css('margin-' + left, $('#pma_navigation').width() + $('#pma_navigation_resizer').width())
4674 'position': 'fixed',
4679 .append($('#server-breadcrumb'))
4680 .append($('#topmenucontainer'));
4681 // Allow the DOM to render, then adjust the padding on the body
4682 setTimeout(function () {
4685 $('#floating_menubar').outerHeight(true)
4687 $('#topmenu').menuResizer('resize');
4693 * Scrolls the page to the top if clicking the server-breadcrumb bar
4696 $(document).on('click', '#server-breadcrumb, #goto_pagetop', function (event) {
4697 event.preventDefault();
4698 $('html, body').animate({ scrollTop: 0 }, 'fast');
4702 var checkboxesSel = 'input.checkall:checkbox:enabled';
4703 Functions.checkboxesSel = checkboxesSel;
4706 * Watches checkboxes in a form to set the checkall box accordingly
4708 Functions.checkboxesChanged = function () {
4709 var $form = $(this.form);
4710 // total number of checkboxes in current form
4711 var totalBoxes = $form.find(checkboxesSel).length;
4712 // number of checkboxes checked in current form
4713 var checkedBoxes = $form.find(checkboxesSel + ':checked').length;
4714 var $checkall = $form.find('input.checkall_box');
4715 if (totalBoxes === checkedBoxes) {
4716 $checkall.prop({ checked: true, indeterminate: false });
4717 } else if (checkedBoxes > 0) {
4718 $checkall.prop({ checked: true, indeterminate: true });
4720 $checkall.prop({ checked: false, indeterminate: false });
4724 $(document).on('change', checkboxesSel, Functions.checkboxesChanged);
4726 $(document).on('change', 'input.checkall_box', function () {
4727 var isChecked = $(this).is(':checked');
4728 $(this.form).find(checkboxesSel).not('.row-hidden').prop('checked', isChecked)
4729 .parents('tr').toggleClass('marked', isChecked);
4732 $(document).on('click', '.checkall-filter', function () {
4733 var $this = $(this);
4734 var selector = $this.data('checkall-selector');
4735 $('input.checkall_box').prop('checked', false);
4736 $this.parents('form').find(checkboxesSel).filter(selector).prop('checked', true).trigger('change')
4737 .parents('tr').toggleClass('marked', true);
4742 * Watches checkboxes in a sub form to set the sub checkall box accordingly
4744 Functions.subCheckboxesChanged = function () {
4745 var $form = $(this).parent().parent();
4746 // total number of checkboxes in current sub form
4747 var totalBoxes = $form.find(checkboxesSel).length;
4748 // number of checkboxes checked in current sub form
4749 var checkedBoxes = $form.find(checkboxesSel + ':checked').length;
4750 var $checkall = $form.find('input.sub_checkall_box');
4751 if (totalBoxes === checkedBoxes) {
4752 $checkall.prop({ checked: true, indeterminate: false });
4753 } else if (checkedBoxes > 0) {
4754 $checkall.prop({ checked: true, indeterminate: true });
4756 $checkall.prop({ checked: false, indeterminate: false });
4760 $(document).on('change', checkboxesSel + ', input.checkall_box:checkbox:enabled', Functions.subCheckboxesChanged);
4762 $(document).on('change', 'input.sub_checkall_box', function () {
4763 var isChecked = $(this).is(':checked');
4764 var $form = $(this).parent().parent();
4765 $form.find(checkboxesSel).prop('checked', isChecked)
4766 .parents('tr').toggleClass('marked', isChecked);
4772 * - rows to filter are identified by data-filter-row attribute
4773 * which contains uppercase string to filter
4774 * - it is simple substring case insensitive search
4775 * - optionally number of matching rows is written to element with
4776 * id filter-rows-count
4778 $(document).on('keyup', '#filterText', function () {
4779 var filterInput = $(this).val().toUpperCase().replace(/ /g, '_');
4781 $('[data-filter-row]').each(function () {
4783 /* Can not use data() here as it does magic conversion to int for numeric values */
4784 if ($row.attr('data-filter-row').indexOf(filterInput) > -1) {
4787 $row.find('input.checkall').removeClass('row-hidden');
4790 $row.find('input.checkall').addClass('row-hidden').prop('checked', false);
4791 $row.removeClass('marked');
4794 setTimeout(function () {
4795 $(checkboxesSel).trigger('change');
4797 $('#filter-rows-count').html(count);
4799 AJAX.registerOnload('functions.js', function () {
4800 /* Trigger filtering of the list based on incoming database name */
4801 var $filter = $('#filterText');
4802 if ($filter.val()) {
4803 $filter.trigger('keyup').trigger('select');
4808 * Formats a byte number to human-readable form
4810 * @param bytes the bytes to format
4811 * @param optional subdecimals the number of digits after the point
4812 * @param optional pointchar the char to use as decimal point
4814 Functions.formatBytes = function (bytesToFormat, subDecimals, pointChar) {
4815 var bytes = bytesToFormat;
4816 var decimals = subDecimals;
4817 var point = pointChar;
4824 var units = ['B', 'KiB', 'MiB', 'GiB'];
4825 for (var i = 0; bytes > 1024 && i < units.length; i++) {
4828 var factor = Math.pow(10, decimals);
4829 bytes = Math.round(bytes * factor) / factor;
4830 bytes = bytes.toString().split('.').join(point);
4831 return bytes + ' ' + units[i];
4834 AJAX.registerOnload('functions.js', function () {
4836 * Reveal the login form to users with JS enabled
4837 * and focus the appropriate input field
4839 var $loginform = $('#loginform');
4840 if ($loginform.length) {
4841 $loginform.find('.js-show').show();
4842 if ($('#input_username').val()) {
4843 $('#input_password').trigger('focus');
4845 $('#input_username').trigger('focus');
4848 var $httpsWarning = $('#js-https-mismatch');
4849 if ($httpsWarning.length) {
4850 if ((window.location.protocol === 'https:') !== CommonParams.get('is_https')) {
4851 $httpsWarning.show();
4857 * Formats timestamp for display
4859 Functions.formatDateTime = function (date, seconds) {
4860 var result = $.datepicker.formatDate('yy-mm-dd', date);
4861 var timefmt = 'HH:mm';
4863 timefmt = 'HH:mm:ss';
4865 return result + ' ' + $.datepicker.formatTime(
4867 hour: date.getHours(),
4868 minute: date.getMinutes(),
4869 second: date.getSeconds()
4875 * Check than forms have less fields than max allowed by PHP.
4877 Functions.checkNumberOfFields = function () {
4878 if (typeof maxInputVars === 'undefined') {
4881 if (false === maxInputVars) {
4884 $('form').each(function () {
4885 var nbInputs = $(this).find(':input').length;
4886 if (nbInputs > maxInputVars) {
4887 var warning = Functions.sprintf(Messages.strTooManyInputs, maxInputVars);
4888 Functions.ajaxShowMessage(warning);
4898 * Ignore the displayed php errors.
4899 * Simply removes the displayed errors.
4901 * @param clearPrevErrors whether to clear errors stored
4902 * in $_SESSION['prev_errors'] at server
4905 Functions.ignorePhpErrors = function (clearPrevErrors) {
4906 var clearPrevious = clearPrevErrors;
4907 if (typeof(clearPrevious) === 'undefined' ||
4908 clearPrevious === null
4910 clearPrevious = false;
4912 // send AJAX request to /error-report with send_error_report=0, exception_type=php & token.
4913 // It clears the prev_errors stored in session.
4914 if (clearPrevious) {
4915 var $pmaReportErrorsForm = $('#pma_report_errors_form');
4916 $pmaReportErrorsForm.find('input[name="send_error_report"]').val(0); // change send_error_report to '0'
4917 $pmaReportErrorsForm.trigger('submit');
4920 // remove displayed errors
4921 var $pmaErrors = $('#pma_errors');
4922 $pmaErrors.fadeOut('slow');
4923 $pmaErrors.remove();
4927 * Toggle the Datetimepicker UI if the date value entered
4928 * by the user in the 'text box' is not going to be accepted
4929 * by the Datetimepicker plugin (but is accepted by MySQL)
4931 Functions.toggleDatepickerIfInvalid = function ($td, $inputField) {
4932 // Regex allowed by the Datetimepicker UI
4933 var dtexpDate = new RegExp(['^([0-9]{4})',
4934 '-(((01|03|05|07|08|10|12)-((0[1-9])|([1-2][0-9])|(3[0-1])))|((02|04|06|09|11)',
4935 '-((0[1-9])|([1-2][0-9])|30)))$'].join(''));
4936 var dtexpTime = new RegExp(['^(([0-1][0-9])|(2[0-3]))',
4937 ':((0[0-9])|([1-5][0-9]))',
4938 ':((0[0-9])|([1-5][0-9]))(.[0-9]{1,6}){0,1}$'].join(''));
4940 // If key-ed in Time or Date values are unsupported by the UI, close it
4941 if ($td.attr('data-type') === 'date' && ! dtexpDate.test($inputField.val())) {
4942 $inputField.datepicker('hide');
4943 } else if ($td.attr('data-type') === 'time' && ! dtexpTime.test($inputField.val())) {
4944 $inputField.datepicker('hide');
4946 $inputField.datepicker('show');
4951 * Function to submit the login form after validation is done.
4952 * NOTE: do NOT use a module or it will break the callback, issue #15435
4954 // eslint-disable-next-line no-unused-vars, camelcase
4955 var Functions_recaptchaCallback = function () {
4956 $('#login_form').trigger('submit');
4960 * Unbind all event handlers before tearing down a page
4962 AJAX.registerTeardown('functions.js', function () {
4963 $(document).off('keydown', 'form input, form textarea, form select');
4966 AJAX.registerOnload('functions.js', function () {
4968 * Handle 'Ctrl/Alt + Enter' form submits
4970 $('form input, form textarea, form select').on('keydown', function (e) {
4971 if ((e.ctrlKey && e.which === 13) || (e.altKey && e.which === 13)) {
4972 var $form = $(this).closest('form');
4974 // There could be multiple submit buttons on the same form,
4975 // we assume all of them behave identical and just click one.
4976 if (! $form.find('input[type="submit"]').first() ||
4977 ! $form.find('input[type="submit"]').first().trigger('click')
4979 $form.trigger('submit');
4986 * Unbind all event handlers before tearing down a page
4988 AJAX.registerTeardown('functions.js', function () {
4989 $(document).off('change', 'input[type=radio][name="pw_hash"]');
4990 $(document).off('mouseover', '.sortlink');
4991 $(document).off('mouseout', '.sortlink');
4994 AJAX.registerOnload('functions.js', function () {
4996 * Display warning regarding SSL when sha256_password
4997 * method is selected
4998 * Used in /user-password (Change Password link on index.php)
5000 $(document).on('change', 'select#select_authentication_plugin_cp', function () {
5001 if (this.value === 'sha256_password') {
5002 $('#ssl_reqd_warning_cp').show();
5004 $('#ssl_reqd_warning_cp').hide();
5008 Cookies.defaults.path = CommonParams.get('rootPath');
5010 // Bind event handlers for toggling sort icons
5011 $(document).on('mouseover', '.sortlink', function () {
5012 $(this).find('.soimg').toggle();
5014 $(document).on('mouseout', '.sortlink', function () {
5015 $(this).find('.soimg').toggle();
5020 * Returns an HTML IMG tag for a particular image from a theme,
5021 * which may be an actual file or an icon from a sprite
5023 * @param string image The name of the file to get
5024 * @param string alternate Used to set 'alt' and 'title' attributes of the image
5025 * @param object attributes An associative array of other attributes
5027 * @return Object The requested image, this object has two methods:
5028 * .toString() - Returns the IMG tag for the requested image
5029 * .attr(name) - Returns a particular attribute of the IMG
5030 * tag given it's name
5031 * .attr(name, value) - Sets a particular attribute of the IMG
5032 * tag to the given value
5034 Functions.getImage = function (image, alternate, attributes) {
5035 var alt = alternate;
5036 var attr = attributes;
5037 // custom image object, it will eventually be returned by this functions
5043 src: 'themes/dot.gif',
5045 attr: function (name, value) {
5046 if (value === undefined) {
5047 if (this.data[name] === undefined) {
5050 return this.data[name];
5053 this.data[name] = value;
5056 toString: function () {
5057 var retval = '<' + 'img';
5058 for (var i in this.data) {
5059 retval += ' ' + i + '="' + this.data[i] + '"';
5061 retval += ' /' + '>';
5065 // initialise missing parameters
5066 if (attr === undefined) {
5069 if (alt === undefined) {
5073 if (attr.alt !== undefined) {
5074 retval.attr('alt', Functions.escapeHtml(attr.alt));
5076 retval.attr('alt', Functions.escapeHtml(alt));
5079 if (attr.title !== undefined) {
5080 retval.attr('title', Functions.escapeHtml(attr.title));
5082 retval.attr('title', Functions.escapeHtml(alt));
5085 retval.attr('class', 'icon ic_' + image);
5086 // set all other attributes
5087 for (var i in attr) {
5089 // do not allow to override the 'src' attribute
5093 retval.attr(i, attr[i]);
5100 * Sets a configuration value.
5102 * A configuration value may be set in both browser's local storage and
5103 * remotely in server's configuration table.
5105 * NOTE: Depending on server's configuration, the configuration table may be or
5108 * @param {string} key Configuration key.
5109 * @param {object} value Configuration value.
5111 Functions.configSet = function (key, value) {
5112 var serialized = JSON.stringify(value);
5113 localStorage.setItem(key, serialized);
5115 url: 'index.php?route=/ajax/config-set',
5119 'ajax_request': true,
5121 server: CommonParams.get('server'),
5124 success: function (data) {
5125 // Updating value in local storage.
5126 if (! data.success) {
5128 Functions.ajaxShowMessage(data.error);
5130 Functions.ajaxShowMessage(data.message);
5133 // Eventually, call callback.
5139 * Gets a configuration value. A configuration value will be searched in
5140 * browser's local storage first and if not found, a call to the server will be
5143 * If value should not be cached and the up-to-date configuration value from
5144 * right from the server is required, the third parameter should be `false`.
5146 * @param {string} key Configuration key.
5147 * @param {boolean} cached Configuration type.
5149 * @return {object} Configuration value.
5151 Functions.configGet = function (key, cached) {
5152 var isCached = (typeof cached !== 'undefined') ? cached : true;
5153 var value = localStorage.getItem(key);
5154 if (isCached && value !== undefined && value !== null) {
5155 return JSON.parse(value);
5158 // Result not found in local storage or ignored.
5159 // Hitting the server.
5161 // TODO: This is ugly, but usually when a configuration is needed,
5162 // processing cannot continue until that value is found.
5163 // Another solution is to provide a callback as a parameter.
5165 url: 'index.php?route=/ajax/config-get',
5169 'ajax_request': true,
5170 server: CommonParams.get('server'),
5173 success: function (data) {
5174 // Updating value in local storage.
5176 localStorage.setItem(key, JSON.stringify(data.value));
5178 Functions.ajaxShowMessage(data.message);
5180 // Eventually, call callback.
5183 return JSON.parse(localStorage.getItem(key));
5187 * Return POST data as stored by Generator::linkOrButton
5189 Functions.getPostData = function () {
5190 var dataPost = this.attr('data-post');
5191 // Strip possible leading ?
5192 if (dataPost !== undefined && dataPost.substring(0,1) === '?') {
5193 dataPost = dataPost.substr(1);
5197 jQuery.fn.getPostData = Functions.getPostData;