1919from pgadmin .utils .ajax import make_json_response
2020from pgadmin .user_login_check import pga_login_required
2121
22- MODULE_NAME = 'ep '
22+ MODULE_NAME = 'expl_pgsql '
2323
24- class EPModule (PgAdminModule ):
24+
25+ class ExplPgsqlModule (PgAdminModule ):
2526 """Explain PostgreSQL configuration module for pgAdmin."""
2627
2728 LABEL = gettext ('Explain PostgreSQL' )
28-
29+
2930 def register_preferences (self ):
3031 """
3132 Register preferences for Explain PostgreSQL.
3233 """
3334
35+ self .explain_module = self .preference .register (
36+ 'Explain PostgreSQL' , 'explain_postgresql' ,
37+ gettext ("Explain Plan" ), 'boolean' , False ,
38+ category_label = gettext ('Configuration' ),
39+ help_str = gettext ('Analyze query plan via Explain PostgreSQL API' )
40+ )
41+
3442 self .explain_postgresql_api = self .preference .register (
3543 'Explain PostgreSQL' , 'explain_postgresql_api' ,
36- gettext ("Explain PostgreSQL API" ), 'text' , 'https://explain.tensor.ru' ,
44+ gettext ("Explain PostgreSQL API" ), 'text' ,
45+ 'https://explain.tensor.ru' ,
3746 category_label = gettext ('Configuration' ),
38- help_str = gettext ('Explain PostgreSQL API endpoint (e.g. https://explain-postgresql.com)' ),
47+ help_str = gettext (
48+ 'Explain PostgreSQL API endpoint '
49+ '(e.g. https://explain.tensor.ru)'
50+ ),
3951 allow_blanks = False
4052 )
4153
4254 self .explain_postgresql_private = self .preference .register (
4355 'Explain PostgreSQL' , 'explain_postgresql_private' ,
4456 gettext ("Private Plans" ), 'boolean' , False ,
4557 category_label = gettext ('Configuration' ),
46- help_str = gettext ('Hide plans from public access on Explain PostgreSQL' )
47- )
48-
49- self .explain_module = self .preference .register (
50- 'Explain PostgreSQL' , 'explain_postgresql' ,
51- gettext ("Explain Plan" ), 'boolean' , True ,
52- category_label = gettext ('Configuration' ),
53- help_str = gettext ('Analyze query plan via Explain PostgreSQL API' )
58+ help_str = gettext (
59+ 'Hide plans from public access on Explain PostgreSQL'
60+ )
5461 )
5562
5663 self .explain_postgresql_format = self .preference .register (
5764 'Explain PostgreSQL' , 'explain_postgresql_format' ,
58- gettext ("Format SQL" ), 'boolean' , True ,
65+ gettext ("Format SQL" ), 'boolean' , False ,
5966 category_label = gettext ('Configuration' ),
6067 help_str = gettext ('Format SQL using Explain PostgreSQL API' )
6168 )
@@ -65,109 +72,154 @@ def get_exposed_url_endpoints(self):
6572 Returns the list of URLs exposed to the client.
6673 """
6774 return [
68- 'ep.explain_postgresql' ,
69- 'ep.explain_postgresql_format' ,
75+ 'expl_pgsql.status' ,
76+ 'expl_pgsql.explain' ,
77+ 'expl_pgsql.formatSQL' ,
7078 ]
7179
7280
7381# Initialise the module
74- blueprint = EPModule (MODULE_NAME , __name__ , static_url_path = '/static' )
82+ blueprint = ExplPgsqlModule (MODULE_NAME , __name__ , static_url_path = '/static' )
83+
84+
85+ @blueprint .route ("/status" , methods = ["GET" ], endpoint = 'status' )
86+ @pga_login_required
87+ def get_status ():
88+ """
89+ Get the status of the Explain PostgreSQL configuration.
90+ Indicates whether the analysis of query plans
91+ via the Explain PostgreSQL API is currently enabled
92+ """
93+
94+ return make_json_response (
95+ success = 1 ,
96+ data = {
97+ 'enabled' : get_preference_value ('explain_postgresql' ),
98+ }
99+ )
100+
75101
76102@blueprint .route (
77- '/explain_postgresql_format ' ,
78- methods = ["POST" ], endpoint = 'explain_postgresql_format '
103+ '/formatSQL ' ,
104+ methods = ["POST" ], endpoint = 'formatSQL '
79105)
80106@pga_login_required
81- def explain_postgresql_format ():
107+ def formatSQL ():
82108 """
83109 This method is used to send sql to explain postgresql beatifier api.
84-
85110 """
86111
87112 data = request .get_json (silent = True )
88113 if not isinstance (data , dict ):
89114 return make_json_response (
90115 success = 0 ,
91116 errormsg = "Invalid JSON payload. Expected an object/dictionary." ,
92- info = gettext ('JSON payload must be an object, not null, array, or scalar value' ),
117+ info = gettext (
118+ 'JSON payload must be an object,'
119+ ' not null, array, or scalar value'
120+ ),
93121 )
94-
122+
95123 explain_postgresql_api = get_preference_value ('explain_postgresql_api' )
96-
124+
97125 # Validate the API URL to prevent SSRF
98126 if not is_valid_url (explain_postgresql_api ):
99127 return make_json_response (
100128 success = 0 ,
101- errormsg = "Invalid API endpoint URL. Only HTTP/HTTPS URLs are allowed." ,
102- info = gettext ('The provided API endpoint is not valid. Only HTTP/HTTPS URLs are permitted.' )
129+ errormsg = gettext (
130+ 'Invalid API endpoint URL. Only HTTP/HTTPS URLs are allowed.'
131+ ),
132+ info = gettext (
133+ 'The provided API endpoint is not valid. '
134+ 'Only HTTP/HTTPS URLs are permitted.'
135+ )
103136 )
104137
105- is_error , data = send_post_request (explain_postgresql_api + '/beautifier-api' , data )
138+ api_url = explain_postgresql_api + '/beautifier-api'
139+ is_error , data = send_post_request (api_url , data )
106140 if is_error :
107- return make_json_response (success = 0 , errormsg = data ,
108- info = gettext ('Failed to post data to the Explain Postgresql API' ),
109- )
141+ return make_json_response (
142+ success = 0 ,
143+ errormsg = data ,
144+ info = gettext ('Failed to post data to the Explain Postgresql API' ),
145+ )
110146
111147 return make_json_response (success = 1 , data = data )
112148
113149
114150@blueprint .route (
115- '/explain_postgresql ' ,
116- methods = ["POST" ], endpoint = 'explain_postgresql '
151+ '/explain ' ,
152+ methods = ["POST" ], endpoint = 'explain '
117153)
118154@pga_login_required
119- def explain_postgresql ():
155+ def explain ():
120156 """
121157 This method is used to send plan to explain postgresql api.
122-
123158 """
124159
125160 data = request .get_json (silent = True )
126161 if not isinstance (data , dict ):
127162 return make_json_response (
128163 success = 0 ,
129164 errormsg = "Invalid JSON payload. Expected an object/dictionary." ,
130- info = gettext ('JSON payload must be an object, not null, array, or scalar value' ),
165+ info = gettext (
166+ 'JSON payload must be an object, '
167+ 'not null, array, or scalar value'
168+ ),
131169 )
132-
170+
133171 explain_postgresql_api = get_preference_value ('explain_postgresql_api' )
134-
172+
135173 # Validate the API URL to prevent SSRF
136174 if not is_valid_url (explain_postgresql_api ):
137175 return make_json_response (
138176 success = 0 ,
139- errormsg = "Invalid API endpoint URL. Only HTTP/HTTPS URLs are allowed." ,
140- info = gettext ('The provided API endpoint is not valid. Only HTTP/HTTPS URLs are permitted.' )
177+ errormsg = gettext (
178+ 'Invalid API endpoint URL. Only HTTP/HTTPS URLs are allowed.'
179+ ),
180+ info = gettext (
181+ 'The provided API endpoint is not valid. '
182+ 'Only HTTP/HTTPS URLs are permitted.'
183+ )
141184 )
142-
143- explain_postgresql_private = get_preference_value ('explain_postgresql_private' )
185+
186+ pref_name = 'explain_postgresql_private'
187+ explain_postgresql_private = get_preference_value (pref_name )
144188 data ['private' ] = explain_postgresql_private
145189
146- is_error , response_data = send_post_request (explain_postgresql_api + '/explain' , data )
190+ api_url = explain_postgresql_api + '/explain'
191+ is_error , response_data = send_post_request (api_url , data )
147192 if is_error :
148- return make_json_response (success = 0 , errormsg = response_data ,
149- info = gettext ('Failed to post data to the Explain Postgresql API' ),
150- )
193+ return make_json_response (
194+ success = 0 ,
195+ errormsg = response_data ,
196+ info = gettext ('Failed to post data to the Explain Postgresql API' ),
197+ )
151198
152199 # response_data should be a relative path from 302 Location header
153200 if not response_data .startswith ('/' ):
154- return make_json_response (success = 0 , errormsg = "Unexpected response format from API" )
155- return make_json_response (success = 1 , data = explain_postgresql_api + response_data )
201+ return make_json_response (
202+ success = 0 ,
203+ errormsg = 'Unexpected response format from API'
204+ )
205+ res_data = explain_postgresql_api + response_data
206+ return make_json_response (success = 1 , data = res_data )
156207
157208
158209def is_valid_url (url ):
159210 """
160- Validate that a URL is safe to use (HTTP/HTTPS only, localhost and private IP ranges are allowed).
161-
211+ Validate that a URL is safe to use
212+ (HTTP/HTTPS only, localhost and private IP ranges are allowed).
213+
162214 Args:
163215 url: The URL to validate
164-
216+
165217 Returns:
166218 bool: True if URL is valid, False otherwise
167219 """
168220 if not url :
169221 return False
170-
222+
171223 try :
172224 parsed = urlparse (url )
173225
@@ -184,7 +236,7 @@ def is_valid_url(url):
184236 return False
185237
186238
187- def send_post_request (url_api , data , parse = False ):
239+ def send_post_request (url_api , data ):
188240 data = json .dumps (data ).encode ('utf-8' )
189241 headers = {
190242 "Content-Type" : "application/json; charset=utf-8" ,
@@ -197,13 +249,11 @@ def send_post_request(url_api, data, parse=False):
197249 if (response .code == 302 ):
198250 return False , response .headers ["Location" ]
199251 response_data = response .read ().decode ('utf-8' )
200- if (parse ):
201- return False , json .loads (response_data )
202- else :
203- return False , response_data
252+ return False , response_data
204253 except Exception as e :
205254 return True , str (e )
206255
256+
207257class No302HTTPErrorProcessor (urllib .request .HTTPErrorProcessor ):
208258
209259 def http_response (self , request , response ):
@@ -222,6 +272,7 @@ def http_response(self, request, response):
222272
223273 https_response = http_response
224274
275+
225276class NoRedirectHandler (urllib .request .HTTPRedirectHandler ):
226277 """
227278 A redirect handler that prevents automatic redirects by returning None
@@ -233,14 +284,18 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
233284 """
234285 return None
235286
236- # Build opener without HTTPRedirectHandler so No302HTTPErrorProcessor can handle 302 responses
287+
288+ # Build opener without HTTPRedirectHandler
289+ # so No302HTTPErrorProcessor can handle 302 responses
237290no302opener = urllib .request .build_opener (
238291 No302HTTPErrorProcessor (),
239- NoRedirectHandler (), # Explicitly add NoRedirectHandler to prevent automatic redirects
292+ # Explicitly add NoRedirectHandler to prevent automatic redirects
293+ NoRedirectHandler (),
240294 urllib .request .HTTPHandler (),
241295 urllib .request .HTTPSHandler ()
242296)
243297
298+
244299def get_preference_value (name ):
245300 """
246301 Get a preference value, returning None if empty or not set.
0 commit comments