From 64d52cf85fc38a474c63ff36e2f100e304f27478 Mon Sep 17 00:00:00 2001
From: Adrian Torres <atorresj@hotmail.be>
Date: Wed, 11 Oct 2017 12:03:05 +0200
Subject: [PATCH] [IMP] base_import_module: Add layer of security (#19175)

Previous to this commit, anyone could import studio customizations into
a community db, this was troublesome since some people would just
install studio, do their customizations and then move them into a
community db, not paying anything at all.

This commit fixes this by checking whether the exported views were
created with studio or not and if studio is installed
---
 addons/base_import_module/models/ir_module.py | 51 +++++++++++++++++--
 1 file changed, 46 insertions(+), 5 deletions(-)

diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py
index 5bad5ea53ebb..a28ee6bf429b 100644
--- a/addons/base_import_module/models/ir_module.py
+++ b/addons/base_import_module/models/ir_module.py
@@ -1,6 +1,8 @@
 # -*- coding: utf-8 -*-
+import ast
 import base64
 import logging
+import lxml
 import os
 import sys
 import zipfile
@@ -32,8 +34,17 @@ class IrModule(models.Model):
         values = self.get_values_from_terp(terp)
 
         unmet_dependencies = set(terp['depends']).difference(installed_mods)
+
         if unmet_dependencies:
-            raise UserError(_("Unmet module dependencies: %s") % ', '.join(unmet_dependencies))
+            if _is_studio_custom(path):
+                err = _("Studio customizations require Studio")
+            else:
+                err = _("Unmet module dependencies: %s") % ', '.join(
+                    unmet_dependencies,
+                )
+            raise UserError(err)
+        elif 'web_studio' not in installed_mods and _is_studio_custom(path):
+            raise UserError(_("Studio customizations require Studio"))
 
         mod = known_mods_names.get(module)
         if mod:
@@ -52,7 +63,7 @@ class IrModule(models.Model):
                     continue
                 _logger.info("module %s: loading %s", module, filename)
                 noupdate = False
-                if filename.endswith('.csv') and kind in ('init', 'init_xml'):
+                if ext == '.csv' and kind in ('init', 'init_xml'):
                     noupdate = True
                 pathname = opj(path, filename)
                 idref = {}
@@ -101,9 +112,9 @@ class IrModule(models.Model):
                     raise UserError(_("File '%s' exceed maximum allowed file size") % zf.filename)
 
             with tempdir() as module_dir:
-                import odoo.modules as addons
+                import odoo.modules.module as module
                 try:
-                    addons.module.ad_paths.append(module_dir)
+                    module.ad_paths.append(module_dir)
                     z.extractall(module_dir)
                     dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))]
                     for mod_name in dirs:
@@ -117,8 +128,38 @@ class IrModule(models.Model):
                             _logger.exception('Error while importing module')
                             errors[mod_name] = exception_to_unicode(e)
                 finally:
-                    addons.module.ad_paths.remove(module_dir)
+                    module.ad_paths.remove(module_dir)
         r = ["Successfully imported module '%s'" % mod for mod in success]
         for mod, error in errors.items():
             r.append("Error while importing module '%s': %r" % (mod, error))
         return '\n'.join(r), module_names
+
+
+def _is_studio_custom(path):
+    """
+    Checks the to-be-imported records to see if there are any references to
+    studio, which would mean that the module was created using studio
+
+    Returns True if any of the records contains a context with the key
+    studio in it, False if none of the records do
+    """
+    path = os.path.join(path, 'data')
+    filenames = next(iter(os.walk(path)))[2]
+    filenames = [f for f in filenames if f.lower().endswith('.xml')]
+
+    for filename in filenames:
+        root = lxml.etree.parse(os.path.join(path, filename)).getroot()
+
+        for record in root:
+            # there might not be a context if it's a non-studio module
+            try:
+                # ast.literal_eval is like eval(), but safer
+                # context is a string representing a python dict
+                ctx = ast.literal_eval(record.get('context'))
+                # there are no cases in which studio is false
+                # so just checking for its existence is enough
+                if ctx and ctx.get('studio'):
+                    return True
+            except Exception:
+                continue
+    return False
-- 
GitLab