From 6c6b77a0cdc4980221b6ca57c78677f613b1a878 Mon Sep 17 00:00:00 2001
From: Julien Muchembled <jm@nexedi.com>
Date: Mon, 3 Jul 2017 21:52:55 +0200
Subject: [PATCH] Prevent ERP5 site creation if SQL databases aren't empty, add
 an option to force

/reviewed-on https://lab.nexedi.com/nexedi/erp5/merge_requests/314
---
 product/ERP5/ERP5Site.py                   | 62 ++++++++++----------
 product/ERP5/dtml/addERP5Site.dtml         |  4 ++
 product/ERP5/tests/testERP5Site.py         | 67 ++++++++++++++++++++++
 product/ERP5Type/tests/ERP5TypeTestCase.py | 27 +++++----
 4 files changed, 115 insertions(+), 45 deletions(-)
 create mode 100644 product/ERP5/tests/testERP5Site.py

diff --git a/product/ERP5/ERP5Site.py b/product/ERP5/ERP5Site.py
index 0818fddd54..9956347cc7 100644
--- a/product/ERP5/ERP5Site.py
+++ b/product/ERP5/ERP5Site.py
@@ -73,6 +73,7 @@ def manage_addERP5Site(self,
                        cmf_activity_sql_connection_string='test test',
                        light_install=0,
                        reindex=1,
+                       sql_reset=0,
                        RESPONSE=None):
   '''
   Adds a portal instance.
@@ -87,7 +88,8 @@ def manage_addERP5Site(self,
                  cmf_activity_sql_connection_string,
                  create_activities=create_activities,
                  light_install=light_install,
-                 reindex=reindex)
+                 reindex=reindex,
+                 sql_reset=sql_reset)
   gen.setupDefaultProperties(p,
                              title,
                              description,
@@ -1967,43 +1969,37 @@ class ERP5Generator(PortalGenerator):
     if not p.hasObject('portal_catalog'):
       addTool('ERP5 Catalog', None)
 
-    if 1:
-    # Add Default SQL connection
-      if not p.hasObject('erp5_sql_connection'):
-        addSQLConnection = p.manage_addProduct['ZMySQLDA'].\
-                                     manage_addZMySQLConnection
-        addSQLConnection('erp5_sql_connection',
-                         'ERP5 SQL Server Connection',
-                         p.erp5_sql_connection_string)
-
-    # Add Deferred SQL Connections
-      if not p.hasObject('erp5_sql_deferred_connection'):
-        addSQLConnection = p.manage_addProduct['ZMySQLDA'].\
-            manage_addZMySQLConnection
-        addSQLConnection('erp5_sql_deferred_connection',
-                         'ERP5 SQL Server Deferred Connection',
-                         p.erp5_sql_deferred_connection_string,
-                         deferred=True)
-
-    # Add Activity SQL Connections
-      if not p.hasObject('cmf_activity_sql_connection'):
-        addSQLConnection = p.manage_addProduct['CMFActivity'].\
-                                     manage_addActivityConnection
-        addSQLConnection('cmf_activity_sql_connection',
-                         'CMF Activity SQL Server Connection',
-                         p.cmf_activity_sql_connection_string)
-      # Warning : This transactionless connection is created with
+    sql_reset = kw.get('sql_reset', 0)
+    def addSQLConnection(id, title, **kw):
+      if p.hasObject(id):
+        return
+      # Warning : The transactionless connection is created with
       # the activity connection string and not the catalog's because
       # it's not compatible with the hot reindexing feature.
       # Though, it has nothing to do with activities.
       # The only difference compared to activity connection is the
       # minus prepended to the connection string.
-      if not p.hasObject('erp5_sql_transactionless_connection'):
-        addSQLConnection = p.manage_addProduct['ZMySQLDA'].\
-                                     manage_addZMySQLConnection
-        addSQLConnection('erp5_sql_transactionless_connection',
-                         'ERP5 Transactionless SQL Server Connection',
-                         '-%s' % p.cmf_activity_sql_connection_string)
+      if id == 'erp5_sql_transactionless_connection':
+        connection_string = '-' + p.cmf_activity_sql_connection_string
+      else:
+        connection_string = getattr(p, id + '_string')
+      manage_add(id, title, connection_string, **kw)
+      if not sql_reset and p[id]().tables():
+        raise Exception("Database %r is not empty." % connection_string)
+
+    # Add Z MySQL Connections
+    manage_add = p.manage_addProduct['ZMySQLDA'].manage_addZMySQLConnection
+    addSQLConnection('erp5_sql_connection',
+                     'ERP5 SQL Server Connection')
+    addSQLConnection('erp5_sql_deferred_connection',
+                     'ERP5 SQL Server Deferred Connection',
+                     deferred=True)
+    addSQLConnection('erp5_sql_transactionless_connection',
+                     'ERP5 Transactionless SQL Server Connection')
+    # Add Activity SQL Connections
+    manage_add = p.manage_addProduct['CMFActivity'].manage_addActivityConnection
+    addSQLConnection('cmf_activity_sql_connection',
+                     'CMF Activity SQL Server Connection')
 
     # Add ERP5Form Tools
     addERP5Tool(p, 'portal_selections', 'Selection Tool')
diff --git a/product/ERP5/dtml/addERP5Site.dtml b/product/ERP5/dtml/addERP5Site.dtml
index 51d500b48f..b6d01e3abc 100644
--- a/product/ERP5/dtml/addERP5Site.dtml
+++ b/product/ERP5/dtml/addERP5Site.dtml
@@ -97,6 +97,10 @@ label {
       <label class="form-label" for="cmf_activity_sql_connection_string">CMF Activity Database</label>
       <input class="form-element" name="cmf_activity_sql_connection_string" id="cmf_activity_sql_connection_string" type="text" size="40" value="test test"/>
     </div>
+    <div>
+      <label class="form-label" for="sql_reset">Drop any existing table</label>
+      <input type="checkbox" name="sql_reset:int" value="1" id="sql_reset"/>
+    </div>
     <div>
       <p>The connection strings used for Z MySQL Database Connections are of the form:</p>
       <blockquote><code>database[@host[:port]] [user [password [unix_socket]]]</code></blockquote>
diff --git a/product/ERP5/tests/testERP5Site.py b/product/ERP5/tests/testERP5Site.py
new file mode 100644
index 0000000000..47dc41efc2
--- /dev/null
+++ b/product/ERP5/tests/testERP5Site.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2014 Nexedi SA and Contributors. All Rights Reserved.
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsibility of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly advised to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+##############################################################################
+
+import unittest
+from Products.ERP5Type.tests.ERP5TypeTestCase import (
+  ERP5TypeTestCase, failed_portal_installation)
+
+class TestERP5Site(ERP5TypeTestCase):
+
+  def getPortalName(self):
+    return self._testMethodName
+
+  def setUp(self):
+    pass
+
+  def test_00_fillSQL(self):
+    """
+    A empty test that is run first, only to make sure that the other test
+    tries to create an ERP5 Site with a non-empty SQL database.
+    """
+    super(TestERP5Site, self).setUp()
+    self.portal
+    self.assertNotIn(self.getPortalName(), failed_portal_installation)
+
+  def test_01_do_not_wipe_SQL_data_by_default(self):
+    """
+    Unless wanted explicitely by the user, creating an ERP5 Site must fail
+    if the given SQL parameters give access to a database that already contains
+    data. This prevents existing SQL databases from being wiped mistakenly.
+    """
+    kw = self._getSiteCreationParameterDict()
+    del kw['sql_reset']
+    self._getSiteCreationParameterDict = lambda: kw
+    self.assertRaisesRegexp(Exception, "not empty",
+      super(TestERP5Site, self).setUp)
+    self.assertFalse(hasattr(self, 'portal'))
+    self.assertIn(self.getPortalName(), failed_portal_installation)
+
+def test_suite():
+  suite = unittest.TestSuite()
+  suite.addTest(unittest.makeSuite(TestERP5Site))
+  return suite
diff --git a/product/ERP5Type/tests/ERP5TypeTestCase.py b/product/ERP5Type/tests/ERP5TypeTestCase.py
index 03a4280fae..66551b9e08 100644
--- a/product/ERP5Type/tests/ERP5TypeTestCase.py
+++ b/product/ERP5Type/tests/ERP5TypeTestCase.py
@@ -211,7 +211,6 @@ DateTime._parse_args = _parse_args
 class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase):
     """Mixin class for ERP5 based tests.
     """
-
     def dummy_test(self):
       ZopeTestCase._print('All tests are skipped when --save option is passed '
                           'with --update_business_templates or without --load')
@@ -1041,6 +1040,18 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin):
         if not quiet:
           ZopeTestCase._print('done (%.3fs)\n' % (time.time() - start))
 
+    def _getSiteCreationParameterDict(self):
+      kw = _getConnectionStringDict()
+      # manage_addERP5Site does not accept the following 2 arguments
+      for k in ('erp5_sql_deferred_connection_string',
+                'erp5_sql_transactionless_connection_string'):
+        kw.pop(k, None)
+      email_from_address = os.environ.get('email_from_address')
+      if email_from_address is not None:
+        kw['email_from_address'] = email_from_address
+      kw['sql_reset'] = 1
+      return kw
+
     def setUpERP5Site(self,
                      business_template_list=(),
                      quiet=0,
@@ -1092,23 +1103,15 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin):
               if not quiet:
                 ZopeTestCase._print('Adding %s ERP5 Site ... ' % portal_name)
 
-              extra_constructor_kw = _getConnectionStringDict()
-              # manage_addERP5Site does not accept the following 2 arguments
-              for k in ('erp5_sql_deferred_connection_string',
-                        'erp5_sql_transactionless_connection_string'):
-                extra_constructor_kw.pop(k, None)
-              email_from_address = os.environ.get('email_from_address')
-              if email_from_address is not None:
-                extra_constructor_kw['email_from_address'] = email_from_address
-
+              kw = self._getSiteCreationParameterDict()
               factory = app.manage_addProduct['ERP5']
               factory.manage_addERP5Site(portal_name,
                                        erp5_catalog_storage=erp5_catalog_storage,
                                        light_install=light_install,
                                        reindex=reindex,
                                        create_activities=create_activities,
-                                       **extra_constructor_kw )
-              sql = extra_constructor_kw.get('erp5_sql_connection_string')
+                                       **kw)
+              sql = kw.get('erp5_sql_connection_string')
               if sql:
                 app[portal_name]._setProperty('erp5_site_global_id',
                                               base64.standard_b64encode(sql))
-- 
2.30.9