diff --git a/docs/en_US/developer_tools.rst b/docs/en_US/developer_tools.rst index 1ff442cc..bb67e33a 100644 --- a/docs/en_US/developer_tools.rst +++ b/docs/en_US/developer_tools.rst @@ -16,3 +16,4 @@ PL/SQL code. editgrid schema_diff erd_tool + psql_tool diff --git a/docs/en_US/images/psql_tool.png b/docs/en_US/images/psql_tool.png new file mode 100644 index 0000000000000000000000000000000000000000..c5f88ba5a573aa60bf7e08b8a38526dc73e8fcd5 GIT binary patch literal 23736 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

?poBJSv;AZ0Y8 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 P=p;Q)S3j3^P6=5.0.1 +eventlet==0.30.2 diff --git a/web/config.py b/web/config.py index 2643ef19..002a9951 100644 --- a/web/config.py +++ b/web/config.py @@ -156,8 +156,8 @@ X_FRAME_OPTIONS = "SAMEORIGIN" # such as JavaScript, CSS, or pretty much anything that the browser loads. # see https://content-security-policy.com/#source_list for more info # e.g. "default-src https: data: 'unsafe-inline' 'unsafe-eval';" -CONTENT_SECURITY_POLICY = "default-src http: data: blob: 'unsafe-inline' " \ - "'unsafe-eval';" +CONTENT_SECURITY_POLICY = "default-src ws: http: data: blob: 'unsafe-inline'" \ + " 'unsafe-eval';" # STRICT_TRANSPORT_SECURITY_ENABLED when set to True will set the # Strict-Transport-Security header @@ -636,6 +636,19 @@ KRB_AUTO_CREATE_USER = True KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache') +# PSQL tool settings +# This will enable PSQL tool in pgAdmin. So user can execute the commands using +# PSQL terminal in pgAdmin. +ENABLE_PSQL = True + +# ALLOW_PSQL_SHELL_COMMAND = True will disable the execution of os level +# commands using meta command \! from PSQL terminal. +# As PSQL allow user to execute the os level commands from the PSQL terminal +# user can execute any system level command as per the system login user +# privileges. Default this setting is set to False but if it set to True +# User will able to execute the system level commands through PSQL terminal in +# pgAdmin. +ALLOW_PSQL_SHELL_COMMANDS = False ########################################################################## # Local config settings diff --git a/web/package.json b/web/package.json index cc1e15ab..fc2e8193 100644 --- a/web/package.json +++ b/web/package.json @@ -103,6 +103,7 @@ "json-bignumber": "^1.0.1", "karma-coverage": "^2.0.3", "leaflet": "^1.5.1", + "lodash": "4.*", "ml-matrix": "^6.5.0", "moment": "^2.24.0", @@ -118,13 +119,18 @@ "shim-loader": "^1.0.1", "slickgrid": "git+https://github.com/6pac/SlickGrid.git#2.3.16", "snapsvg-cjs": "^0.0.6", + "socket.io-client": "^4.0.0", "split.js": "^1.5.10", "tablesorter": "^2.31.2", "tempusdominus-bootstrap-4": "^5.1.2", "tempusdominus-core": "^5.0.3", "underscore": "^1.9.1", "webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker/#c4a3398b89588408dc705895675bce7bd7660d36", - "wkx": "^0.5.0" + "wkx": "^0.5.0", + "xterm": "^4.11.0", + "xterm-addon-fit": "^0.5.0", + "xterm-addon-search": "^0.8.0", + "xterm-addon-web-links": "^0.4.0" }, "scripts": { "linter": "yarn eslint --no-eslintrc -c .eslintrc.js --ext .js --ext .jsx .", diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index d2bd1af6..6e1e4ea6 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -15,6 +15,7 @@ to start a web server.""" import sys from cheroot.wsgi import Server as CherootServer + if sys.version_info < (3, 4): raise RuntimeError('This application must be run under Python 3.4 ' 'or later.') @@ -37,7 +38,7 @@ else: builtins.SERVER_MODE = None import config -from pgadmin import create_app +from pgadmin import create_app, socketio from pgadmin.utils import u_encode, fs_encoding, file_quote from pgadmin.utils.constants import INTERNAL # Get the config database schema version. We store this in pgadmin.model @@ -97,6 +98,8 @@ if not os.path.isfile(config.SQLITE_PATH): ########################################################################## app = create_app() app.debug = False +app.config['sessions'] = dict() + if config.SERVER_MODE: app.wsgi_app = ReverseProxied(app.wsgi_app) @@ -206,17 +209,16 @@ def main(): else: # Can use cheroot instead of flask dev server when not in debug # 10 is default thread count in CherootServer - num_threads = 10 if config.THREADED_MODE else 1 - prod_server = CherootServer( - (config.DEFAULT_SERVER, config.EFFECTIVE_SERVER_PORT), - wsgi_app=app, - numthreads=num_threads, - server_name=config.APP_NAME) + # num_threads = 10 if config.THREADED_MODE else 1 try: - print("Using production server...") - prod_server.start() + socketio.run( + app, + host=config.DEFAULT_SERVER, + port=config.EFFECTIVE_SERVER_PORT, + ) except KeyboardInterrupt: - prod_server.stop() + print("CLOSE SERVER") + socketio.stop() except IOError: app.logger.error("Error starting the app server: %s", sys.exc_info()) diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index a7333537..a641ea66 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -19,6 +19,7 @@ from collections import defaultdict from importlib import import_module from flask import Flask, abort, request, current_app, session, url_for +from flask_socketio import SocketIO from werkzeug.exceptions import HTTPException from flask_babelex import Babel, gettext from flask_babelex import gettext as _ @@ -52,10 +53,15 @@ import mimetypes mimetypes.add_type('application/javascript', '.js') mimetypes.add_type('text/css', '.css') + winreg = None if os.name == 'nt': import winreg +socketio = SocketIO(manage_session=False, async_mode='eventlet', + logger=True, engineio_logger=True, debug=False, + ping_interval=25, ping_timeout=120) + class PgAdmin(Flask): def __init__(self, *args, **kwargs): @@ -811,4 +817,5 @@ def create_app(app_name=None): ########################################################################## # All done! ########################################################################## + socketio.init_app(app) return app diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py index 235db027..b093e3a4 100644 --- a/web/pgadmin/browser/register_browser_preferences.py +++ b/web/pgadmin/browser/register_browser_preferences.py @@ -10,6 +10,7 @@ from flask_babelex import gettext from pgadmin.utils.constants import PREF_LABEL_DISPLAY,\ PREF_LABEL_KEYBOARD_SHORTCUTS, PREF_LABEL_TABS_SETTINGS, \ PREF_LABEL_OPTIONS +from flask_security import current_user import config LOCK_LAYOUT_LEVEL = { @@ -511,10 +512,12 @@ def register_browser_preferences(self): options=[{'label': gettext('Query Tool'), 'value': 'qt'}, {'label': gettext('Debugger'), 'value': 'debugger'}, {'label': gettext('Schema Diff'), 'value': 'schema_diff'}, - {'label': gettext('ERD Tool'), 'value': 'erd_tool'}], - help_str=gettext('Select Query Tool, Debugger, or Schema Diff from ' - 'the drop-down to set open in new browser tab for ' - 'that particular module.'), + {'label': gettext('ERD Tool'), 'value': 'erd_tool'}, + {'label': gettext('PSQL Tool'), 'value': 'psql_tool'}], + help_str=gettext('Select Query Tool, Debugger, Schema Diff, ERD Tool ' + 'or PSQL Tool from the drop-down to set ' + 'open in new browser tab for that particular module.' + ), select2={ 'multiple': True, 'allowClear': False, 'tags': True, 'first_empty': False, diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js index 01ab89c5..c4784d51 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js @@ -91,6 +91,11 @@ define('pgadmin.node.database', [ name: 'generate_erd', node: 'database', module: this, applies: ['object', 'context'], callback: 'generate_erd', category: 'erd', priority: 5, label: gettext('Generate ERD (Beta)'), + },{ + name: 'psql_tool', node: 'database', module: this, + applies: ['object', 'context'], callback: 'db_psql_tool', + category: 'psql_tool', priority: 5, label: gettext('PSQL Tool (Beta)'), + enable: 'is_psql_enabled' }]); _.bindAll(this, 'connection_lost'); @@ -122,6 +127,9 @@ define('pgadmin.node.database', [ is_connected: function(node) { return (node && node.connected == true && node.canDisconn == true); }, + is_psql_enabled: function(node) { + return (node && node.connected == true) && pgAdmin['enable_psql']; + }, is_conn_allow: function(node) { return (node && node.allowConn == true); }, @@ -266,6 +274,15 @@ define('pgadmin.node.database', [ pgBrowser.erd.showErdTool(d, i, true); }, + /* Open psql tool for db*/ + db_psql_tool: function(args) { + var input = args || {}, + t = pgBrowser.tree, + i = input.item || t.selected(), + d = i && i.length == 1 ? t.itemData(i) : undefined; + pgBrowser.psql.psql_tool(d, i, true); + }, + /* Connect the database (if not connected), before opening this node */ beforeopen: function(item, data) { if(!data || data._type != 'database' || data.label == 'template0') { diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js index b21cba43..1a7a0cef 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js @@ -102,6 +102,12 @@ define('pgadmin.node.server', [ data_disabled: gettext('Database is already disconnected.'), }, },{ + name: 'server_psql', node: 'server', module: this, + applies: ['object', 'context'], callback: 'server_psql_tool', + category: 'psql_tool', priority: 5, label: gettext('PSQL Tool (Beta)'), + enable : 'is_psql_enabled', + }, + { name: 'reload_configuration', node: 'server', module: this, applies: ['tools', 'context'], callback: 'reload_configuration', category: 'reload', priority: 6, label: gettext('Reload Configuration'), @@ -183,6 +189,9 @@ define('pgadmin.node.server', [ is_connected: function(node) { return (node && node.connected == true); }, + is_psql_enabled: function(node) { + return (node && node.connected == true) && pgAdmin['enable_psql']; + }, enable_reload_config: function(node) { // Must be connected & is Super user if (node && node._type == 'server' && @@ -728,6 +737,14 @@ define('pgadmin.node.server', [ return false; }, + /* Open psql tool for server*/ + server_psql_tool: function(args) { + var input = args || {}, + t = pgBrowser.tree, + i = input.item || t.selected(), + d = i && i.length == 1 ? t.itemData(i) : undefined; + pgBrowser.psql.psql_tool(d, i, true); + } }, model: pgAdmin.Browser.Node.Model.extend({ defaults: { diff --git a/web/pgadmin/browser/static/js/panel.js b/web/pgadmin/browser/static/js/panel.js index c9e64132..3068aabe 100644 --- a/web/pgadmin/browser/static/js/panel.js +++ b/web/pgadmin/browser/static/js/panel.js @@ -122,10 +122,30 @@ define( myPanel.on(ev, that.handleVisibility.bind(myPanel, ev)); }); } + + // Listen on detach panel event + myPanel.on(wcDocker.EVENT.DETACHED, function(obj) { + that.setCodeMirrorHeight(obj); + }); }, }); } }, + setCodeMirrorHeight: function() { + // Fix for mac os code-mirror showing black screen. + var txtArea = $('.pg-panel-content .sql_textarea > textarea').first(); + txtArea.css('z-index', -1); + var $tabContent = $('.pg-panel-content > .sql_textarea').first(); + var $sqlPane = $tabContent.find('.CodeMirror > div > textarea'); + for(let i=0; i<$sqlPane.length; i++) {$($sqlPane[i]).css('z-index', -1);} + + $tabContent = $('.pg-panel-content > .sql_textarea').first(); + $sqlPane = $tabContent.find('.pg-panel-content'); + $sqlPane.find('.CodeMirror').css( + 'cssText', + 'height: ' + ($tabContent.height()) + 'px !important;' + ); + }, eventFunc: function(eventName) { var name = $(this).data('pgAdminName'); diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js index 8597df48..b0a317a5 100644 --- a/web/pgadmin/browser/templates/browser/js/utils.js +++ b/web/pgadmin/browser/templates/browser/js/utils.js @@ -52,6 +52,10 @@ define('pgadmin.browser.utils', pgAdmin['user_inactivity_timeout'] = {{ current_app.config.get('USER_INACTIVITY_TIMEOUT') }}; pgAdmin['override_user_inactivity_timeout'] = '{{ current_app.config.get('OVERRIDE_USER_INACTIVITY_TIMEOUT') }}' == 'True'; + /* GET PSQL Tool related config */ + pgAdmin['enable_psql'] = '{{ current_app.config.get('ENABLE_PSQL') }}' == 'True'; + pgAdmin['allow_psql_shell_commands'] = '{{ current_app.config.get('ALLOW_PSQL_SHELL_COMMANDS') }}' == 'True'; + // Define list of nodes on which Query tool option doesn't appears var unsupported_nodes = pgAdmin.unsupported_nodes = [ 'server_group', 'server', 'coll-tablespace', 'tablespace', diff --git a/web/pgadmin/static/bundle/browser.js b/web/pgadmin/static/bundle/browser.js index 14140a2f..0b3ad81b 100644 --- a/web/pgadmin/static/bundle/browser.js +++ b/web/pgadmin/static/bundle/browser.js @@ -11,6 +11,7 @@ define('bundled_browser',[ 'pgadmin.browser', 'sources/browser/index', 'top/tools/erd/static/js/index', + 'top/tools/psql/static/js/index', ], function(pgBrowser) { pgBrowser.init(); }); diff --git a/web/pgadmin/static/css/style.css b/web/pgadmin/static/css/style.css index c2a776c8..5b4a9f2d 100644 --- a/web/pgadmin/static/css/style.css +++ b/web/pgadmin/static/css/style.css @@ -21,3 +21,5 @@ @import '../vendor/backgrid/backgrid.css'; @import '../vendor/backgrid/backgrid-select-all.css'; + +@import 'node_modules/xterm/css/xterm.css'; diff --git a/web/pgadmin/tools/psql/__init__.py b/web/pgadmin/tools/psql/__init__.py new file mode 100644 index 00000000..27ae7e2a --- /dev/null +++ b/web/pgadmin/tools/psql/__init__.py @@ -0,0 +1,503 @@ +#!/usr/bin/env python3 +import os +import re +import select +import signal +import termios +import struct +import fcntl +import pty +import config +import eventlet.green.subprocess as subprocess +from flask import Response, session, url_for, request +from pgadmin.browser.utils import underscore_unescape +from flask import render_template, copy_current_request_context, \ + current_app as app +from flask_babelex import gettext +from pgadmin.utils import PgAdminModule +from flask_security import login_required, current_user +from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \ + ERROR_MSG_TRANS_ID_NOT_FOUND +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.model import Server +from pgadmin.utils import get_complete_file_path + +from ... import socketio as sio + +session_input = dict() +session_input_cursor = dict() +session_last_cmd = dict() +pdata = dict() +cdata = dict() + + +class PSQLModule(PgAdminModule): + """ + class PSQLModule(PgAdminModule) + A module class for PSQL derived from PgAdminModule. + """ + + LABEL = gettext("PSQL") + + def get_own_menuitems(self): + return {} + + def get_own_javascripts(self): + return [{ + 'name': 'pgadmin.psql', + 'path': url_for('psql.index') + "psql", + 'when': None + }] + + def get_panels(self): + return [] + + def get_exposed_url_endpoints(self): + """ + Returns: + list: URL endpoints for PSQL module + """ + return [ + 'psql.panel', + ] + + +blueprint = PSQLModule('psql', __name__, static_url_path='/static') + + +@blueprint.route("/psql.js") +@login_required +def script(): + """render the required javascript""" + return Response( + response=render_template("psql/js/psql.js", _=gettext), + status=200, + mimetype=MIMETYPE_APP_JS + ) + + +@blueprint.route('/panel/', + methods=["POST"], + endpoint="panel") +@login_required +def panel(trans_id): + """ + Return panel template for PSQL tools. + :param trans_id: + """ + params = { + 'trans_id': trans_id, + 'title': request.form['title'] + } + if request.args: + params.update({k: v for k, v in request.args.items()}) + + return render_template('editor_template.html', + sid=params['sid'], + db=params['db'], + server_type=params['server_type'], + is_enable=config.ENABLE_PSQL, + title=underscore_unescape(params['title'])) + + +def set_term_size(fd, row, col, xpix=0, ypix=0): + """ + Set the terminal size as per UI xterm size. + :param fd: + :param row: + :param col: + :param xpix: + :param ypix: + """ + term_size = struct.pack('HHHH', row, col, xpix, ypix) + fcntl.ioctl(fd, termios.TIOCSWINSZ, term_size) + + +@sio.on('connect', namespace='/pty') +def connect(): + """ + Connect to the server through socket. + :return: + :rtype: + """ + if config.ENABLE_PSQL: + sio.emit('connected', {'sid': request.sid}, namespace='/pty', + to=request.sid) + if request.sid in session_last_cmd: + session_last_cmd[request.sid]['is_new_connection'] = False + else: + session_last_cmd[request.sid] = {'cmd': '', 'arrow_up': False, + 'invalid_cmd': False, + 'is_new_connection': True} + else: + sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty', + to=request.sid) + + +@sio.on('start_process', namespace='/pty') +@login_required +def start_process(data): + """ + Start the pty terminal and execute psql command and emit results to user. + :param data: + :return: + """ + + @copy_current_request_context + def read_and_forward_pty_output(sid, data): + max_read_bytes = 1024 * 20 + # Create the pty terminal process, parent and fd are file descriptors + # for parent and child. + parent, fd = pty.openpty() + p = None + if parent is not None: + # Child process + p = subprocess.Popen(connection_data, + preexec_fn=os.setsid, + stdin=fd, + stdout=fd, + stderr=fd, + universal_newlines=True + ) + + app.config['sessions'][request.sid] = parent + pdata[request.sid] = p + cdata[request.sid] = fd + else: + app.config['sessions'][request.sid] = parent + cdata[request.sid] = fd + set_term_size(fd, 50, 50) + + while p and p.poll() is None: + if request.sid in app.config['sessions']: + # This code is added to make this unit testable. + if "is_test" not in data: + sio.sleep(0.01) + else: + data['count'] += 1 + if data['count'] == 5: + break + + timeout = 0 + # module provides access to platform-specific I/O + # monitoring functions + (data_ready, _, _) = select.select([parent, fd], [], [], + timeout) + + if parent in data_ready: + # Read the output from parent fd (terminal). + output = os.read(parent, max_read_bytes) + emit_output = True + + if sid in session_last_cmd and session_last_cmd[sid][ + 'arrow_up'] and not session_last_cmd[request.sid][ + 'arrow_left_right']: + session_last_cmd[sid]['cmd'] = output.decode() + session_input_cursor[request.sid] = len( + session_last_cmd[sid]['cmd']) + session_last_cmd[sid]['arrow_up'] = True + + if sid in session_last_cmd: + # If command is invalid then emit error to user. + if session_last_cmd[sid]['invalid_cmd']: + emit_output = False + sio.emit( + 'pty-output', + { + 'result': gettext( + "ERROR: Shell commands are disabled " + "in psql for security;\r\n"), + 'error': True + }, + namespace='/pty', room=sid) + # If command is valid then emit output to user. + if emit_output: + sio.emit('pty-output', + {'result': output.decode(), + 'error': False}, + namespace='/pty', room=sid) + else: + session_last_cmd[request.sid]['invalid_cmd'] = False + + # Check user is authenticated and PSQL is enabled in config. + if current_user.is_authenticated and config.ENABLE_PSQL: + connection_data = [] + try: + db = '' + if data['db']: + db = data['db'] + # driver = get_driver(PG_DEFAULT_DRIVER) + # manager = driver.connection_manager(int(data['sid'])) + # conn = manager.connection() + conn, manager = _get_connection(int(data['sid']), data) + psql_utility = manager.utility('sql') + print("psql_utility: ", psql_utility) + connection_data = get_connection_str(psql_utility, conn, db, + manager) + except Exception as e: + # If any error raised during the start the PSQL emit error to UI. + sio.emit( + 'conn_error', + { + 'error': 'Error while running psql command: {0}'.format(e), + }, namespace='/pty', room=request.sid) + + try: + sio.start_background_task(read_and_forward_pty_output, + request.sid, data) + except Exception as e: + print(e) + else: + # Show error if user is not authenticated. + sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty', + to=request.sid) + + +def _get_connection(sid, data): + """ + Get the connection object of ERD. + :param sid: + :param did: + :param trans_id: + :return: + """ + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) + try: + conn = manager.connection() + if 'pwd' in data: + kwargs = {'password': data['pwd'], "user": data['user']} + status, msg = conn.connect(**kwargs) + else: + status, msg = conn.connect() + if not status: + app.logger.error(msg) + # raise ConnectionLost(sid, conn.db, trans_id) + + return conn, manager + except Exception as e: + app.logger.error(e) + raise + + +def get_connection_str(psql_utility, conn, db, manager): + """ + Get connection string(through connection dsn) + :param psql_utility: PostgreSQL binary path. + :param conn: Connection data + :param db: database name to connect specific db. + :return: connection attribute list for PSQL connection. + """ + + conn_attr = conn.conn.dsn + if 'password=xxx' in conn_attr: + conn_attr = conn_attr.replace('password=xxx', '') + + if db != '': + conn_attr = conn_attr.replace('dbname=postgres', + 'dbname={0}'.format(db)) + # Add application name to new created psql terminal instance + # through pgAdmin 4. + conn_attr = "{0} {1}".format(conn_attr, + " application_name='pgAdmin4: psql'") + + conn_attr_list = list() + conn_attr_list.append(psql_utility) + conn_attr_list.append(conn_attr) + return conn_attr_list + + +@sio.on('socket_input', namespace='/pty') +def socket_input(data): + """ + This get the user input through socket. + :param data: User input from socket. + """ + try: + # Check PSQL enabled setting from config. + enable_psql = True if config.ENABLE_PSQL else False + + if request.sid in app.config['sessions']: + if data['key_name'] == 'Enter' and enable_psql: + # If user get previous executed command from history then set + # current command as previous executed command. + if session_last_cmd[request.sid]['cmd'] \ + and session_last_cmd[request.sid]['arrow_up']: + user_input = str( + session_last_cmd[request.sid]['cmd']).strip() + session_last_cmd[request.sid]['arrow_up'] = False + session_last_cmd[request.sid]['cmd'] = '' + else: + user_input = str(session_input[request.sid]).strip() + session_input_cursor[request.sid] = 0 + + # If ALLOW_PSQL_SHELL_COMMANDS is False then user can't execute + # \! meta command to run shell commands through PSQL terminal. + # Check before executing the user entered command does not + # contains \! in input. + is_new_connection = session_last_cmd[request.sid][ + 'is_new_connection'] + if user_input.startswith('\\!') and \ + re.match("^\\\!$", user_input) and \ + len(user_input) == 2 and \ + not config.ALLOW_PSQL_SHELL_COMMANDS \ + and not is_new_connection: + + session_last_cmd[request.sid]['invalid_cmd'] = True + + for i in range(len(session_input[request.sid])): + os.write(app.config['sessions'][request.sid], + '\b \b'.encode()) + + os.write(app.config['sessions'][request.sid], + '\n'.encode()) + session_input[request.sid] = '' + elif re.search("\\\!", user_input) and \ + not config.ALLOW_PSQL_SHELL_COMMANDS and \ + not session_last_cmd[request.sid]['is_new_connection']: + stop_execution = True + # Check \! is passed as string or not. + double_quote_strs = re.findall('"([^"]*)"', user_input) + + if double_quote_strs: + for sub_str in double_quote_strs: + if re.search("\\\!", sub_str): + stop_execution = False + # break + + if stop_execution: + session_last_cmd[request.sid]['invalid_cmd'] = True + # Remove already added command from terminal. + for i in range(len(user_input)): + os.write(app.config['sessions'][request.sid], + '\b \b'.encode()) + # Add Enter event to execute the command. + os.write(app.config['sessions'][request.sid], + '\n'.encode()) + else: + session_last_cmd[request.sid]['invalid_cmd'] = False + os.write(app.config['sessions'][request.sid], + '\n'.encode()) + elif user_input == '\q': + # If user enter \q to terminate the PSQL, emit the msg to + # notify user connection is terminated. + os.write(app.config['sessions'][request.sid], + '\n'.encode()) + sio.emit('pty-output', + { + 'result': gettext( + 'Connection terminated, TO create new ' + 'connection please open another psql' + ' tool.'), + 'error': True}, + namespace='/pty', room=request.sid) + else: + os.write(app.config['sessions'][request.sid], + data['input'].encode()) + session_input[request.sid] = '' + session_last_cmd[request.sid]['is_new_connection'] = False + else: + if data['key_name'] == 'ArrowLeft': + session_last_cmd[request.sid]['arrow_left_right'] = True + if session_input_cursor[request.sid] > 0: + session_input_cursor[request.sid] -= 1 + + elif data['key_name'] == 'ArrowRight': + session_last_cmd[request.sid]['arrow_left_right'] = True + if session_input_cursor[request.sid] < len( + session_input[request.sid]): + session_input_cursor[request.sid] += 1 + + elif data['key_name'] == 'ArrowUp': + session_last_cmd[request.sid]['arrow_up'] = True + session_last_cmd[request.sid]['arrow_left_right'] = False + session_input[request.sid] = session_last_cmd[request.sid][ + 'cmd'] + session_input_cursor[request.sid] = len( + session_last_cmd[request.sid]['cmd']) + + elif request.sid in session_input and data[ + 'key_name'] == 'Backspace' and ( + len(session_input[request.sid]) or len( + session_last_cmd[request.sid])): + + session_last_cmd[request.sid]['arrow_left_right'] = True + + if session_last_cmd[request.sid]['cmd']: + session_input[request.sid] = \ + session_last_cmd[request.sid]['cmd'] + + user_input = list(session_input[request.sid]) + + if session_input_cursor[request.sid] == 1: + index = 0 + session_input_cursor[request.sid] -= 1 + else: + if session_input_cursor[request.sid] > 0: + index = (session_input_cursor[request.sid]) - 1 + session_input_cursor[request.sid] -= 1 + else: + index = session_input_cursor[request.sid] + session_input_cursor[request.sid] = 0 + + if len(user_input): + del user_input[index] + session_input[request.sid] = "".join(user_input) + + if len(session_input[request.sid]) == 0: + session_input_cursor[request.sid] = 0 + session_last_cmd[request.sid]['cmd'] = '' + elif request.sid in session_input: + if session_last_cmd[request.sid]['cmd'] and session_input[ + request.sid] == '': + session_input[request.sid] = \ + session_last_cmd[request.sid]['cmd'] + session_input_cursor[request.sid] = len( + session_input[request.sid]) + else: + session_last_cmd[request.sid]['arrow_up'] = False + session_last_cmd[request.sid]['cmd'] = '' + user_input = list(session_input[request.sid]) + user_input.insert(session_input_cursor[request.sid], + data['input']) + session_input[request.sid] = ''.join(user_input) + session_input_cursor[request.sid] += 1 + session_last_cmd[request.sid]['arrow_left_right'] = False + else: + session_input_cursor[request.sid] = 0 + session_input[request.sid] = data['input'] + session_input_cursor[request.sid] += 1 + + # Write user input to terminal parent fd. + os.write(app.config['sessions'][request.sid], + data['input'].encode()) + except Exception as e: + # Delete socket id from sessions. + del app.config['sessions'][request.sid] + + +@sio.on('resize', namespace='/pty') +def resize(data): + """ + Resize the pty terminal as per the UI terminal. + :param data: UI terminal rows and cols data + """ + if request.sid in app.config['sessions']: + set_term_size(app.config['sessions'][request.sid], data['rows'], + data['cols']) + + +@sio.on('disconnect', namespace='/pty') +def disconnect(): + """ + Disconnect the socket and terminate the process + """ + if request.sid in pdata: + # On disconnect socket manually exit the psql terminal and close the + # parend and child fd then kill the subprocess. + os.write(app.config['sessions'][request.sid], '\q\n'.encode()) + sio.sleep(1) + os.close(app.config['sessions'][request.sid]) + os.close(cdata[request.sid]) + del app.config['sessions'][request.sid] + os.kill(pdata[request.sid].pid, signal.SIGSTOP) diff --git a/web/pgadmin/tools/psql/static/js/index.js b/web/pgadmin/tools/psql/static/js/index.js new file mode 100644 index 00000000..4d88ba04 --- /dev/null +++ b/web/pgadmin/tools/psql/static/js/index.js @@ -0,0 +1,23 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import url_for from 'sources/url_for'; +import $ from 'jquery'; +import _ from 'underscore'; +import pgAdmin from 'sources/pgadmin'; +import pgBrowser from 'top/browser/static/js/browser'; +import * as csrfToken from 'sources/csrf'; +import {initialize} from './psql_module'; + +let pgBrowserOut = initialize(gettext, url_for, $, _, pgAdmin, csrfToken, pgBrowser); + +module.exports = { + pgBrowser: pgBrowserOut, +}; diff --git a/web/pgadmin/tools/psql/static/js/psql_module.js b/web/pgadmin/tools/psql/static/js/psql_module.js new file mode 100644 index 00000000..a9bdd90b --- /dev/null +++ b/web/pgadmin/tools/psql/static/js/psql_module.js @@ -0,0 +1,329 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +import { Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; +import { WebLinksAddon } from 'xterm-addon-web-links'; +import { SearchAddon } from 'xterm-addon-search'; +import { io } from 'socketio'; +import Alertify from 'pgadmin.alertifyjs'; +import clipboard from 'sources/selection/clipboard'; + +import 'wcdocker'; +import {getRandomInt} from 'sources/utils'; +import {getTreeNodeHierarchyFromIdentifier} from 'sources/tree/pgadmin_tree_node'; + + +export function setPanelTitle(psqlToolPanel, panelTitle) { + psqlToolPanel.title(''+panelTitle+''); +} + +var wcDocker = window.wcDocker; + +export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, Browser) { + var pgBrowser = Browser; + var terminal = Terminal; + var parentData = null; + /* Return back, this has been called more than once */ + if (pgBrowser.psql) + return pgBrowser.psql; + + // Create an Object Restore of pgBrowser class + pgBrowser.psql = { + init: function() { + this.initialized = true; + + csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); + // Define the nodes on which the menus to be appear + var menus = [{ + name: 'psql', + module: this, + applies: ['tools'], + callback: 'psql_tool', + priority: 1, + label: gettext('PSQL Tool (Beta)'), + enable: this.psqlToolEnabled, + }]; + + this.enable_psql_tool = pgAdmin['enable_psql']; + if(pgAdmin['enable_psql']) { + pgBrowser.add_menus(menus); + } + + // Creating a new pgBrowser frame to show the data. + var psqlFrameType = new pgBrowser.Frame({ + name: 'frm_psqltool', + showTitle: true, + isCloseable: true, + isPrivate: true, + url: 'about:blank', + }); + + let self = this; + /* Cache may take time to load for the first time + * Keep trying till available + */ + let cacheIntervalId = setInterval(function() { + if(pgBrowser.preference_version() > 0) { + self.preferences = pgBrowser.get_preferences_for_module('psql'); + clearInterval(cacheIntervalId); + } + },0); + + pgBrowser.onPreferencesChange('psql', function() { + self.preferences = pgBrowser.get_preferences_for_module('psql'); + }); + + // Load the newly created frame + psqlFrameType.load(pgBrowser.docker); + return this; + }, + + psqlToolEnabled: function(obj) { + //Same as query tool + var isEnabled = (() => { + if (!_.isUndefined(obj) && !_.isNull(obj)) { + if ((this.enable_psql_tool == true) && ((obj._type == 'server' && obj.connected == true )|| obj._type == 'database')) { + return true; + } else { + return false; + } + } else { + return false; + } + })(); + return isEnabled; + }, + retrieveAncestorOfTypeServer: function(item) { + let serverInformation = null; + // let aciTreeItem = item || pgBrowser.treeMenu.selected(); + let treeNode = pgBrowser.treeMenu.findNodeByDomElement(item); + + if (treeNode) { + let nodeData; + let databaseNode = treeNode.ancestorNode( + (node) => { + nodeData = node.getData(); + return (nodeData._type === 'database'); + } + ); + let isServerNode = (node) => { + nodeData = node.getData(); + return nodeData._type === 'server'; + }; + + if (databaseNode !== null) { + if (nodeData._label.indexOf('=') >= 0) { + this.alertify.alert( + gettext(this.errorAlertTitle), + gettext( + 'Databases with = symbols in the name cannot be backed up or restored using this utility.' + ) + ); + } else { + if (databaseNode.anyParent(isServerNode)) + serverInformation = nodeData; + } + } else { + if (treeNode.anyFamilyMember(isServerNode)) + serverInformation = nodeData; + } + } + + if (serverInformation === null) { + this.alertify.alert( + gettext(this.errorAlertTitle), + gettext('Please select server or child node from the browser tree.') + ); + } + return serverInformation; + }, + psql_tool: function(data, aciTreeIdentifier, gen=false) { + const module = 'paths'; + let preference_name = 'pg_bin_dir'; + let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.'); + const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeIdentifier); + + // const node1 = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier); + if ((serverInformation.type && serverInformation.type === 'ppas') || + serverInformation.server_type === 'ppas') { + preference_name = 'ppas_bin_dir'; + msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'); + } + const preference = pgBrowser.get_preference(module, preference_name); + + if (preference) { + if (!preference.value) { + Alertify.alert(gettext('Configuration required'), msg); + return false; + } + } else { + Alertify.alert( + gettext(this.errorAlertTitle), + gettext('Failed to load preference %s of module %s', preference_name, module) + ); + return false; + } + const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier); + if (node === undefined || !node.getData()) { + Alertify.alert( + gettext('PSQL Error'), + gettext('No object selected.') + ); + return; + } + + parentData = getTreeNodeHierarchyFromIdentifier.call( + pgBrowser, + aciTreeIdentifier + ); + + if(_.isUndefined(parentData.server)) { + Alertify.alert( + gettext('PSQL Error'), + gettext('Please select a server/database object.') + ); + return; + } + + const transId = getRandomInt(1, 9999999); + + var panelTitle = ''; + if (parentData.database) { + panelTitle = parentData.database.label + '/' + parentData.server.user_name + '@' + parentData.server.label; + } else { + panelTitle = parentData.server.user_name + '@' + parentData.server.label; + } + const [panelUrl, panelCloseUrl] = this.getPanelUrls(transId, panelTitle, parentData, gen); + + let psqlToolForm = ` +

