Added support to launch PSQL for the connected database server. Fixes #2341

pull/49/head
Nikhil Mohite 2021-05-25 20:12:57 +05:30 committed by Akshay Joshi
parent 37dece2cd8
commit 3ddf941cd7
53 changed files with 2267 additions and 27 deletions

View File

@ -16,3 +16,4 @@ PL/SQL code.
editgrid
schema_diff
erd_tool
psql_tool

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -128,6 +128,8 @@ Use the *Tools* menu to access the following options (in alphabetical order):
+---------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
| *Search Objects...* | Click to open the :ref:`Search Objects... <search_objects>` and start searching any kind of objects in a database. |
+---------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
| *PSQL Tool* | Click to open the :ref:`PSQL Tool <psql_tool>` and start PSQL in the current database context. |
+---------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
The Help Menu
*************

19
docs/en_US/psql_tool.rst Normal file
View File

@ -0,0 +1,19 @@
.. _psql_tool:
******************
`PSQL Tool`:index:
******************
PSQL tool allows user to connect to PostgreSQL/EDB Advanced server using psql terminal.
* Open PSQL Tool from Tools menu or PSQL tool button from browser tree or from context menu.
* PSQL will connect to the current connected database from browser tree.
.. image:: images/psql_tool.png
:alt: PSQL tool window
:align: center
You can open multiple instance of the PSQL tool in individual tabs simultaneously.
To close the PSQL tool, click the *X* in the upper-right hand corner of the tab bar.

View File

@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
New features
************
| `Issue #2341 <https://redmine.postgresql.org/issues/2341>`_ - Added support to launch PSQL for the connected database server.
| `Issue #4064 <https://redmine.postgresql.org/issues/4064>`_ - Added window maximize/restore functionality for properties dialog.
Housekeeping

View File

@ -20,4 +20,6 @@ the selected browser node.
* Use the :ref:`Filtered Rows <viewdata_filter>` button to access the Data Filter popup
to apply a filter to a set of data for viewing/editing.
* Use the :ref:`Search objects <search_objects>` button to access the search objects
dialog. It helps you search any database object.
dialog. It helps you search any database object.
* Use the :ref:`PSQL Tool <psql_tool>` button to open the PSQL in the current
database context.

View File

@ -8,7 +8,6 @@
#
###############################################################################
cheroot==8.*
Flask==1.*
Flask-Gravatar==0.*
Flask-Login==0.*
@ -37,3 +36,5 @@ sshtunnel==0.*
ldap3==2.*
Flask-BabelEx==0.*
gssapi==1.6.*
flask-socketio>=5.0.1
eventlet==0.30.2

View File

@ -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,21 @@ 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

View File

@ -117,13 +117,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.13.1",
"webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker/#89e006611f4d0fc24b0a098fa2041821d093be4f",
"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 .",

View File

@ -13,7 +13,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 '
@ -37,7 +37,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 +97,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 +208,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())

View File

@ -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=False, engineio_logger=False, 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

View File

@ -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,
@ -523,3 +526,16 @@ def register_browser_preferences(self):
'placeholder': gettext('Select open new tab...')
}
)
self.psql_tab_title = self.preference.register(
'tab_settings', 'psql_tab_title_placeholder',
gettext("PSQL tool tab title"),
'text', '%DATABASE%/%USERNAME%@%SERVER%',
category_label=PREF_LABEL_DISPLAY,
help_str=gettext(
'Supported placeholders are %DATABASE%, %USERNAME%, and %SERVER%. '
'Users can provide any string with or without placeholders of'
' their choice. The blank title will be revert back to the'
' default title with placeholders.'
)
)

View File

@ -34,6 +34,7 @@ from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED
from sqlalchemy import or_
from pgadmin.utils.preferences import Preferences
from .... import socketio as sio
def has_any(data, keys):
@ -1499,6 +1500,13 @@ class ServerNode(PGChildNodeView):
# Release Connection
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
# Check if any psql terminal is running for the current disconnecting
# server. If any terminate the psql tool connection.
if 'sid_soid_mapping' in current_app.config and str(sid) in \
current_app.config['sid_soid_mapping']:
if str(sid) in current_app.config['sid_soid_mapping']:
for i in current_app.config['sid_soid_mapping'][str(sid)]:
sio.emit('disconnect-psql', namespace='/pty', to=i)
status = manager.release()

