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.

174 lines
6.9 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. #
  4. # Authors: Guewen Baconnier
  5. # Copyright 2015 Camptocamp SA
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as
  9. # published by the Free Software Foundation, either version 3 of the
  10. # License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. #
  21. import re
  22. import psycopg2
  23. from openerp.osv import orm, fields
  24. from openerp.tools.translate import _
  25. # views are created with a prefix to prevent conflicts
  26. SQL_VIEW_PREFIX = u'sql_view_'
  27. # 63 chars is the length of a postgres identifier
  28. USER_NAME_SIZE = 63 - len(SQL_VIEW_PREFIX)
  29. PG_NAME_RE = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I)
  30. class SQLView(orm.Model):
  31. _name = 'sql.view'
  32. _description = 'SQL Views'
  33. def _complete_from_sql_name(self, cr, uid, sql_name, context=None):
  34. return SQL_VIEW_PREFIX + (sql_name or '')
  35. def _compute_complete_sql_name(self, cr, uid, ids, name, args,
  36. context=None):
  37. res = {}
  38. for sql_view in self.browse(cr, uid, ids, context=context):
  39. res[sql_view.id] = self._complete_from_sql_name(cr, uid,
  40. sql_view.sql_name,
  41. context=context)
  42. return res
  43. _columns = {
  44. 'name': fields.char(string='View Name', required=True),
  45. 'sql_name': fields.char(string='SQL Name', required=True,
  46. size=USER_NAME_SIZE,
  47. help="Name of the view. Will be prefixed "
  48. "by %s" % (SQL_VIEW_PREFIX,)),
  49. 'complete_sql_name': fields.function(_compute_complete_sql_name,
  50. string='Complete SQL Name',
  51. readonly=True,
  52. type='char'),
  53. 'definition': fields.text(string='Definition', required=True),
  54. }
  55. def _check_sql_name(self, cr, uid, ids, context=None):
  56. records = self.browse(cr, uid, ids, context=context)
  57. return all(PG_NAME_RE.match(record.sql_name) for record in records)
  58. def _check_definition(self, cr, uid, ids, context=None):
  59. """ Forbid a SQL definition with unbalanced parenthesis.
  60. The reason is that the view's definition will be enclosed in:
  61. CREATE VIEW {view_name} AS ({definition})
  62. The parenthesis around the definition prevent users to inject
  63. and execute arbitrary queries after the SELECT part (by using a
  64. semicolon). However, it would still be possible to craft a
  65. definition like the following which would close the parenthesis
  66. and start a new query:
  67. SELECT * FROM res_users); DELETE FROM res_users WHERE id IN (1
  68. This is no longer possible if we ensure that we don't have an
  69. unbalanced closing parenthesis.
  70. """
  71. for record in self.browse(cr, uid, ids, context=context):
  72. balanced = 0
  73. for char in record.definition:
  74. if char == '(':
  75. balanced += 1
  76. elif char == ')':
  77. balanced -= 1
  78. if balanced == -1:
  79. return False
  80. return True
  81. _constraints = [
  82. (_check_sql_name,
  83. 'The SQL name is not a valid PostgreSQL identifier',
  84. ['sql_name']),
  85. (_check_definition,
  86. 'This SQL definition is not allowed',
  87. ['definition']),
  88. ]
  89. _sql_constraints = [
  90. ('sql_name_uniq', 'unique (sql_name)',
  91. 'Another view has the same SQL name.')
  92. ]
  93. def _sql_view_comment(self, cr, uid, sql_view, context=None):
  94. return "%s (created by the module sql_view)" % sql_view.name
  95. def _create_or_replace_sql_view(self, cr, uid, sql_view, context=None):
  96. view_name = sql_view.complete_sql_name
  97. try:
  98. # The parenthesis around the definition are important:
  99. # they prevent to insert a semicolon and another query after
  100. # the view declaration
  101. cr.execute(
  102. "CREATE VIEW {view_name} AS ({definition})".format(
  103. view_name=view_name,
  104. definition=sql_view.definition)
  105. )
  106. except psycopg2.ProgrammingError as err:
  107. raise orm.except_orm(
  108. _('Error'),
  109. _('The definition of the view is not correct:\n\n%s') % (err,)
  110. )
  111. comment = self._sql_view_comment(cr, uid, sql_view, context=context)
  112. cr.execute(
  113. "COMMENT ON VIEW {view_name} IS %s".format(view_name=view_name),
  114. (comment,)
  115. )
  116. def _delete_sql_view(self, cr, uid, sql_view, context=None):
  117. view_name = sql_view.complete_sql_name
  118. cr.execute("DROP view IF EXISTS %s" % (view_name,))
  119. def create(self, cr, uid, vals, context=None):
  120. record_id = super(SQLView, self).create(cr, uid, vals,
  121. context=context)
  122. record = self.browse(cr, uid, record_id, context=context)
  123. self._create_or_replace_sql_view(cr, uid, record, context=context)
  124. return record_id
  125. def write(self, cr, uid, ids, vals, context=None):
  126. # If the name has been changed, we have to drop the view,
  127. # it will be created with the new name.
  128. # If the definition have been changed, we better have to delete
  129. # it and create it again otherwise we can have 'cannot drop
  130. # columns from view' errors.
  131. for record in self.browse(cr, uid, ids, context=context):
  132. self._delete_sql_view(cr, uid, record, context=context)
  133. result = super(SQLView, self).write(cr, uid, ids, vals,
  134. context=context)
  135. for record in self.browse(cr, uid, ids, context=context):
  136. self._create_or_replace_sql_view(cr, uid, record,
  137. context=context)
  138. return result
  139. def unlink(self, cr, uid, ids, context=None):
  140. for record in self.browse(cr, uid, ids, context=context):
  141. self._delete_sql_view(cr, uid, record, context=context)
  142. result = super(SQLView, self).unlink(cr, uid, ids, context=context)
  143. return result
  144. def onchange_sql_name(self, cr, uid, ids, sql_name, context=None):
  145. complete_name = self._complete_from_sql_name(cr, uid, sql_name,
  146. context=context)
  147. return {'value': {'complete_sql_name': complete_name}}