gooderp18绿色标准版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

682 lines
23KB

  1. ##########################################################################
  2. #
  3. # pgAdmin 4 - PostgreSQL Tools
  4. #
  5. # Copyright (C) 2013 - 2020, The pgAdmin Development Team
  6. # This software is released under the PostgreSQL Licence
  7. #
  8. ##########################################################################
  9. """Browser helper utilities"""
  10. from abc import abstractmethod
  11. import flask
  12. from flask import render_template, current_app
  13. from flask.views import View, MethodViewType, with_metaclass
  14. from flask_babelex import gettext
  15. from config import PG_DEFAULT_DRIVER
  16. from pgadmin.utils.ajax import make_json_response, precondition_required,\
  17. internal_server_error
  18. from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
  19. CryptKeyMissing
  20. def underscore_escape(text):
  21. """
  22. This function mimics the behaviour of underscore js escape function
  23. The html escaped by jinja is not compatible for underscore unescape
  24. function
  25. :param text: input html text
  26. :return: escaped text
  27. """
  28. html_map = {
  29. '&': "&",
  30. '<': "&lt;",
  31. '>': "&gt;",
  32. '"': "&quot;",
  33. '`': "&#96;",
  34. "'": "&#x27;"
  35. }
  36. # always replace & first
  37. for c, r in sorted(html_map.items(),
  38. key=lambda x: 0 if x[0] == '&' else 1):
  39. text = text.replace(c, r)
  40. return text
  41. def underscore_unescape(text):
  42. """
  43. This function mimics the behaviour of underscore js unescape function
  44. The html unescape by jinja is not compatible for underscore escape
  45. function
  46. :param text: input html text
  47. :return: unescaped text
  48. """
  49. html_map = {
  50. "&amp;": '&',
  51. "&lt;": '<',
  52. "&gt;": '>',
  53. "&quot;": '"',
  54. "&#96;": '`',
  55. "&#x27;": "'"
  56. }
  57. # always replace & first
  58. for c, r in html_map.items():
  59. text = text.replace(c, r)
  60. return text
  61. def is_version_in_range(sversion, min_ver, max_ver):
  62. assert (max_ver is None or isinstance(max_ver, int))
  63. assert (min_ver is None or isinstance(min_ver, int))
  64. if min_ver is None and max_ver is None:
  65. return True
  66. if (min_ver is None or min_ver <= sversion) and \
  67. (max_ver is None or max_ver >= sversion):
  68. return True
  69. return False
  70. class PGChildModule(object):
  71. """
  72. class PGChildModule
  73. This is a base class for children/grand-children of PostgreSQL, and
  74. all EDB Postgres Advanced Server version
  75. (i.e. EDB Postgres Advanced Server, Green Plum, etc).
  76. Method:
  77. ------
  78. * backend_supported(manager)
  79. - Return True when it supports certain version.
  80. Uses the psycopg2 server connection manager as input for checking the
  81. compatibility of the current module.
  82. """
  83. def __init__(self, *args, **kwargs):
  84. self.min_ver = 0
  85. self.max_ver = 1100000000
  86. self.min_ppasver = 0
  87. self.max_ppasver = 1100000000
  88. self.server_type = None
  89. self.min_gpdbver = 80323
  90. self.max_gpdbver = 1000000000
  91. super(PGChildModule, self).__init__()
  92. def backend_supported(self, manager, **kwargs):
  93. if hasattr(self, 'show_node'):
  94. if not self.show_node:
  95. return False
  96. sversion = getattr(manager, 'sversion', None)
  97. if sversion is None or not isinstance(sversion, int):
  98. return False
  99. assert (self.server_type is None or isinstance(self.server_type, list))
  100. if self.server_type is None or manager.server_type in self.server_type:
  101. min_server_version = self.min_ver
  102. max_server_version = self.max_ver
  103. if manager.server_type == 'ppas':
  104. min_server_version = self.min_ppasver
  105. max_server_version = self.max_ppasver
  106. if manager.server_type == 'gpdb':
  107. min_server_version = self.min_gpdbver
  108. max_server_version = self.max_gpdbver
  109. return is_version_in_range(sversion, min_server_version,
  110. max_server_version)
  111. return False
  112. @abstractmethod
  113. def get_nodes(self, sid=None, **kwargs):
  114. pass
  115. class NodeView(with_metaclass(MethodViewType, View)):
  116. """
  117. A PostgreSQL Object has so many operaions/functions apart from CRUD
  118. (Create, Read, Update, Delete):
  119. i.e.
  120. - Reversed Engineered SQL
  121. - Modified Query for parameter while editing object attributes
  122. i.e. ALTER TABLE ...
  123. - Statistics of the objects
  124. - List of dependents
  125. - List of dependencies
  126. - Listing of the children object types for the certain node
  127. It will used by the browser tree to get the children nodes
  128. This class can be inherited to achieve the diffrent routes for each of the
  129. object types/collections.
  130. OPERATION | URL | HTTP Method | Method
  131. ---------------+-----------------------------+-------------+--------------
  132. List | /obj/[Parent URL]/ | GET | list
  133. Properties | /obj/[Parent URL]/id | GET | properties
  134. Create | /obj/[Parent URL]/ | POST | create
  135. Delete | /obj/[Parent URL]/id | DELETE | delete
  136. Update | /obj/[Parent URL]/id | PUT | update
  137. SQL (Reversed | /sql/[Parent URL]/id | GET | sql
  138. Engineering) |
  139. SQL (Modified | /msql/[Parent URL]/id | GET | modified_sql
  140. Properties) |
  141. Statistics | /stats/[Parent URL]/id | GET | statistics
  142. Dependencies | /dependency/[Parent URL]/id | GET | dependencies
  143. Dependents | /dependent/[Parent URL]/id | GET | dependents
  144. Nodes | /nodes/[Parent URL]/ | GET | nodes
  145. Current Node | /nodes/[Parent URL]/id | GET | node
  146. Children | /children/[Parent URL]/id | GET | children
  147. NOTE:
  148. Parent URL can be seen as the path to identify the particular node.
  149. i.e.
  150. In order to identify the TABLE object, we need server -> database -> schema
  151. information.
  152. """
  153. operations = dict({
  154. 'obj': [
  155. {'get': 'properties', 'delete': 'delete', 'put': 'update'},
  156. {'get': 'list', 'post': 'create'}
  157. ],
  158. 'nodes': [{'get': 'node'}, {'get': 'nodes'}],
  159. 'sql': [{'get': 'sql'}],
  160. 'msql': [{'get': 'modified_sql'}],
  161. 'stats': [{'get': 'statistics'}],
  162. 'dependency': [{'get': 'dependencies'}],
  163. 'dependent': [{'get': 'dependents'}],
  164. 'children': [{'get': 'children'}]
  165. })
  166. @classmethod
  167. def generate_ops(cls):
  168. cmds = []
  169. for op in cls.operations:
  170. idx = 0
  171. for ops in cls.operations[op]:
  172. meths = []
  173. for meth in ops:
  174. meths.append(meth.upper())
  175. if len(meths) > 0:
  176. cmds.append({
  177. 'cmd': op, 'req': (idx == 0),
  178. 'with_id': (idx != 2), 'methods': meths
  179. })
  180. idx += 1
  181. return cmds
  182. # Inherited class needs to modify these parameters
  183. node_type = None
  184. # This must be an array object with attributes (type and id)
  185. parent_ids = []
  186. # This must be an array object with attributes (type and id)
  187. ids = []
  188. @classmethod
  189. def get_node_urls(cls):
  190. assert cls.node_type is not None, \
  191. "Please set the node_type for this class ({0})".format(
  192. str(cls.__class__.__name__))
  193. common_url = '/'
  194. for p in cls.parent_ids:
  195. common_url += '<{0}:{1}>/'.format(str(p['type']), str(p['id']))
  196. id_url = None
  197. for p in cls.ids:
  198. id_url = '{0}<{1}:{2}>'.format(
  199. common_url if not id_url else id_url,
  200. p['type'], p['id'])
  201. return id_url, common_url
  202. def __init__(self, **kwargs):
  203. self.cmd = kwargs['cmd']
  204. # Check the existance of all the required arguments from parent_ids
  205. # and return combination of has parent arguments, and has id arguments
  206. def check_args(self, **kwargs):
  207. has_id = has_args = True
  208. for p in self.parent_ids:
  209. if p['id'] not in kwargs:
  210. has_args = False
  211. break
  212. for p in self.ids:
  213. if p['id'] not in kwargs:
  214. has_id = False
  215. break
  216. return has_args, has_id and has_args
  217. def dispatch_request(self, *args, **kwargs):
  218. http_method = flask.request.method.lower()
  219. if http_method == 'head':
  220. http_method = 'get'
  221. assert self.cmd in self.operations, \
  222. 'Unimplemented command ({0}) for {1}'.format(
  223. self.cmd,
  224. str(self.__class__.__name__)
  225. )
  226. has_args, has_id = self.check_args(**kwargs)
  227. assert (
  228. self.cmd in self.operations and
  229. (has_id and len(self.operations[self.cmd]) > 0 and
  230. http_method in self.operations[self.cmd][0]) or
  231. (not has_id and len(self.operations[self.cmd]) > 1 and
  232. http_method in self.operations[self.cmd][1]) or
  233. (len(self.operations[self.cmd]) > 2 and
  234. http_method in self.operations[self.cmd][2])
  235. ), \
  236. 'Unimplemented method ({0}) for command ({1}), which {2} ' \
  237. 'an id'.format(http_method,
  238. self.cmd,
  239. 'requires' if has_id else 'does not require')
  240. meth = None
  241. if has_id:
  242. meth = self.operations[self.cmd][0][http_method]
  243. elif has_args and http_method in self.operations[self.cmd][1]:
  244. meth = self.operations[self.cmd][1][http_method]
  245. else:
  246. meth = self.operations[self.cmd][2][http_method]
  247. method = getattr(self, meth, None)
  248. if method is None:
  249. return make_json_response(
  250. status=406,
  251. success=0,
  252. errormsg=gettext(
  253. 'Unimplemented method ({0}) for this url ({1})').format(
  254. meth, flask.request.path
  255. )
  256. )
  257. return method(*args, **kwargs)
  258. @classmethod
  259. def register_node_view(cls, blueprint):
  260. cls.blueprint = blueprint
  261. id_url, url = cls.get_node_urls()
  262. commands = cls.generate_ops()
  263. for c in commands:
  264. cmd = c['cmd'].replace('.', '-')
  265. if c['with_id']:
  266. blueprint.add_url_rule(
  267. '/{0}{1}'.format(
  268. c['cmd'], id_url if c['req'] else url
  269. ),
  270. view_func=cls.as_view(
  271. '{0}{1}'.format(
  272. cmd, '_id' if c['req'] else ''
  273. ),
  274. cmd=c['cmd']
  275. ),
  276. methods=c['methods']
  277. )
  278. else:
  279. blueprint.add_url_rule(
  280. '/{0}'.format(c['cmd']),
  281. view_func=cls.as_view(
  282. cmd, cmd=c['cmd']
  283. ),
  284. methods=c['methods']
  285. )
  286. def children(self, *args, **kwargs):
  287. """Build a list of treeview nodes from the child nodes."""
  288. children = self.get_children_nodes(*args, **kwargs)
  289. # Return sorted nodes based on label
  290. return make_json_response(
  291. data=sorted(
  292. children, key=lambda c: c['label']
  293. )
  294. )
  295. def get_children_nodes(self, *args, **kwargs):
  296. """
  297. Returns the list of children nodes for the current nodes. Override this
  298. function for special cases only.
  299. :param args:
  300. :param kwargs: Parameters to generate the correct set of tree node.
  301. :return: List of the children nodes
  302. """
  303. children = []
  304. for module in self.blueprint.submodules:
  305. children.extend(module.get_nodes(*args, **kwargs))
  306. return children
  307. class PGChildNodeView(NodeView):
  308. _NODE_SQL = 'node.sql'
  309. _NODES_SQL = 'nodes.sql'
  310. _CREATE_SQL = 'create.sql'
  311. _UPDATE_SQL = 'update.sql'
  312. _PROPERTIES_SQL = 'properties.sql'
  313. _DELETE_SQL = 'delete.sql'
  314. _GRANT_SQL = 'grant.sql'
  315. _SCHEMA_SQL = 'schema.sql'
  316. _ACL_SQL = 'acl.sql'
  317. _OID_SQL = 'get_oid.sql'
  318. def get_children_nodes(self, manager, **kwargs):
  319. """
  320. Returns the list of children nodes for the current nodes.
  321. :param manager: Server Manager object
  322. :param kwargs: Parameters to generate the correct set of browser tree
  323. node
  324. :return:
  325. """
  326. nodes = []
  327. for module in self.blueprint.submodules:
  328. if isinstance(module, PGChildModule):
  329. if (
  330. manager is not None and
  331. module.backend_supported(manager, **kwargs)
  332. ):
  333. nodes.extend(module.get_nodes(**kwargs))
  334. else:
  335. nodes.extend(module.get_nodes(**kwargs))
  336. return nodes
  337. def children(self, **kwargs):
  338. """Build a list of treeview nodes from the child nodes."""
  339. if 'sid' not in kwargs:
  340. return precondition_required(
  341. gettext('Required properties are missing.')
  342. )
  343. from pgadmin.utils.driver import get_driver
  344. manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(
  345. sid=kwargs['sid']
  346. )
  347. did = None
  348. if 'did' in kwargs:
  349. did = kwargs['did']
  350. try:
  351. conn = manager.connection(did=did)
  352. if not conn.connected():
  353. status, msg = conn.connect()
  354. if not status:
  355. return internal_server_error(errormsg=msg)
  356. except (ConnectionLost, SSHTunnelConnectionLost, CryptKeyMissing):
  357. raise
  358. except Exception as e:
  359. return precondition_required(
  360. gettext(
  361. "Connection to the server has been lost."
  362. )
  363. )
  364. # Return sorted nodes based on label
  365. return make_json_response(
  366. data=sorted(
  367. self.get_children_nodes(manager, **kwargs),
  368. key=lambda c: c['label']
  369. )
  370. )
  371. def get_dependencies(self, conn, object_id, where=None,
  372. show_system_objects=None):
  373. """
  374. This function is used to fetch the dependencies for the selected node.
  375. Args:
  376. conn: Connection object
  377. object_id: Object Id of the selected node.
  378. where: where clause for the sql query (optional)
  379. Returns: Dictionary of dependencies for the selected node.
  380. """
  381. # Set the sql_path
  382. sql_path = 'depends/{0}/#{1}#'.format(
  383. conn.manager.server_type, conn.manager.version)
  384. if where is None:
  385. where_clause = "WHERE dep.objid={0}::oid".format(object_id)
  386. else:
  387. where_clause = where
  388. query = render_template("/".join([sql_path, 'dependencies.sql']),
  389. where_clause=where_clause)
  390. # fetch the dependency for the selected object
  391. dependencies = self.__fetch_dependency(conn, query,
  392. show_system_objects)
  393. # fetch role dependencies
  394. if where_clause.find('subid') < 0:
  395. sql = render_template(
  396. "/".join([sql_path, 'role_dependencies.sql']),
  397. where_clause=where_clause)
  398. status, result = conn.execute_dict(sql)
  399. if not status:
  400. current_app.logger.error(result)
  401. for row in result['rows']:
  402. ref_name = row['refname']
  403. dep_str = row['deptype']
  404. dep_type = ''
  405. if dep_str == 'a':
  406. dep_type = 'ACL'
  407. elif dep_str == 'o':
  408. dep_type = 'Owner'
  409. if row['refclassid'] == 1260:
  410. dependencies.append(
  411. {'type': 'role',
  412. 'name': ref_name,
  413. 'field': dep_type}
  414. )
  415. return dependencies
  416. def get_dependents(self, conn, object_id, where=None):
  417. """
  418. This function is used to fetch the dependents for the selected node.
  419. Args:
  420. conn: Connection object
  421. object_id: Object Id of the selected node.
  422. where: where clause for the sql query (optional)
  423. Returns: Dictionary of dependents for the selected node.
  424. """
  425. # Set the sql_path
  426. sql_path = 'depends/{0}/#{1}#'.format(
  427. conn.manager.server_type, conn.manager.version)
  428. if where is None:
  429. where_clause = "WHERE dep.refobjid={0}::oid".format(object_id)
  430. else:
  431. where_clause = where
  432. query = render_template("/".join([sql_path, 'dependents.sql']),
  433. where_clause=where_clause)
  434. # fetch the dependency for the selected object
  435. dependents = self.__fetch_dependency(conn, query)
  436. return dependents
  437. def __fetch_dependency(self, conn, query, show_system_objects=None):
  438. """
  439. This function is used to fetch the dependency for the selected node.
  440. Args:
  441. conn: Connection object
  442. query: sql query to fetch dependencies/dependents
  443. Returns: Dictionary of dependency for the selected node.
  444. """
  445. standard_types = {
  446. 'r': None,
  447. 'i': 'index',
  448. 'S': 'sequence',
  449. 'v': 'view',
  450. 'p': 'partition_table',
  451. 'f': 'foreign_table',
  452. 'm': 'materialized_view',
  453. 't': 'toast_table',
  454. 'I': 'partition_index'
  455. }
  456. # Dictionary for the object types
  457. custom_types = {
  458. 'x': 'external_table', 'n': 'schema', 'd': 'domain',
  459. 'l': 'language', 'Cc': 'check', 'Cd': 'domain_constraints',
  460. 'Cf': 'foreign_key', 'Cp': 'primary_key', 'Co': 'collation',
  461. 'Cu': 'unique_constraint', 'Cx': 'exclusion_constraint',
  462. 'Fw': 'foreign_data_wrapper', 'Fs': 'foreign_server',
  463. 'Fc': 'fts_configuration', 'Fp': 'fts_parser',
  464. 'Fd': 'fts_dictionary', 'Ft': 'fts_template',
  465. 'Ex': 'extension', 'Et': 'event_trigger', 'Pa': 'package',
  466. 'Pf': 'function', 'Pt': 'trigger_function', 'Pp': 'procedure',
  467. 'Rl': 'rule', 'Rs': 'row_security_policy', 'Sy': 'synonym',
  468. 'Ty': 'type', 'Tr': 'trigger', 'Tc': 'compound_trigger',
  469. # None specified special handling for this type
  470. 'A': None
  471. }
  472. # Merging above two dictionaries
  473. types = {**standard_types, **custom_types}
  474. # Dictionary for the restrictions
  475. dep_types = {
  476. # None specified special handling for this type
  477. 'n': 'normal',
  478. 'a': 'auto',
  479. 'i': None,
  480. 'p': None
  481. }
  482. status, result = conn.execute_dict(query)
  483. if not status:
  484. current_app.logger.error(result)
  485. dependency = list()
  486. for row in result['rows']:
  487. _ref_name = row['refname']
  488. type_str = row['type']
  489. dep_str = row['deptype']
  490. nsp_name = row['nspname']
  491. ref_name = ''
  492. if nsp_name is not None:
  493. ref_name = nsp_name + '.'
  494. type_name = ''
  495. icon = None
  496. # Fetch the type name from the dictionary
  497. # if type is not present in the types dictionary then
  498. # we will continue and not going to add it.
  499. if len(type_str) and type_str in types and \
  500. types[type_str] is not None:
  501. type_name = types[type_str]
  502. if type_str == 'Rl':
  503. ref_name = \
  504. _ref_name + ' ON ' + ref_name + row['ownertable']
  505. _ref_name = None
  506. elif type_str == 'Cf':
  507. ref_name += row['ownertable'] + '.'
  508. elif type_str == 'm':
  509. icon = 'icon-mview'
  510. elif len(type_str) and type_str[0] in types and \
  511. types[type_str[0]] is None:
  512. # if type is present in the types dictionary, but it's
  513. # value is None then it requires special handling.
  514. if type_str[0] == 'r':
  515. if int(type_str[1]) > 0:
  516. type_name = 'column'
  517. else:
  518. type_name = 'table'
  519. if 'is_inherits' in row and row['is_inherits'] == '1':
  520. if 'is_inherited' in row and \
  521. row['is_inherited'] == '1':
  522. icon = 'icon-table-multi-inherit'
  523. # For tables under partitioned tables,
  524. # is_inherits will be true and dependency
  525. # will be auto as it inherits from parent
  526. # partitioned table
  527. elif ('is_inherited' in row and
  528. row['is_inherited'] == '0') and \
  529. dep_str == 'a':
  530. type_name = 'partition'
  531. else:
  532. icon = 'icon-table-inherits'
  533. elif 'is_inherited' in row and \
  534. row['is_inherited'] == '1':
  535. icon = 'icon-table-inherited'
  536. elif type_str[0] == 'A':
  537. # Include only functions
  538. if row['adbin'].startswith('{FUNCEXPR'):
  539. type_name = 'function'
  540. ref_name = row['adsrc']
  541. else:
  542. continue
  543. else:
  544. continue
  545. if _ref_name is not None:
  546. ref_name += _ref_name
  547. dep_type = ''
  548. if show_system_objects is None:
  549. show_system_objects = self.blueprint.show_system_objects
  550. if dep_str[0] in dep_types:
  551. # if dep_type is present in the dep_types dictionary, but it's
  552. # value is None then it requires special handling.
  553. if dep_types[dep_str[0]] is None:
  554. if dep_str[0] == 'i':
  555. if show_system_objects:
  556. dep_type = 'internal'
  557. else:
  558. continue
  559. elif dep_str[0] == 'p':
  560. dep_type = 'pin'
  561. type_name = ''
  562. else:
  563. dep_type = dep_types[dep_str[0]]
  564. dependency.append(
  565. {
  566. 'type': type_name,
  567. 'name': ref_name,
  568. 'field': dep_type,
  569. 'icon': icon,
  570. }
  571. )
  572. return dependency
上海开阖软件有限公司 沪ICP备12045867号-1