View File

@ -122,6 +122,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);
},

View File

@ -101,7 +101,8 @@ define('pgadmin.node.server', [
icon: 'fa fa-unlink', enable : 'is_connected',data: {
data_disabled: gettext('Database is already disconnected.'),
},
},{
},
{
name: 'reload_configuration', node: 'server', module: this,
applies: ['tools', 'context'], callback: 'reload_configuration',
category: 'reload', priority: 6, label: gettext('Reload Configuration'),
@ -728,6 +729,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: {

View File

@ -64,6 +64,14 @@ define([
priority: 997, label: gettext('Search Objects...'),
icon: 'fa fa-search',
}]);
// show psql tool same as query tool.
pgAdmin.Browser.add_menus([{
name: 'show_psql_tool', node: this.type, module: this,
applies: ['context'], callback: 'show_psql_tool',
priority: 998, label: gettext('PSQL Tool (Beta)'),
icon: 'fas fa-terminal',
}]);
}
}
},
@ -498,6 +506,13 @@ define([
pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected());
}
},
show_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);
},
});
return pgBrowser.Collection;

View File

@ -209,6 +209,16 @@ define('pgadmin.browser.node', [
priority: 997, label: gettext('Search Objects...'),
icon: 'fa fa-search', enable: enable,
}]);
if(pgAdmin['enable_psql']) {
// show psql tool same as query tool.
pgAdmin.Browser.add_menus([{
name: 'show_psql_tool', node: this.type, module: this,
applies: ['context'], callback: 'show_psql_tool',
priority: 998, label: gettext('PSQL Tool (Beta)'),
icon: 'fas fa-terminal',
}]);
}
}
// This will add options of scripts eg:'CREATE Script'
@ -903,6 +913,15 @@ define('pgadmin.browser.node', [
pgAdmin.DataGrid.show_query_tool('', i);
},
// Callback to render psql tool.
show_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);
},
// Logic to change the server background colour
// There is no way of applying CSS to parent element so we have to
// do it via JS code only

View File

@ -56,9 +56,23 @@ let _defaultToolBarButtons = [
toggleClass: '',
parentClass: 'pg-toolbar-btn btn-primary-icon',
enabled: false,
},
}
];
if(pgAdmin['enable_psql']) {
_defaultToolBarButtons.push({
label: gettext('PSQL Tool'),
ariaLabel: gettext('PSQL Tool'),
btnClass: 'fas fa-terminal',
text: '',
toggled: false,
toggleClass: '',
parentClass: 'pg-toolbar-btn btn-primary-icon pg-toolbar-psql',
enabled: false,
});
}
// Place holder for non default tool bar buttons.
let _otherToolbarButtons = [];
@ -105,6 +119,13 @@ export function initializeToolbar(panel, wcDocker) {
pgAdmin.DataGrid.show_filtered_row({mnuid: 4}, pgAdmin.Browser.tree.selected());
else if ('name' in data && data.name === gettext('Search objects'))
pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected());
else if ('name' in data && data.name === gettext('PSQL Tool')){
var input = {},
t = pgAdmin.Browser.tree,
i = input.item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
pgAdmin.Browser.psql.psql_tool(d, i, true);
}
});
}

View File

@ -53,3 +53,9 @@ samp,
border-width: 1px;
font-size: 1.15em;
}
.pg-toolbar-psql {
padding-top: 0em;
font-size: inherit;
align-items: center;
}

View File

@ -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',

View File

@ -62,7 +62,8 @@ def underscore_unescape(text):
"&gt;": '>',
"&quot;": '"',
"&#96;": '`',
"&#x27;": "'"
"&#x27;": "'",
"&#39;": "'"
}
# always replace & first

View File

@ -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();
});

View File

@ -21,3 +21,5 @@
@import '../vendor/backgrid/backgrid.css';
@import '../vendor/backgrid/backgrid-select-all.css';
@import 'node_modules/xterm/css/xterm.css';

