codekingpro/portable-devtools
114k
1//////////////////////////////////////////////////////////////////////////2//3// pgAdmin 4 - PostgreSQL Tools4//5// Copyright (C) 2013 - 2024, The pgAdmin Development Team6// This software is released under the PostgreSQL Licence7//8//////////////////////////////////////////////////////////////////////////9 10import _ from 'lodash';11import gettext from 'sources/gettext';12import { hasTrojanSource } from 'anti-trojan-source';13import convert from 'convert-units';14import getApiInstance from './api_instance';15import usePreferences from '../../preferences/static/js/store';16import pgAdmin from 'sources/pgadmin';17import { isMac } from './keyboard_shortcuts';18 19export function parseShortcutValue(obj) {20 let shortcut = '';21 if (!obj){22 return null;23 }24 if (obj.alt) { shortcut += 'alt+'; }25 if (obj.shift) { shortcut += 'shift+'; }26 if (obj.control) { shortcut += 'ctrl+'; }27 shortcut += obj?.key.char?.toLowerCase();28 return shortcut;29}30 31export function isShortcutValue(obj) {32 if(!obj) return false;33 return [obj.alt, obj.control, obj?.key, obj?.key?.char].every((k)=>!_.isUndefined(k));34}35 36// Convert shortcut obj to codemirror key format37export function toCodeMirrorKey(obj) {38 let shortcut = '';39 if (!obj){40 return shortcut;41 }42 if (obj.alt) { shortcut += 'Alt-'; }43 if (obj.shift) { shortcut += 'Shift-'; }44 if (obj.control) {45 if(isMac() && obj.ctrl_is_meta) {46 shortcut += 'Meta-';47 } else {48 shortcut += 'Ctrl-';49 }50 }51 if(obj?.key.char?.length == 1) {52 shortcut += obj?.key.char?.toLowerCase();53 } else {54 shortcut += obj?.key.char;55 }56 return shortcut;57}58 59export function getEpoch(inp_date) {60 let date_obj = inp_date || new Date();61 return parseInt(date_obj.getTime()/1000);62}63 64/* Eucladian GCD */65export function getGCD(inp_arr) {66 let gcd_for_two = (a, b) => {67 return a == 0?b:gcd_for_two(b % a, a);68 };69 70 let inp_len = inp_arr.length;71 if(inp_len <= 2) {72 return gcd_for_two(inp_arr[0], inp_arr[1]);73 }74 75 let result = inp_arr[0];76 for(let i=1; i<inp_len; i++) {77 result = gcd_for_two(inp_arr[i], result);78 }79 80 return result;81}82 83export function getMod(no, divisor) {84 return ((no % divisor) + divisor) % divisor;85}86 87export function parseFuncParams(label) {88 let paramArr = [],89 funcName = '',90 paramStr = '';91 92 if(label.endsWith('()')) {93 funcName = label.substring(0, label.length-2);94 } else if(!label.endsWith(')')) {95 funcName = label;96 } else if(!label.endsWith('()') && label.endsWith(')')) {97 let i = 0,98 startBracketPos = label.length;99 100 /* Parse through the characters in reverse to find the param start bracket */101 i = label.length-2;102 while(i >= 0) {103 if(label[i] == '(') {104 startBracketPos = i;105 break;106 } else if(label[i] == '"') {107 /* If quotes, skip all the chars till next quote */108 i--;109 while(label[i] != '"') i--;110 }111 i--;112 }113 114 funcName = label.substring(0, startBracketPos);115 paramStr = label.substring(startBracketPos+1, label.length-1);116 117 let paramStart = 0,118 paramName = '',119 paramModes = ['IN', 'OUT', 'INOUT', 'VARIADIC'];120 121 i = 0;122 while(i < paramStr.length) {123 if(paramStr[i] == '"') {124 /* If quotes, skip all the chars till next quote */125 i++;126 while(paramStr[i] != '"') i++;127 } else if (paramStr[i] == ' ') {128 /* if paramName is already set, ignore till comma129 * Or if paramName is parsed as one of the modes, reset.130 */131 if(paramName == '' || paramModes.indexOf(paramName) > -1 ) {132 paramName = paramStr.substring(paramStart, i);133 paramStart = i+1;134 }135 }136 else if (paramStr[i] == ',') {137 paramArr.push([paramName, paramStr.substring(paramStart, i)]);138 paramName = '';139 paramStart = i+1;140 }141 i++;142 }143 paramArr.push([paramName, paramStr.substring(paramStart)]);144 }145 146 return {147 'func_name': funcName,148 'param_string': paramStr,149 'params': paramArr,150 };151}152 153export function quote_ident(value) {154 /* check if the string is number or not */155 let quoteIt = false;156 if (!isNaN(parseInt(value))){157 quoteIt = true;158 }159 160 if(value.search(/[^a-z0-9_]/g) > -1) {161 /* escape double quotes */162 value = value.replace(/"/g, '""');163 quoteIt = true;164 }165 166 if(quoteIt) {167 return `"${value}"`;168 } else {169 return value;170 }171}172 173export function fully_qualify(pgBrowser, data, item) {174 const parentData = pgBrowser.tree.getTreeNodeHierarchy(item);175 let namespace = '';176 177 if (parentData.schema !== undefined) {178 namespace = quote_ident(parentData.schema._label);179 }180 else if (parentData.view !== undefined) {181 namespace = quote_ident(parentData.view._label);182 }183 else if (parentData.catalog !== undefined) {184 namespace = quote_ident(parentData.catalog._label);185 }186 187 if (parentData.package !== undefined && data._type != 'package') {188 if(namespace == '') {189 namespace = quote_ident(parentData.package._label);190 } else {191 namespace += '.' + quote_ident(parentData.package._label);192 }193 }194 195 if(namespace != '') {196 return namespace + '.' + quote_ident(data._label);197 } else {198 return quote_ident(data._label);199 }200}201 202export function getRandomInt(min, max) {203 const intArray = new Uint32Array(1);204 crypto.getRandomValues(intArray);205 206 let range = max - min + 1;207 return min + (intArray[0] % range);208}209 210export function titleize(i_str) {211 if(i_str === '' || i_str === null) return i_str;212 return i_str.split(' ')213 .map(w => w[0].toUpperCase() + w.substr(1).toLowerCase())214 .join(' ');215}216 217export function sprintf(i_str) {218 try {219 let replaceArgs = arguments;220 return i_str.split('%s')221 .map(function(w, i) {222 if(i > 0) {223 if(i < replaceArgs.length) {224 return [replaceArgs[i], w].join('');225 } else {226 return ['%s', w].join('');227 }228 } else {229 return w;230 }231 })232 .join('');233 } catch(e) {234 console.error(e);235 return i_str;236 }237}238 239// Modified ref: http://stackoverflow.com/a/1293163/2343 to suite pgAdmin.240// This will parse a delimited string into an array of arrays.241export function CSVToArray(strData, strDelimiter, quoteChar){242 strDelimiter = strDelimiter || ',';243 quoteChar = quoteChar || '"';244 245 // Create a regular expression to parse the CSV values.246 let objPattern = new RegExp(247 (248 // Delimiters.249 '(\\' + strDelimiter + '|\\r?\\n|\\r|^)' +250 // Quoted fields.251 (quoteChar == '"' ? '(?:"([^"]*(?:""[^"]*)*)"|' : '(?:\'([^\']*(?:\'\'[^\']*)*)\'|') +252 // Standard fields.253 (quoteChar == '"' ? '([^"\\' + strDelimiter + '\\r\\n]*))': '([^\'\\' + strDelimiter + '\\r\\n]*))')254 ),255 'gi'256 );257 258 // Create an array to hold our data. Give the array259 // a default empty first row.260 let arrData = [[]];261 262 // The regex doesn't handle and skips start value if263 // string starts with delimiter264 if(strData.startsWith(strDelimiter)) {265 arrData[ arrData.length - 1 ].push(null);266 }267 268 // Create an array to hold our individual pattern269 // matching groups.270 let arrMatches = null;271 272 // Keep looping over the regular expression matches273 // until we can no longer find a match.274 while ((arrMatches = objPattern.exec( strData ))){275 // Get the delimiter that was found.276 let strMatchedDelimiter = arrMatches[ 1 ];277 278 // Check to see if the given delimiter has a length279 // (is not the start of string) and if it matches280 // field delimiter. If id does not, then we know281 // that this delimiter is a row delimiter.282 if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter){283 // Since we have reached a new row of data,284 // add an empty row to our data array.285 arrData.push( [] );286 }287 288 let strMatchedValue;289 290 // Now that we have our delimiter out of the way,291 // let's check to see which kind of value we292 // captured (quoted or unquoted).293 if (arrMatches[ 2 ]){294 // We found a quoted value. When we capture295 // this value, unescape any quotes.296 strMatchedValue = arrMatches[ 2 ].replace(new RegExp( quoteChar+quoteChar, 'g' ), quoteChar);297 } else {298 // We found a non-quoted value.299 strMatchedValue = arrMatches[ 3 ];300 }301 // Now that we have our value string, let's add302 // it to the data array.303 arrData[ arrData.length - 1 ].push( strMatchedValue );304 }305 // Return the parsed data.306 return arrData;307}308 309export function hasBinariesConfiguration(pgBrowser, serverInformation) {310 const module = 'paths';311 let preference_name = 'pg_bin_dir';312 let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');313 314 if ((serverInformation.type && serverInformation.type === 'ppas') ||315 serverInformation.server_type === 'ppas') {316 preference_name = 'ppas_bin_dir';317 msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');318 }319 320 const preference = usePreferences.getState().getPreferences(module, preference_name);321 322 if (preference) {323 if (_.isUndefined(preference.value) || !checkBinaryPathExists(preference.value, serverInformation.version)) {324 pgAdmin.Browser.notifier.alert(gettext('Configuration required'), msg);325 return false;326 }327 } else {328 pgAdmin.Browser.notifier.alert(329 gettext('Preferences Error'),330 gettext('Failed to load preference %s of module %s', preference_name, module)331 );332 return false;333 }334 return true;335}336 337function checkBinaryPathExists(binaryPathArray, selectedServerVersion) {338 let foundDefaultPath = false,339 serverSpecificPathExist = false,340 binPathArray = JSON.parse(binaryPathArray);341 342 _.each(binPathArray, function(binPath) {343 if (selectedServerVersion >= binPath.version && selectedServerVersion < binPath.next_major_version) {344 if (!_.isUndefined(binPath.binaryPath) && !_.isNull(binPath.binaryPath) && binPath.binaryPath.trim() !== '')345 serverSpecificPathExist = true;346 }347 348 // Check for default path349 if (binPath.isDefault) {350 foundDefaultPath = true;351 }352 });353 354 return (serverSpecificPathExist | foundDefaultPath);355}356 357/* If a function, then evaluate */358export function evalFunc(obj, func, ...param) {359 if(_.isFunction(func)) {360 return func.apply(obj, [...param]);361 }362 return func;363}364 365export function getBrowser() {366 let ua=navigator.userAgent,tem,M=ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];367 if(/trident/i.test(M[1])) {368 tem=/\brv[ :]+(\d+)/g.exec(ua) || [];369 return {name:'IE', version:(tem[1]||'')};370 }371 if(ua.startsWith('Nwjs')) {372 let nwjs = ua.split('-')[0]?.split(':');373 return {name:nwjs[0], version: nwjs[1]};374 }375 376 if(M[1]==='Chrome') {377 tem=ua.match(/\bOPR|Edge\/(\d+)/);378 if(tem!=null) {return {name:tem[0], version:tem[1]};}379 }380 381 M=M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?'];382 if((tem=ua.match(/version\/(\d+)/i))!=null) {M.splice(1,1,tem[1]);}383 return {384 name: M[0],385 version: M[1],386 };387}388 389export function checkTrojanSource(content, isPasteEvent) {390 // Call the hasTrojanSource function of 'anti-trojan-source' package391 if (hasTrojanSource({ sourceText: content})) {392 let msg = gettext('The file opened contains bidirectional Unicode characters which could be interpreted differently than what is displayed. If this is unexpected it is recommended that you review the text in an application that can display hidden Unicode characters before proceeding.');393 if (isPasteEvent) {394 msg = gettext('The pasted text contains bidirectional Unicode characters which could be interpreted differently than what is displayed. If this is unexpected it is recommended that you review the text in an application that can display hidden Unicode characters before proceeding.');395 }396 pgAdmin.Browser.notifier.alert(gettext('Trojan Source Warning'), msg);397 }398}399 400export function downloadBlob(blob, fileName) {401 let urlCreator = window.URL || window.webkitURL,402 downloadUrl = urlCreator.createObjectURL(blob),403 link = document.createElement('a');404 405 document.body.appendChild(link);406 407 if (getBrowser() == 'IE' && window.navigator.msSaveBlob) {408 // IE10+ : (has Blob, but not a[download] or URL)409 window.navigator.msSaveBlob(blob, fileName);410 } else {411 link.setAttribute('href', downloadUrl);412 link.setAttribute('download', fileName);413 link.click();414 }415 document.body.removeChild(link);416}417 418export function toPrettySize(rawSize, from='B') {419 try {420 //if the integer need to be converted to K for thousands, M for millions , B for billions only421 if (from == '') {422 return Intl.NumberFormat('en', {notation: 'compact'}).format(rawSize);423 }424 let conVal = convert(rawSize).from(from).toBest();425 conVal.val = Math.round(conVal.val * 100) / 100;426 return `${conVal.val} ${conVal.unit}`;427 }428 catch {429 return '';430 }431}432 433export function compareSizeVals(val1, val2) {434 const parseAndConvert = (val)=>{435 try {436 let [size, unit] = val.split(' ');437 return convert(size).from(unit.toUpperCase()).to('B');438 } catch {439 return -1;440 }441 };442 val1 = parseAndConvert(val1);443 val2 = parseAndConvert(val2);444 if(val1 > val2) return 1;445 return (val1 < val2 ? -1 : 0);446}447 448export function calcFontSize(fontSize) {449 if(fontSize) {450 fontSize = parseFloat((Math.round(parseFloat(fontSize + 'e+2')) + 'e-2'));451 let rounded = Number(fontSize);452 if(rounded > 0) {453 return rounded + 'em';454 }455 }456 return '1em';457}458 459export function pgHandleItemError(error, args) {460 let pgBrowser = window.pgAdmin.Browser;461 462 if (!error || !pgBrowser) {463 return;464 }465 466 if(error.response.headers['content-type'] == 'application/json') {467 let jsonResp = error.response.data;468 if (469 jsonResp && (470 error.response.status == 503 ? (471 jsonResp.info == 'CONNECTION_LOST' &&472 'server' in args.info && jsonResp.data.sid >= 0 &&473 jsonResp.data.sid == args.info.server._id474 ) : (475 error.response.status == 428 &&476 jsonResp.errormsg &&477 jsonResp.errormsg == gettext('Connection to the server has been lost.')478 )479 )480 ) {481 if (482 args.preHandleConnectionLost &&483 typeof(args.preHandleConnectionLost) == 'function'484 ) {485 args.preHandleConnectionLost.apply(this, arguments);486 }487 488 // Check the status of the maintenance server connection.489 let server = pgBrowser.Nodes['server'],490 ctx = {491 resp: jsonResp,492 error: error,493 args: args,494 },495 reconnectServer = function() {496 let ctx_local = this,497 onServerConnect = function(_sid, _i, _d) {498 // Yay - server is reconnected.499 if (this.args.info.server._id == _sid) {500 pgBrowser.Events.off(501 'pgadmin:server:connected', onServerConnect502 );503 pgBrowser.Events.off(504 'pgadmin:server:connect:cancelled', onConnectCancel505 );506 507 // Do we need to connect the disconnected server now?508 if (509 this.resp.data.database &&510 this.resp.data.database != _d.db511 ) {512 // Server is connected now, we will need to inform the513 // database to connect it now.514 pgBrowser.Events.trigger(515 'pgadmin:database:connection:lost', this.args.item,516 this.resp, true517 );518 }519 }520 }.bind(ctx_local),521 onConnectCancel = function(_sid, _item, _data) {522 // User has cancelled the operation in between.523 if (_sid == this.args.info.server.id) {524 pgBrowser.Events.off('pgadmin:server:connected', onServerConnect);525 pgBrowser.Events.off('pgadmin:server:connect:cancelled', onConnectCancel);526 527 // Connection to the database will also be cancelled528 pgBrowser.Events.trigger(529 'pgadmin:database:connect:cancelled', _sid,530 this.resp.data.database || _data.db, _item, _data531 );532 }533 }.bind(ctx_local);534 535 pgBrowser.Events.on('pgadmin:server:connected', onServerConnect);536 pgBrowser.Events.on('pgadmin:server:connect:cancelled', onConnectCancel);537 538 // Connection to the server has been lost, we need to inform the539 // server first to take the action first.540 pgBrowser.Events.trigger(541 'pgadmin:server:connection:lost', this.args.item, this.resp542 );543 }.bind(ctx);544 545 getApiInstance().get(server.generate_url(546 null, 'connect', args.info.server, true, args.info547 ))548 .then(({data: res})=>{549 if (res.success && 'connected' in res.data) {550 if (res.data.connected) {551 // Server is connected, but - the connection with the552 // particular database has been lost.553 pgBrowser.Events.trigger(554 'pgadmin:database:connection:lost', args.item, jsonResp555 );556 return;557 }558 }559 560 // Server was not connected, we should first try to connect561 // the server.562 reconnectServer();563 })564 .catch(()=>{565 reconnectServer();566 });567 return true;568 } else if (jsonResp && jsonResp.info == 'CRYPTKEY_MISSING' && error.response.status == 503) {569 /* Suppress the error here and handle in pgNotifier wherever570 * required, as it has callback option571 */572 return false;573 }574 }575 576 return false;577}578 579export function fullHexColor(shortHex) {580 if(shortHex?.length == 4) {581 return shortHex.replace(RegExp('#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])'), '#$1$1$2$2$3$3').toUpperCase();582 }583 return shortHex;584}585 586export function gettextForTranslation(translations, ...replaceArgs) {587 const text = replaceArgs[0];588 let rawTranslation = translations[text] ? translations[text] : text;589 590 if(arguments.length == 2) {591 return rawTranslation;592 }593 594 try {595 return rawTranslation.split('%s')596 .map(function(w, i) {597 if(i > 0) {598 if(i < replaceArgs.length) {599 return [replaceArgs[i], w].join('');600 } else {601 return ['%s', w].join('');602 }603 } else {604 return w;605 }606 })607 .join('');608 } catch(e) {609 console.error(e);610 return rawTranslation;611 }612}613 614// https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame615const requestAnimationFrame =616 window.requestAnimationFrame ||617 window.mozRequestAnimationFrame ||618 window.webkitRequestAnimationFrame ||619 window.msRequestAnimationFrame;620 621const cancelAnimationFrame =622 window.cancelAnimationFrame || window.mozCancelAnimationFrame;623 624/* Usefull in focussing an element after it appears on the screen */625export function requestAnimationAndFocus(ele) {626 if(!ele) return;627 628 const animateId = requestAnimationFrame(()=>{629 ele?.focus?.();630 cancelAnimationFrame(animateId);631 });632}633 634 635export function scrollbarWidth() {636 // thanks too https://davidwalsh.name/detect-scrollbar-width637 const scrollDiv = document.createElement('div');638 scrollDiv.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position:absolute; top:-9999px;');639 document.body.appendChild(scrollDiv);640 const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;641 document.body.removeChild(scrollDiv);642 return scrollbarWidth;643}644 