+ + +
+ + `; + + var open_new_tab = pgBrowser.get_preferences_for_module('browser').new_browser_tab_open; + if (open_new_tab && open_new_tab.includes('psql_tool')) { + var newWin = window.open('', '_blank'); + newWin.document.write(psqlToolForm); + newWin.document.title = panelTitle; + } else { + /* On successfully initialization find the properties panel, + * create new panel and add it to the dashboard panel. + */ + var propertiesPanel = pgBrowser.docker.findPanels('properties'); + var psqlToolPanel = pgBrowser.docker.addPanel('frm_psqltool', wcDocker.DOCK.STACKED, propertiesPanel[0]); + + // Set panel title and icon + setPanelTitle(psqlToolPanel, panelTitle); + psqlToolPanel.icon('fas fa-terminal'); + psqlToolPanel.focus(); + + // Listen on the panel closed event. + /*psqlToolPanel.on(wcDocker.EVENT.CLOSED, function() { + $.ajax({ + url: panelCloseUrl, + method: 'DELETE', + }); + });*/ + + var openPSQLToolURL = function(j) { + // add spinner element + let $spinner_el = + $(`
+
+
+
+
+
+
`).appendTo($(j).data('embeddedFrame').$container); + + let init_poller_id = setInterval(function() { + var frameInitialized = $(j).data('frameInitialized'); + if (frameInitialized) { + clearInterval(init_poller_id); + var frame = $(j).data('embeddedFrame'); + if (frame) { + frame.onLoaded(()=>{ + $spinner_el.remove(); + }); + frame.openHTML(psqlToolForm); + } + } + }, 100); + }; + + openPSQLToolURL(psqlToolPanel); + + } + + // var url_params = { + // 'sid': parentData.server._id, + // }; + // var baseUrl = url_for('psql.initialize', url_params); + // + // window.open(baseUrl, '_blank'); + }, + getPanelUrls: function(transId, panelTitle, parentData) { + let openUrl = url_for('psql.panel', { + trans_id: transId, + }); + + openUrl += `?sgid=${parentData.server_group._id}` + +`&sid=${parentData.server._id}` + +`&server_type=${parentData.server.server_type}`; + + if(parentData.database && parentData.database._id) { + openUrl += `&db=${parentData.database._label}`; + } else { + openUrl += `&db=${''}`; + } + + let closeUrl = url_for('psql.close', { + trans_id: transId, + // sgid: parentData.server_group._id, + // sid: parentData.server._id, + // did: parentData.database._id, + }); + return [openUrl, closeUrl]; + }, + psql_terminal: function() { + return new terminal({ + cursorBlink: true, + macOptionIsMeta: true, + scrollback: 10000, + }); + }, + psql_fit_screen: function() { + return new FitAddon(); + }, + psql_web_link: function() { + return new WebLinksAddon(); + }, + psql_search: function() { + return new SearchAddon(); + }, + psql_socket: function() { + return io('/pty', {pingTimeout: 120000, pingInterval: 25000}); + }, + get_parent_db: function(){ + return parentData.server; + }, + _clipboard: function() { + return clipboard; + } + }; + + return pgBrowser.psql; +} + diff --git a/web/pgadmin/tools/psql/templates/editor_template.html b/web/pgadmin/tools/psql/templates/editor_template.html new file mode 100644 index 00000000..e5f50a29 --- /dev/null +++ b/web/pgadmin/tools/psql/templates/editor_template.html @@ -0,0 +1,137 @@ +{% extends "base.html" %} +{% block title %}{{title}}{% endblock %} + +{% block css_link %} + +{% endblock %} +{% block body %} + +
+{% endblock %} + + +{% block init_script %} +require( + ['sources/generated/psql_tool'], + function(pgBrowser) { + const term = self.pgAdmin.Browser.psql.psql_terminal(); + + const fitAddon = self.pgAdmin.Browser.psql.psql_fit_screen(); + term.loadAddon(fitAddon); + let clipboard = self.pgAdmin.Browser.psql._clipboard(); + + const webLinksAddon = self.pgAdmin.Browser.psql.psql_web_link(); + term.loadAddon(webLinksAddon); + + const searchAddon = self.pgAdmin.Browser.psql.psql_search(); + term.loadAddon(searchAddon); + + term.open(document.getElementById('psql-terminal')); + fitAddon.fit() + term.resize(15, 50) + fitAddon.fit() + let selected_text = ''; + let user_input = ''; + let is_pwd = true; + let cursor_position = 0; + + term.attachCustomKeyEventHandler(e => { + e.stopPropagation(); + if(e.type=='keydown' && e.metaKey &&(e.key == 'v' || e.key == 'V')) { + if(selected_text != '') { + if (selected_text.length > 0) { + socket.emit("socket_input", {"input": selected_text, 'key_name': e.code}); + selected_text = ''; + } + } else { + navigator.clipboard.readText().then( clipText => { + selected_text = clipText; + if (selected_text.length > 0) { + socket.emit("socket_input", {"input": selected_text, 'key_name': e.code}); + selected_text = ''; + } + }); + } + + }else if(e.type=='keydown' && e.metaKey && (e.key == 'c' || e.key == 'C')) { + if (term.hasSelection()) { + selected_text = term.getSelection(); + } else { + selected_text = clipboard.readText(); + } + } + + + + + + return true; + }); + + term.onKey(function (ev) { + if (pgAdmin['allow_psql_shell_commands']) { + socket.emit("socket_input", {"input": ev.key, 'key_name': ev.domEvent.code}); + } else { + console.log("socket_input" + ev.key); + socket.emit("socket_input", {"input": ev.key, 'key_name': ev.domEvent.code}); + } + }); + + const socket = self.pgAdmin.Browser.psql.psql_socket(); + + socket.on("pty-output", function(data){ + if(data.error) { + term.write('\r\n'); + } + term.write(data.result); + if(data.error) { + term.write('\r\n'); + } + }) + + socket.on("connect", () => { + if('{{is_enable}}' == 'True'){ + socket.emit('start_process', {"sid": {{sid}}, "db": '{{db}}', 'stype': '{{server_type}}' }); + } + fitToscreen(); + }); + + socket.on("conn_error", (response) => { + term.write(response.error); + fitToscreen(); + }); + + socket.on("conn_not_allow", () => { + term.write('PSQL connection not allowed'); + fitToscreen(); + }); + + function fitToscreen(){ + fitAddon.fit() + socket.emit("resize", {"cols": term.cols, "rows": term.rows}) + } + + function debounce(func, wait_ms) { + let timeout + return function(...args) { + const context = this + clearTimeout(timeout) + timeout = setTimeout(() => func.apply(context, args), wait_ms) + } + } + + const wait_ms = 50;; + window.onresize = debounce(fitToscreen, wait_ms) + }); +{% endblock %} + + diff --git a/web/pgadmin/tools/psql/tests/__init__.py b/web/pgadmin/tools/psql/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/tools/psql/tests/psql_test_data.json b/web/pgadmin/tools/psql/tests/psql_test_data.json new file mode 100644 index 00000000..9b2cfaf5 --- /dev/null +++ b/web/pgadmin/tools/psql/tests/psql_test_data.json @@ -0,0 +1,174 @@ +{ + "psql_user_input": [ + { + "name": "Enter Select 1;", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "select 1;", + "mock_data": { + + }, + "expected_data": { + } + }, + { + "name": "Enter Backspace", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "select 1;", + "is_backspace": true, + "mock_data": { + + }, + "expected_data": { + } + },{ + "name": "Enter Backspace", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "select 1;", + "is_backspace": true, + "move_cursor_up": true, + "mock_data": { + + }, + "expected_data": { + } + }, + { + "name": "Enter ArrowUp", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "select 1;", + "is_arrowUp": true, + "mock_data": { + + }, + "expected_data": { + } + }, + { + "name": "Enter ArrowUp", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "select 1;", + "is_arrowUp": true, + "is_history": true, + "mock_data": { + + }, + "expected_data": { + } + }, + { + "name": "Enter ArrowLeft", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "select 1;", + "is_arrowLeft": true, + "mock_data": { + + }, + "expected_data": { + } + }, + { + "name": "Enter ArrowRight", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "select 1;", + "is_arrowRight": true, + "mock_data": { + + }, + "expected_data": { + } + },{ + "name": "Read previous executed command", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "select 1;", + "is_arrowRight": true, + "move_cursor_right": true, + "mock_data": { + + }, + "expected_data": { + } + }, + { + "name": "Meta command \\! not allowed", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "\\!", + "mock_data": { + + }, + "expected_data": { + } + }, + { + "name": "Meta command \\! with other cmd not allowed", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "\\! ls", + "mock_data": { + + }, + "expected_data": { + } + }, + { + "name": "Valid commands", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "select \"\\!\"", + "mock_data": { + + }, + "expected_data": { + } + }, + { + "name": "Exist psql terminal by using \\q", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "\\q", + "mock_data": { + + }, + "expected_data": { + } + } + ], + "resize_terminal": [ + { + "name": "Resize psql terminal as per UI.", + "is_positive_test": true, + "mocking_required": false, + "input_data": { + "cols": 141, + "rows": 39 + }, + "mock_data": { + + }, + "expected_data": { + } + } + ], + "backend_task": [ + { + "name": "Backend Task", + "is_positive_test": true, + "mocking_required": false, + "input_cmd": "Select 1;", + "is_backend_task": true, + "mock_data": { + "is_test": true + }, + "expected_data": { + } + } + ] +} diff --git a/web/pgadmin/tools/psql/tests/test_backend_task.py b/web/pgadmin/tools/psql/tests/test_backend_task.py new file mode 100644 index 00000000..99073c0a --- /dev/null +++ b/web/pgadmin/tools/psql/tests/test_backend_task.py @@ -0,0 +1,88 @@ +import uuid +import config +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from regression import parent_node_dict +from regression.test_setup import config_data +from pgadmin.utils import server_utils as server_utils +from pgAdmin4 import app +from . import utils as psql_utils +from .... import socketio + + +class PSQLBackend(BaseTestGenerator): + scenarios = utils.generate_scenarios('backend_task', + psql_utils.test_cases) + + def setUp(self): + self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8]) + database_info = parent_node_dict["database"][-1] + self.did = database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data["server_group"] + config.ENABLE_PSQL = True + self.server_con = server_utils.connect_server(self, self.sid) + + def runTest(self): + + # Fetch flask client to access current user and other cookies. + flask_client = app.test_client() + flask_client.get('/session') + self.test_client = socketio.test_client(app, namespace='/pty', + flask_test_client=flask_client) + self.assertTrue(self.test_client.is_connected('/pty')) + received = self.test_client.get_received('/pty') + + assert received[0]['name'] == 'connected' + assert received[0]['args'][0]['sid'] != '' + + data = { + 'sid': self.sid, + 'db': 'postgres', + 'pwd': self.server['db_password'], + 'user': self.server['username'], + 'is_test': True, + 'count': 0 + } + + self.test_client.emit('start_process', data, namespace='/pty') + self.test_client.get_received('/pty') + + for p in self.server['db_password']: + input_data = { + 'input': p, + 'key_name': 'Key{0}'.format(p) + } + self.test_client.emit('socket_input', input_data, namespace='/pty') + self.test_client.get_received('/pty') + + input_data = { + 'input': '\\n', + 'key_name': 'Enter' + } + self.test_client.emit('socket_input', input_data, namespace='/pty') + self.test_client.get_received('/pty') + + for ip in self.input_cmd: + input_data = { + 'input': ip, + 'key_name': 'Key{0}'.format(ip) + } + self.test_client.emit('socket_input', input_data, namespace='/pty') + self.test_client.get_received('/pty') + + input_data = { + 'input': '\\n', + 'key_name': 'Enter' + } + self.test_client.emit('socket_input', input_data, namespace='/pty') + self.test_client.get_received('/pty') + self.test_client.disconnect(namespace='/pty') + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/tools/psql/tests/test_panel.py b/web/pgadmin/tools/psql/tests/test_panel.py new file mode 100644 index 00000000..83480681 --- /dev/null +++ b/web/pgadmin/tools/psql/tests/test_panel.py @@ -0,0 +1,35 @@ +import uuid +import random +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from regression import parent_node_dict +from regression.test_setup import config_data + + +class PSQLPanel(BaseTestGenerator): + + def setUp(self): + self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8]) + self.sid = parent_node_dict["server"][-1]["server_id"] + self.did = utils.create_database(self.server, self.db_name) + self.sgid = config_data["server_group"] + + def runTest(self): + trans_id = random.randint(1, 9999999) + url = '/psql/panel/{trans_id}?sgid={sgid}&sid={sid}&server_type=pg' \ + '&db={db_name}'.\ + format(trans_id=trans_id, sgid=self.sgid, sid=self.sid, + db_name=self.db_name) + + response = self.tester.post( + url, data={"title": "panel_title"}, + content_type="application/x-www-form-urlencoded") + self.assertEqual(response.status_code, 200) + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/tools/psql/tests/test_psql_disabled.py b/web/pgadmin/tools/psql/tests/test_psql_disabled.py new file mode 100644 index 00000000..32eaa623 --- /dev/null +++ b/web/pgadmin/tools/psql/tests/test_psql_disabled.py @@ -0,0 +1,36 @@ +import uuid +import config +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from regression import parent_node_dict +from regression.test_setup import config_data +# from flask import current_app as app + +from pgAdmin4 import app +from .... import socketio + + +class PSQLSocketDisabled(BaseTestGenerator): + def setUp(self): + self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8]) + self.sid = parent_node_dict["server"][-1]["server_id"] + self.did = utils.create_database(self.server, self.db_name) + self.sgid = config_data["server_group"] + config.ENABLE_PSQL = False + + def runTest(self): + self.test_client = socketio.test_client(app, namespace='/pty') + self.assertTrue(self.test_client.is_connected('/pty')) + received = self.test_client.get_received('/pty') + print("received:", received) + assert received[0]['name'] == 'conn_not_allow' + self.test_client.disconnect(namespace='/pty') + self.assertFalse(self.test_client.is_connected('/pty')) + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/tools/psql/tests/test_psql_input.py b/web/pgadmin/tools/psql/tests/test_psql_input.py new file mode 100644 index 00000000..8fe0b47a --- /dev/null +++ b/web/pgadmin/tools/psql/tests/test_psql_input.py @@ -0,0 +1,159 @@ +import uuid +import config +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from regression import parent_node_dict +from regression.test_setup import config_data +from pgadmin.utils import server_utils as server_utils +from pgAdmin4 import app +from . import utils as psql_utils +from .... import socketio + + +class PSQLInput(BaseTestGenerator): + scenarios = utils.generate_scenarios('psql_user_input', + psql_utils.test_cases) + + def setUp(self): + self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8]) + database_info = parent_node_dict["database"][-1] + self.did = database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data["server_group"] + config.ENABLE_PSQL = True + self.server_con = server_utils.connect_server(self, self.sid) + + def runTest(self): + + # Fetch flask client to access current user and other cookies. + flask_client = app.test_client() + flask_client.get('/session') + self.test_client = socketio.test_client(app, namespace='/pty', + flask_test_client=flask_client) + self.assertTrue(self.test_client.is_connected('/pty')) + received = self.test_client.get_received('/pty') + + assert received[0]['name'] == 'connected' + assert received[0]['args'][0]['sid'] != '' + + data = { + 'sid': self.sid, + 'db': 'postgres', + 'pwd': self.server['db_password'], + 'user': self.server['username'] + } + + self.test_client.emit('start_process', data, namespace='/pty') + received = self.test_client.get_received('/pty') + print("received values: ", received) + + for p in self.server['db_password']: + input_data = { + 'input': p, + 'key_name': 'Key{0}'.format(p) + } + self.test_client.emit('socket_input', input_data, namespace='/pty') + received = self.test_client.get_received('/pty') + print("user I/P:: ", received) + + input_data = { + 'input': '\\n', + 'key_name': 'Enter' + } + self.test_client.emit('socket_input', input_data, namespace='/pty') + received = self.test_client.get_received('/pty') + + for ip in self.input_cmd: + input_data = { + 'input': ip, + 'key_name': 'Key{0}'.format(ip) + } + self.test_client.emit('socket_input', input_data, namespace='/pty') + self.test_client.get_received('/pty') + + if hasattr(self, 'is_backspace') and self.is_backspace: + if hasattr(self, 'move_cursor_up') and self.move_cursor_up: + input_data = { + 'input': '', + 'key_name': 'ArrowUp' + } + self.test_client.emit('socket_input', input_data, + namespace='/pty') + + for ip in self.input_cmd: + input_data = { + 'input': ip, + 'key_name': 'Backspace' + } + self.test_client.emit('socket_input', input_data, + namespace='/pty') + self.test_client.get_received('/pty') + + if hasattr(self, 'is_arrowUp') and self.is_arrowUp: + if hasattr(self, 'is_history') and self.is_history: + for ip in self.input_cmd: + input_data = { + 'input': ip, + 'key_name': 'Key{0}'.format(ip) + } + self.test_client.emit('socket_input', input_data, + namespace='/pty') + self.test_client.get_received('/pty') + + input_data = { + 'input': '', + 'key_name': 'ArrowUp' + } + self.test_client.emit('socket_input', input_data, + namespace='/pty') + self.test_client.get_received('/pty') + + if hasattr(self, 'is_arrowLeft') and self.is_arrowLeft: + for ip in self.input_cmd: + input_data = { + 'input': ip, + 'key_name': 'ArrowLeft' + } + self.test_client.emit('socket_input', input_data, + namespace='/pty') + self.test_client.get_received('/pty') + + if hasattr(self, 'is_arrowRight') and self.is_arrowRight: + for ip in self.input_cmd: + input_data = { + 'input': ip, + 'key_name': 'ArrowRight' + } + self.test_client.emit('socket_input', input_data, + namespace='/pty') + self.test_client.get_received('/pty') + + if hasattr(self, 'move_cursor_right') and self.is_arrowRight: + for i in range(2): + input_data = { + 'input': '', + 'key_name': 'ArrowLeft' + } + self.test_client.emit('socket_input', input_data, + namespace='/pty') + input_data = { + 'input': '', + 'key_name': 'ArrowRight' + } + self.test_client.emit('socket_input', input_data, + namespace='/pty') + + input_data = { + 'input': '\\n', + 'key_name': 'Enter' + } + self.test_client.emit('socket_input', input_data, namespace='/pty') + received = self.test_client.get_received('/pty') + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/tools/psql/tests/test_resize_terminal.py b/web/pgadmin/tools/psql/tests/test_resize_terminal.py new file mode 100644 index 00000000..9ce7d004 --- /dev/null +++ b/web/pgadmin/tools/psql/tests/test_resize_terminal.py @@ -0,0 +1,60 @@ +import uuid +import config +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from regression import parent_node_dict +from regression.test_setup import config_data +from pgadmin.utils import server_utils as server_utils +from pgAdmin4 import app +from . import utils as psql_utils +from .... import socketio + + +class PSQLResizeTerminal(BaseTestGenerator): + scenarios = utils.generate_scenarios('resize_terminal', + psql_utils.test_cases) + + def setUp(self): + self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8]) + database_info = parent_node_dict["database"][-1] + self.did = database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data["server_group"] + config.ENABLE_PSQL = True + self.server_con = server_utils.connect_server(self, self.sid) + + def runTest(self): + + # Fetch flask client to access current user and other cookies. + flask_client = app.test_client() + flask_client.get('/session') + self.test_client = socketio.test_client(app, namespace='/pty', + flask_test_client=flask_client) + self.assertTrue(self.test_client.is_connected('/pty')) + received = self.test_client.get_received('/pty') + + assert received[0]['name'] == 'connected' + assert received[0]['args'][0]['sid'] != '' + + data = { + 'sid': self.sid, + 'db': 'postgres', + 'pwd': self.server['db_password'], + 'user': self.server['username'] + } + + self.test_client.emit('start_process', data, namespace='/pty') + received = self.test_client.get_received('/pty') + + self.test_client.emit('resize', self.input_data, namespace='/pty') + + # self.test_client.disconnect(namespace='/pty') + # self.assertFalse(self.test_client.is_connected('/pty')) + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/tools/psql/tests/test_socket_connect.py b/web/pgadmin/tools/psql/tests/test_socket_connect.py new file mode 100644 index 00000000..7b8031bd --- /dev/null +++ b/web/pgadmin/tools/psql/tests/test_socket_connect.py @@ -0,0 +1,36 @@ +import uuid +import config +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from regression import parent_node_dict +from regression.test_setup import config_data +# from flask import current_app as app +from pgAdmin4 import app +from .... import socketio + + +class PSQLSocketConnect(BaseTestGenerator): + def setUp(self): + self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8]) + self.sid = parent_node_dict["server"][-1]["server_id"] + self.did = utils.create_database(self.server, self.db_name) + self.sgid = config_data["server_group"] + config.ENABLE_PSQL = True + + def runTest(self): + self.test_client = socketio.test_client(app, namespace='/pty') + self.assertTrue(self.test_client.is_connected('/pty')) + received = self.test_client.get_received('/pty') + print("received:", received) + assert received[0]['name'] == 'connected' + assert received[0]['args'][0]['sid'] != '' + self.test_client.disconnect(namespace='/pty') + self.assertFalse(self.test_client.is_connected('/pty')) + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/tools/psql/tests/test_socket_disconnect.py b/web/pgadmin/tools/psql/tests/test_socket_disconnect.py new file mode 100644 index 00000000..cd2cf5ac --- /dev/null +++ b/web/pgadmin/tools/psql/tests/test_socket_disconnect.py @@ -0,0 +1,35 @@ +import uuid +import config +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from regression import parent_node_dict +from regression.test_setup import config_data +# from flask import current_app as app +from pgAdmin4 import app +from .... import socketio + + +class PSQLSocketDisconnect(BaseTestGenerator): + def setUp(self): + self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8]) + self.sid = parent_node_dict["server"][-1]["server_id"] + self.did = utils.create_database(self.server, self.db_name) + self.sgid = config_data["server_group"] + config.ENABLE_PSQL = True + + def runTest(self): + self.test_client = socketio.test_client(app, namespace='/pty') + self.assertTrue(self.test_client.is_connected('/pty')) + received = self.test_client.get_received('/pty') + print("received:", received) + assert received[0]['name'] == 'connected' + assert received[0]['args'][0]['sid'] != '' + self.test_client.disconnect(namespace='/pty') + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/tools/psql/tests/test_start_process.py b/web/pgadmin/tools/psql/tests/test_start_process.py new file mode 100644 index 00000000..055f7ce3 --- /dev/null +++ b/web/pgadmin/tools/psql/tests/test_start_process.py @@ -0,0 +1,63 @@ +import uuid +import config +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from regression import parent_node_dict +from regression.test_setup import config_data +from pgadmin.utils import server_utils as server_utils +from pgAdmin4 import app +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from .... import socketio + + +class PSQLStartProcess(BaseTestGenerator): + def setUp(self): + self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8]) + database_info = parent_node_dict["database"][-1] + self.did = database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data["server_group"] + config.ENABLE_PSQL = True + + self.server_con = server_utils.connect_server(self, self.sid) + print(self.server_con) + + def runTest(self): + # Fetch flask client to access current user and other cookies. + flask_client = app.test_client() + flask_client.get('/session') + self.test_client = socketio.test_client(app, namespace='/pty', + flask_test_client=flask_client) + self.assertTrue(self.test_client.is_connected('/pty')) + received = self.test_client.get_received('/pty') + + assert received[0]['name'] == 'connected' + assert received[0]['args'][0]['sid'] != '' + + import random + trans_id = random.randint(1, 9999999) + + data = { + 'sid': self.sid, + 'db': 'postgres', + 'pwd': self.server['db_password'], + 'user': self.server['username'] + } + + self.test_client.emit('start_process', data, namespace='/pty') + received = self.test_client.get_received('/pty') + print("received values: ", received) + + self.test_client.disconnect(namespace='/pty') + self.assertFalse(self.test_client.is_connected('/pty')) + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/tools/psql/tests/test_start_process_fail.py b/web/pgadmin/tools/psql/tests/test_start_process_fail.py new file mode 100644 index 00000000..895fc533 --- /dev/null +++ b/web/pgadmin/tools/psql/tests/test_start_process_fail.py @@ -0,0 +1,60 @@ +import uuid +import config +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from regression import parent_node_dict +from regression.test_setup import config_data +from pgadmin.utils import server_utils as server_utils +from pgAdmin4 import app +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from .... import socketio + + +class PSQLStartProcessFail(BaseTestGenerator): + def setUp(self): + self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8]) + self.sid = parent_node_dict["server"][-1]["server_id"] + self.did = utils.create_database(self.server, self.db_name) + self.sgid = config_data["server_group"] + config.ENABLE_PSQL = True + + db_con = database_utils.connect_database(self, + self.sgid, + self.sid, + self.did) + print("DB connection") + + def runTest(self): + # Fetch flask client to access current user and other cookies. + flask_client = app.test_client() + flask_client.get('/session') + self.test_client = socketio.test_client(app, namespace='/pty', + flask_test_client=flask_client) + self.assertTrue(self.test_client.is_connected('/pty')) + received = self.test_client.get_received('/pty') + print("received:", received) + assert received[0]['name'] == 'connected' + assert received[0]['args'][0]['sid'] != '' + import random + trans_id = random.randint(1, 9999999) + data = { + 'sid': self.sid, + 'db': 'postgres', + 'pwd': self.server['db_password'], + 'user': self.server['username'] + } + config.ENABLE_PSQL = False + self.test_client.emit('start_process', data, namespace='/pty') + received = self.test_client.get_received('/pty') + assert received[0]['name'] == 'conn_not_allow' + self.test_client.disconnect(namespace='/pty') + self.assertFalse(self.test_client.is_connected('/pty')) + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/tools/psql/tests/utils.py b/web/pgadmin/tools/psql/tests/utils.py new file mode 100644 index 00000000..85bd5373 --- /dev/null +++ b/web/pgadmin/tools/psql/tests/utils.py @@ -0,0 +1,6 @@ +import os +import json + +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/psql_test_data.json") as data_file: + test_cases = json.load(data_file) diff --git a/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss b/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss index 0bb40802..5ffc8241 100644 --- a/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss +++ b/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss @@ -374,3 +374,9 @@ div.strikeout:after { /* Setting it to hardcoded white as the SVG generated is having white bg * Need to check what can be done. */ + +/* Css for psql */ +.psql_terminal .terminal { + padding-top: 1%; + padding-left: 0.5%; +} diff --git a/web/pgadmin/utils/csrf.py b/web/pgadmin/utils/csrf.py index 23abfffa..71ae82ea 100644 --- a/web/pgadmin/utils/csrf.py +++ b/web/pgadmin/utils/csrf.py @@ -38,6 +38,7 @@ class _PGCSRFProtect(CSRFProtect): 'pgadmin.tools.schema_diff.ddl_compare', 'pgadmin.authenticate.login', 'pgadmin.tools.erd.panel', + 'pgadmin.tools.psql.panel', ] for exempt in exempt_views: diff --git a/web/pgadmin/utils/preferences.py b/web/pgadmin/utils/preferences.py index 15ceeb1a..e9efe479 100644 --- a/web/pgadmin/utils/preferences.py +++ b/web/pgadmin/utils/preferences.py @@ -72,6 +72,7 @@ class _Preference(object): self.select2 = kwargs.get('select2', None) self.fields = kwargs.get('fields', None) self.allow_blanks = kwargs.get('allow_blanks', None) + self.disabled = kwargs.get('disabled', False) # Look into the configuration table to find out the id of the specific # preference. @@ -252,6 +253,7 @@ class _Preference(object): 'select2': self.select2, 'value': self.get(), 'fields': self.fields, + 'disabled': self.disabled, } return res @@ -414,6 +416,7 @@ class Preferences(object): :param fields: field schema (if preference has more than one field to take input from user e.g. keyboardshortcut preference) :param allow_blanks: Flag specify whether to allow blank value. + :param disabled: Flag specify whether to disable the setting or not. """ min_val = kwargs.get('min_val', None) max_val = kwargs.get('max_val', None) @@ -423,6 +426,7 @@ class Preferences(object): select2 = kwargs.get('select2', None) fields = kwargs.get('fields', None) allow_blanks = kwargs.get('allow_blanks', None) + disabled = kwargs.get('disabled', False) cat = self.__category(category, category_label) if name in cat['preferences']: @@ -439,7 +443,8 @@ class Preferences(object): (cat['preferences'])[name] = res = _Preference( cat['id'], name, label, _type, default, help_str=help_str, min_val=min_val, max_val=max_val, options=options, - select2=select2, fields=fields, allow_blanks=allow_blanks + select2=select2, fields=fields, allow_blanks=allow_blanks, + disabled=disabled ) return res diff --git a/web/webpack.config.js b/web/webpack.config.js index e906a567..104a3b20 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -355,6 +355,7 @@ module.exports = [{ debugger_direct: './pgadmin/tools/debugger/static/js/direct.js', schema_diff: './pgadmin/tools/schema_diff/static/js/schema_diff_hook.js', erd_tool: './pgadmin/tools/erd/static/js/erd_tool_hook.js', + psql_tool: './pgadmin/tools/psql/static/js/index.js', file_utils: './pgadmin/misc/file_manager/static/js/utility.js', 'pgadmin.style': pgadminCssStyles, pgadmin: pgadminScssStyles, @@ -493,7 +494,7 @@ module.exports = [{ ], }, }, - }, { + },{ test: require.resolve('./node_modules/acitree/js/jquery.aciTree.min'), use: { loader: 'imports-loader', @@ -532,6 +533,7 @@ module.exports = [{ 'pure|pgadmin.tools.storage_manager', 'pure|pgadmin.tools.search_objects', 'pure|pgadmin.tools.erd_module', + 'pure|pgadmin.tools.psql_module', ], }, }, diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 074b2580..e0b28091 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -159,6 +159,15 @@ var webpackShimConfig = { 'jquery.acisortable': path.join(__dirname, './node_modules/acitree/js/jquery.aciSortable.min'), 'jquery.acifragment': path.join(__dirname, './node_modules/acitree/js/jquery.aciFragment.min'), + //xterm + 'xterm': path.join(__dirname, './node_modules/xterm/lib/xterm.js'), + 'xterm-addon-fit': path.join(__dirname, './node_modules/xterm-addon-fit/lib/xterm-addon-fit.js'), + 'xterm-addon-web-links': path.join(__dirname, './node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js'), + 'xterm-addon-search': path.join(__dirname, './node_modules/xterm-addon-search/lib/xterm-addon-search.js'), + + //socket + 'socketio': path.join(__dirname, './node_modules/socket.io-client/dist/socket.io.js'), + // Backbone and Backgrid 'backbone': path.join(__dirname, './node_modules/backbone/backbone'), 'backbone.undo': path.join(__dirname, './node_modules/backbone-undo/Backbone.Undo'), @@ -288,6 +297,8 @@ var webpackShimConfig = { 'pgadmin.tools.storage_manager': path.join(__dirname, './pgadmin/tools/storage_manager/static/js/storage_manager'), 'pgadmin.tools.erd_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'), 'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'), + 'pgadmin.tools.psql_module': path.join(__dirname, './pgadmin/tools/psql/static/js/psql_module'), + 'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'), 'pgadmin.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js'), 'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'), 'pgadmin.user_management.current_user': '/user_management/current_user', diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js index e0b6fd69..3cc079f9 100644 --- a/web/webpack.test.config.js +++ b/web/webpack.test.config.js @@ -177,6 +177,7 @@ module.exports = { 'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'), 'pgadmin.browser.activity': path.join(__dirname, './pgadmin/browser/static/js/activity'), 'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'), + 'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'), 'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'), 'tools': path.join(__dirname, './pgadmin/tools/'), 'pgadmin.user_management.current_user': regressionDir + '/javascript/fake_current_user',