View File

@ -10,6 +10,11 @@ $theme-colors: (
--color-fg: #{$color-fg};
--color-bg: #{$color-bg};
--border-color: #{$border-color};
--psql-background: #{$psql-background};
--psql-foreground: #{$psql-foreground};
--psql-cursor: #{$psql-cursor};
--psql-cursorAccent: #{$psql-cursorAccent};
--psql-selection: #{$psql-selection};
}
@import "bootstrap/scss/bootstrap";

View File

@ -371,3 +371,10 @@ $erd-link-selected-color: $color-fg !default;
$erd-bg-grid: url("data:image/svg+xml, %3Csvg width='100%25' viewBox='0 0 45 45' style='background-color:#{url-friendly-colour($erd-canvas-bg)}' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cpattern id='smallGrid' width='15' height='15' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 15 0 L 0 0 0 15' fill='none' stroke='#{url-friendly-colour($erd-canvas-grid)}' stroke-width='0.5'/%3E%3C/pattern%3E%3Cpattern id='grid' width='45' height='45' patternUnits='userSpaceOnUse'%3E%3Crect width='100' height='100' fill='url(%23smallGrid)'/%3E%3Cpath d='M 100 0 L 0 0 0 100' fill='none' stroke='#{url-friendly-colour($erd-canvas-grid)}' stroke-width='1'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23grid)' /%3E%3C/svg%3E%0A");
$select2-readonly: $color-gray-lighter !default;
// psql tool variables
$psql-background: $color-bg !default;
$psql-foreground: $color-fg !default;
$psql-cursor: $color-fg !default;
$psql-cursorAccent: $color-fg !default;
$psql-selection: #326690 !default;

View File

@ -136,3 +136,10 @@ $erd-link-color: $color-fg;
$erd-link-selected-color: $color-fg;
$select2-readonly: $color-bg;
// psql tool variables
$psql-background: $color-bg;
$psql-foreground: $color-fg;
$psql-cursor: $color-fg;
$psql-cursorAccent: $color-fg;
$psql-selection: #d6effc;

View File

@ -207,3 +207,10 @@ $quick-search-a-text-color: $black !default;
$quick-search-info-icon: #8A8A8A !default;
$select2-readonly: $color-gray;
// psql tool variables
$psql-background: $color-bg;
$psql-foreground: $color-fg;
$psql-cursor: $color-fg;
$psql-cursorAccent: $color-fg;
$psql-selection: $color-primary-light;

View File

@ -100,6 +100,10 @@ export function generateTitle(title_placeholder, title_data) {
title_placeholder = title_placeholder.replace(new RegExp('%ARGS%'), _.unescape(title_data.args));
title_placeholder = title_placeholder.replace(new RegExp('%SCHEMA%'), _.unescape(title_data.schema));
title_placeholder = title_placeholder.replace(new RegExp('%DATABASE%'), _.unescape(title_data.database));
} else if(title_data.type == 'psql_tool') {
title_placeholder = title_placeholder.replace(new RegExp('%DATABASE%'), _.unescape(title_data.database));
title_placeholder = title_placeholder.replace(new RegExp('%USERNAME%'), _.unescape(title_data.username));
title_placeholder = title_placeholder.replace(new RegExp('%SERVER%'), _.unescape(title_data.server));
}
return _.escape(title_placeholder);

View File

@ -0,0 +1,686 @@
#!/usr/bin/env python3
import fcntl
import os
import pty
import re
import select
import struct
import termios
import config
import eventlet.green.subprocess as subprocess
from config import PG_DEFAULT_DRIVER
from flask import Response, url_for, request
from flask import render_template, copy_current_request_context, \
current_app as app
from flask_babelex import gettext
from flask_security import login_required, current_user
from pgadmin.browser.utils import underscore_unescape
from pgadmin.utils import PgAdminModule
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.utils.driver import get_driver
from ... import socketio as sio
from pgadmin.utils import get_complete_file_path
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/<int:trans_id>',
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 'sid_soid_mapping' not in app.config:
app.config['sid_soid_mapping'] = dict()
if request.args:
params.update({k: v for k, v in request.args.items()})
# Set TERM env for xterm.
os.environ['TERM'] = 'xterm'
return render_template('editor_template.html',
sid=params['sid'],
db=underscore_unescape(params['db']) if params[
'db'] else 'postgres',
server_type=params['server_type'],
is_enable=config.ENABLE_PSQL,
title=underscore_unescape(params['title']),
theme=params['theme']
)
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': False}
else:
sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
to=request.sid)
def create_pty_terminal(connection_data):
# 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)
return p, parent, fd
def read_terminal_data(parent, data_ready, max_read_bytes, sid):
"""
Read the terminal output.
:param parent:
:param data_ready:
:param max_read_bytes:
:param sid:
:return:
"""
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 and session_last_cmd[sid]['invalid_cmd']:
# If command is invalid then emit error to user.
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
@sio.on('start_process', namespace='/pty')
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
p, parent, fd = create_pty_terminal(connection_data)
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)
read_terminal_data(parent, data_ready, max_read_bytes, sid)
# 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 = underscore_unescape(data['db']).replace('\\', "\\\\")
conn, manager = _get_connection(int(data['sid']), data)
psql_utility = manager.utility('sql')
connection_data = get_connection_str(psql_utility, db,
manager)
except Exception as e:
# If any error raised during the start the PSQL emit error to UI.
# request.sid: This sid is socket id.
sio.emit(
'conn_error',
{
'error': 'Error while running psql command: {0}'.format(e),
}, namespace='/pty', room=request.sid)
try:
if str(data['sid']) not in app.config['sid_soid_mapping']:
# request.sid: refer request.sid as socket id.
app.config['sid_soid_mapping'][str(data['sid'])] = list()
app.config['sid_soid_mapping'][str(data['sid'])].append(
request.sid)
else:
app.config['sid_soid_mapping'][str(data['sid'])].append(
request.sid)
sio.start_background_task(read_and_forward_pty_output,
request.sid, data)
except Exception as e:
sio.emit(
'conn_error',
{
'error': 'Error while running psql command: {0}'.format(e),
}, namespace='/pty', room=request.sid)
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()
# This is added for unit test only, no use in normal execution.
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)
sio.emit(sio.emit(
'conn_error',
{
'error': 'Error while running psql command: {0}'
''.format('Server connection not present.'),
}, namespace='/pty', room=request.sid))
raise RuntimeError('Server is not connected.')
return conn, manager
except Exception as e:
app.logger.error(e)
raise
def get_connection_str(psql_utility, db, manager):
"""
Get connection string(through connection dsn)
:param psql_utility: PostgreSQL binary path.
:param db: database name to connect specific db.
:return: connection attribute list for PSQL connection.
"""
conn_attr = get_conn_str(manager, db)
conn_attr_list = list()
conn_attr_list.append(psql_utility)
conn_attr_list.append(conn_attr)
return conn_attr_list
def get_conn_str(manager, db):
"""
Get connection attributes for psql connection.
:param manager:
:param db:
:return:
"""
manager.export_password_env('PGPASSWORD')
conn_attr =\
"host={0} port={1} dbname={2} user={3} sslmode={4} " \
"sslcompression={5} " \
"".format(
manager.local_bind_host if manager.use_ssh_tunnel else
manager.host,
manager.local_bind_port if manager.use_ssh_tunnel else
manager.port,
db if db != '' else 'postgres',
manager.user if manager.user else 'postgres',
manager.ssl_mode,
True if manager.sslcompression else False,
)
if manager.hostaddr:
conn_attr = " {0} hostaddr={1}".format(conn_attr, manager.hostaddr)
if manager.passfile:
conn_attr = " {0} passfile={1}".format(conn_attr,
get_complete_file_path(
manager.passfile))
if get_complete_file_path(manager.sslcert):
conn_attr = " {0} sslcert={1}".format(
conn_attr, get_complete_file_path(manager.sslcert))
if get_complete_file_path(manager.sslkey):
conn_attr = " {0} sslkey={1}".format(
conn_attr, get_complete_file_path(manager.sslkey))
if get_complete_file_path(manager.sslrootcert):
conn_attr = " {0} sslrootcert={1}".format(
conn_attr, get_complete_file_path(manager.sslrootcert))
if get_complete_file_path(manager.sslcrl):
conn_attr = " {0} sslcrl={1}".format(
conn_attr, get_complete_file_path(manager.sslcrl))
if manager.service:
conn_attr = " {0} service={1}".format(
conn_attr, get_complete_file_path(manager.service))
return conn_attr
def check_last_exe_cmd(data):
"""
Check the is user try to execute last executed command.
:param data:
:return:
"""
# 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:
if request.sid not in session_input:
session_input[request.sid] = data['input']
user_input = str(session_input[request.sid]).strip()
else:
user_input = str(session_input[request.sid]).strip()
return user_input
def invalid_cmd():
"""
Invalid command
:return:
:rtype:
"""
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] = ''
def check_valid_cmd(user_input):
"""
Check if user entered a valid cmd and \\! command is preset as a string
only in current executing command. if \\! is present as command don't
allow the execution of command.
:param user_input:
:return:
"""
stop_execution = True
# Check \! is passed as string or not.
double_quote_strs = re.findall('"([^"]*)"', user_input)
if not double_quote_strs:
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())
def enter_key_press(data):
"""
Handel the Enter key press event.
:param data:
"""
user_input = check_last_exe_cmd(data)
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:
invalid_cmd()
elif re.search("\\\!", user_input) and \
not config.ALLOW_PSQL_SHELL_COMMANDS and\
not session_last_cmd[request.sid]['is_new_connection']:
check_valid_cmd(user_input)
elif user_input == '\q' or user_input == 'q\\q':
# If user enter \q to terminate the PSQL, emit the msg to
# notify user connection is terminated.
sio.emit('pty-output',
{
'result': gettext(
'Connection terminated, To create new '
'connection please open another psql'
' tool.'),
'error': True},
namespace='/pty', room=request.sid)
os.write(app.config['sessions'][request.sid],
'\n'.encode())
else:
os.write(app.config['sessions'][request.sid],
data['input'].encode())
session_input[request.sid] = ''
session_last_cmd[request.sid]['is_new_connection'] = False
def backspace_key_press():
"""
Handel the backspace key press event.
:return:
:rtype:
"""
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'] = ''
def set_user_input(data):
"""
Check and set current input as user input in session_input.
:param data:
"""
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
def other_key_press(data):
"""
Handel the other key press from psql tool.
:param data:
:type data:
:return:
:rtype:
"""
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])):
backspace_key_press()
elif request.sid in session_input:
set_user_input(data)
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())
@sio.on('socket_input', namespace='/pty')
def socket_input(data):
"""
This get the user input through socket.
:param data: User input from socket.
"""
try:
# request.sid: refer request.sid as socket id.
# 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:
enter_key_press(data)
else:
other_key_press(data)
except Exception as e:
# Delete socket id from sessions.
# request.sid: refer request.sid as socket id.
sio.emit('pty-output',
{
'result': gettext('Invalid session.\r\n'),
'error': True
},
namespace='/pty', room=request.sid)
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
"""
# request.sid: refer request.sid as socket id.
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
"""
# request.sid: refer request.sid as socket id.
if request.sid in pdata:
# On disconnect socket manually exit the psql terminal and close the
# parend and child fd then kill the subprocess.
disconnect_socket()
@sio.on('server-disconnect', namespace='/pty')
def server_disconnect(data):
"""
Disconnect the socket and terminate the process after user disconnect
the server. we can't use disconnect event name as it is reserved for socket
internal use.
"""
# request.sid: refer request.sid as socket id.
if request.sid in pdata and request.sid in app.config['sid_soid_mapping'][
data['sid']]:
# On disconnect socket manually exit the psql terminal and close the
# parend and child fd then kill the subprocess.
app.config['sid_soid_mapping'][data['sid']] = [soid for soid in
app.config[
'sid_soid_mapping'][
data['sid']] if
soid != request.sid]
disconnect_socket()
def disconnect_socket():
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]

View File

@ -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,
};

View File

@ -0,0 +1,429 @@
/////////////////////////////////////////////////////////////
//
// 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 {enable} from 'pgadmin.browser.toolbar';
import clipboard from 'sources/selection/clipboard';
import 'wcdocker';
import {getRandomInt} from 'sources/utils';
import {getTreeNodeHierarchyFromIdentifier} from 'sources/tree/pgadmin_tree_node';
import {generateTitle} from 'tools/datagrid/static/js/datagrid_panel_title';
export function setPanelTitle(psqlToolPanel, panelTitle) {
psqlToolPanel.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');
}
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',
});
var 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;
},
/* Enable/disable PSQL tool menu in tools based
* on node selected. if selected node is present
* in unsupported_nodes, menu will be disabled
* otherwise enabled.
*/
psqlToolEnabled: function(obj) {
var isEnabled = (() => {
if (!_.isUndefined(obj) && !_.isNull(obj) && pgAdmin['enable_psql']) {
if (_.indexOf(pgAdmin.unsupported_nodes, obj._type) == -1) {
if (obj._type == 'database' && obj.allowConn) {
return true;
} else if (obj._type != 'database') {
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
})();
enable(gettext('PSQL Tool'), isEnabled);
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);
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 = '';
// Set psql tab title as per prefrences setting.
var title_data = {
'database': parentData.database ? parentData.database.label : 'postgres' ,
'username': parentData.server.user_name,
'server': parentData.server.label,
'type': 'psql_tool',
};
var tab_title_placeholder = pgBrowser.get_preferences_for_module('browser').psql_tab_title_placeholder;
panelTitle = generateTitle(tab_title_placeholder, title_data);
const [panelUrl, panelCloseUrl] = this.getPanelUrls(transId, panelTitle, parentData, gen);
let psqlToolForm = `
<form id="psqlToolForm" action="${panelUrl}" method="post">
<input id="title" name="title" hidden />
<input name="close_url" value="${panelCloseUrl}" hidden />
</form>
<script>
document.getElementById("title").value = "${_.escape(panelTitle)}";
document.getElementById("psqlToolForm").submit();
</script>
`;
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 psql-tab-style');
psqlToolPanel.focus();
var openPSQLToolURL = function(j) {
// add spinner element
let $spinner_el =
$(`<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>`).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);
}
},
getPanelUrls: function(transId, panelTitle, parentData) {
let openUrl = url_for('psql.panel', {
trans_id: transId,
});
const misc_preferences = pgBrowser.get_preferences_for_module('misc');
var theme = misc_preferences.theme;
openUrl += `?sgid=${parentData.server_group._id}`
+`&sid=${parentData.server._id}`
+`&server_type=${parentData.server.server_type}`
+ `&theme=${theme}`;
if(parentData.database && parentData.database._id) {
let db_label = parentData.database._label.replace('\\', '\\\\');
openUrl += `&db=${db_label}`;
} else {
openUrl += `&db=${''}`;
}
let closeUrl = url_for('psql.close', {
trans_id: transId,
});
return [openUrl, closeUrl];
},
psql_terminal: function() {
// theme colors
var term = new terminal({
cursorBlink: true,
macOptionIsMeta: true,
scrollback: 5000,
});
return term;
},
psql_Addon: function(term) {
const fitAddon = this.psql_fit_screen();
term.loadAddon(fitAddon);
const webLinksAddon = this.psql_web_link();
term.loadAddon(webLinksAddon);
const searchAddon = this.psql_search();
term.loadAddon(searchAddon);
fitAddon.fit();
term.resize(15, 50);
fitAddon.fit();
return fitAddon;
},
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});
},
set_theme: function(term) {
var theme = {
background: getComputedStyle(document.documentElement).getPropertyValue('--psql-background'),
foreground: getComputedStyle(document.documentElement).getPropertyValue('--psql-foreground'),
cursor: getComputedStyle(document.documentElement).getPropertyValue('--psql-cursor'),
cursorAccent: getComputedStyle(document.documentElement).getPropertyValue('--psql-cursorAccent'),
selection: getComputedStyle(document.documentElement).getPropertyValue('--psql-selection'),
};
term.setOption('theme', theme);
},
psql_socket_io: function(socket, is_enable, sid, db, server_type, fitAddon, term) {
// Listen all the socket events emit from server.
socket.on('pty-output', function(data){
if(data.error) {
term.write('\r\n');
}
term.write(data.result);
if(data.error) {
term.write('\r\n');
}
});
// Connect socket
socket.on('connect', () => {
if(is_enable == 'True'){
socket.emit('start_process', {'sid': sid, 'db': db, 'stype': server_type });
}
fitAddon.fit();
socket.emit('resize', {'cols': term.cols, 'rows': term.rows});
});
socket.on('conn_error', (response) => {
term.write(response.error);
fitAddon.fit();
socket.emit('resize', {'cols': term.cols, 'rows': term.rows});
});
socket.on('conn_not_allow', () => {
term.write('PSQL connection not allowed');
fitAddon.fit();
socket.emit('resize', {'cols': term.cols, 'rows': term.rows});
});
socket.on('disconnect-psql', () => {
socket.emit('server-disconnect', {'sid': sid});
term.write('\r\nServer disconnected, Connection terminated, To create new connection please open another psql tool.');
});
},
psql_terminal_io: function(term, socket) {
// Listen key press event from terminal and emit socket event.
let selected_text = '';
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 {
socket.emit('socket_input', {'input': ev.key, 'key_name': ev.domEvent.code});
}
});
}
};
return pgBrowser.psql;
}

View File

@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block title %}{{title}}{% endblock %}
{% block css_link %}
<link type="text/css" rel="stylesheet" href="{{ url_for('browser.browser_css')}}"/>
{% endblock %}
{% block body %}
<style>
body {padding: 0px;}
{% if is_desktop_mode and is_linux %}
.alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform: none;}
.alertify-notifier{-webkit-transform: none;}
.alertify-notifier .ajs-message{-webkit-transform: none;}
.alertify .ajs-dialog.ajs-shake{-webkit-animation-name: none;}
.sql-editor-busy-icon.fa-pulse{-webkit-animation: none;}
{% endif %}
</style>
<div style="width: 100%; height: 100%;" id="psql-terminal" class="psql_terminal"></div>
{% endblock %}
{% block init_script %}
require(
['sources/generated/psql_tool'],
function(pgBrowser) {
const term = self.pgAdmin.Browser.psql.psql_terminal();
<!--Addon for fitAddon, webLinkAddon, SearchAddon -->
const fitAddon = self.pgAdmin.Browser.psql.psql_Addon(term);
<!-- Update the theme for terminal as per pgAdmin 4 theme.-->
self.pgAdmin.Browser.psql.set_theme(term);
<!-- Open the terminal -->
term.open(document.getElementById('psql-terminal'));
<!-- Socket-->
const socket = self.pgAdmin.Browser.psql.psql_socket();
self.pgAdmin.Browser.psql.psql_socket_io(socket, '{{is_enable}}', '{{sid}}', '{{db}}', '{{server_type}}', fitAddon, term);
self.pgAdmin.Browser.psql.psql_terminal_io(term, socket);
<!-- Resize the terminal -->
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 %}

View File

View File

@ -0,0 +1,184 @@
{
"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": "First command as enter",
"is_positive_test": true,
"mocking_required": false,
"input_cmd": "",
"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": {
}
}
]
}

View File

@ -0,0 +1,87 @@
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('/')
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)

View File

@ -0,0 +1,36 @@
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"]
self.theme = 'standard'
def runTest(self):
trans_id = random.randint(1, 9999999)
url = '/psql/panel/{trans_id}?sgid={sgid}&sid={sid}&server_type=pg' \
'&db={db_name}&theme={theme}'.\
format(trans_id=trans_id, sgid=self.sgid, sid=self.sid,
db_name=self.db_name, theme=self.theme)
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)

View File

@ -0,0 +1,34 @@
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 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')
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)

View File

@ -0,0 +1,148 @@
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('/')
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')
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')
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')
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)

View File

@ -0,0 +1,58 @@
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('/')
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')
self.test_client.get_received('/pty')
self.test_client.emit('resize', self.input_data, namespace='/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)

View File

@ -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 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')
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)

View File

@ -0,0 +1,52 @@
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 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):
# Fetch flask client to access current user and other cookies.
flask_test_client = app.test_client()
flask_test_client.get('/')
self.test_client = socketio.test_client(
app,
flask_test_client=flask_test_client,
namespace='/pty')
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')
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)

View File

@ -0,0 +1,57 @@
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 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)
def runTest(self):
# Fetch flask client to access current user and other cookies.
flask_client = app.test_client()
flask_client.get('/')
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')
self.test_client.get_received('/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)

View File

@ -0,0 +1,48 @@
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 pgAdmin4 import app
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
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')
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']
}
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)

View File

@ -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)

View File

@ -374,3 +374,20 @@ 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%;
height: 100%;
}
.psql-icon-style {
font-size: inherit;
padding-left: 0em;
}
.psql-tab-style {
font-size: small;
padding-left: 0em;
}

View File

@ -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:

View File

@ -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

View File

@ -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',
],
},
},

View File

@ -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',

View File

@ -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',

View File

@ -2031,6 +2031,11 @@ backgrid@~0.3.7:
backbone "1.1.2 || 1.2.3 || ~1.3.2"
underscore "^1.8.0"
backo2@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -3638,7 +3643,22 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
engine.io-parser@~4.0.0:
engine.io-client@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.1.1.tgz#f5c3aaaef1bdc9443aac6ffde48b3b2fb2dc56fc"
integrity sha512-jPFpw2HLL0lhZ2KY0BpZhIJdleQcUO9W1xkIpo0h3d6s+5D6+EV/xgQw9qWOmymszv2WXef/6KUUehyxEKomlQ==
dependencies:
base64-arraybuffer "0.1.4"
component-emitter "~1.3.0"
debug "~4.3.1"
engine.io-parser "~4.0.1"
has-cors "1.1.0"
parseqs "0.0.6"
parseuri "0.0.6"
ws "~7.4.2"
yeast "0.1.2"
engine.io-parser@~4.0.0, engine.io-parser@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
@ -4643,6 +4663,11 @@ has-bigints@^1.0.1:
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
has-cors@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@ -6734,6 +6759,16 @@ parse5@^6.0.1:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parseqs@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
parseuri@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@ -7985,7 +8020,20 @@ socket.io-adapter@~2.1.0:
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527"
integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==
socket.io-parser@~4.0.3:
socket.io-client@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.1.2.tgz#95ad7113318ea01fba0860237b96d71e1b1fd2eb"
integrity sha512-RDpWJP4DQT1XeexmeDyDkm0vrFc0+bUsHDKiVGaNISJvJonhQQOMqV9Vwfg0ZpPJ27LCdan7iqTI92FRSOkFWQ==
dependencies:
"@types/component-emitter" "^1.2.10"
backo2 "~1.0.2"
component-emitter "~1.3.0"
debug "~4.3.1"
engine.io-client "~5.1.1"
parseuri "0.0.6"
socket.io-parser "~4.0.4"
socket.io-parser@~4.0.3, socket.io-parser@~4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
@ -9218,6 +9266,26 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
xterm-addon-fit@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
xterm-addon-search@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0.tgz#e33eab918df7eac7e7baf95dd2b3d14133754881"
integrity sha512-MPJGPVPpHRUw9cLIuqQbrVepmENMOybVUSxIALz5h1ryyQBrVqVujq2hL5aroX5/dZJoHx9lGHQTVLQ07SKgKA==
xterm-addon-web-links@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0.tgz#265cbf8221b9b315d0a748e1323bee331cd5da03"
integrity sha512-xv8GeiINmx0zENO9hf5k+5bnkaE8mRzF+OBAr9WeFq2eLaQSudioQSiT34M1ofKbzcdjSsKiZm19Rw3i4eXamg==
xterm@^4.11.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0.tgz#db09b425b4dcae5b96f8cbbaaa93b3bc60997ca9"
integrity sha512-K5mF/p3txUV18mjiZFlElagoHFpqXrm5OYHeoymeXSu8GG/nMaOO/+NRcNCwfdjzAbdQ5VLF32hEHiWWKKm0bw==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
@ -9273,6 +9341,11 @@ yauzl@^2.4.2:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"