From 3c027e6313d257e8ca3687f618ce931de2120295 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Tue, 24 Nov 2020 16:54:09 +0100
Subject: [PATCH 1/2] settings: add an xml storage for site options (#48751)

---
 tests/test_api.py       | 46 +++++++++++++++++++++++++++++++
 wcs/parameters.py       | 61 +++++++++++++++++++++++++++++++++++++++++
 wcs/qommon/publisher.py | 28 +++++++++++++++++--
 3 files changed, 133 insertions(+), 2 deletions(-)
 create mode 100644 wcs/parameters.py

diff --git a/tests/test_api.py b/tests/test_api.py
index a596b763..47bf02b6 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -293,16 +293,40 @@ def test_get_user(pub, local_user):
             '1234'
             )
     url = signed_url[len('http://example.net'):]
     output = get_app(pub).get(url)
     assert output.json['user_display_name'] == u'Jean Darmette'
     assert [x['name'] for x in output.json['user_roles']] == ['Foo bar']
     assert [x['slug'] for x in output.json['user_roles']] == ['foo-bar']
 
+    # authentication info using XML
+    open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w').write('')
+    signed_url = sign_url(
+            'http://example.net/api/user/?format=json&orig=coucou&email=%s' % urllib.quote(local_user.email),
+            '1234'
+            )
+    url = signed_url[len('http://example.net'):]
+    output = get_app(pub).get(url, status=403)
+    assert output.json['err_desc'] == 'invalid orig'
+    parameters = """<?xml version="1.0"?>
+<parameters>
+  <api>
+    <secrets>
+       <coucou>1234</coucou>
+    </secrets>
+  </api>
+</parameters>
+"""
+    with open(os.path.join(pub.app_dir, 'site-parameters.xml'), 'w') as fd:
+        fd.write(parameters)
+    url = signed_url[len('http://example.net'):]
+    output = get_app(pub).get(url)
+    assert output.json['user_display_name'] == u'Jean Darmette'
+
 
 def test_is_url_signed_check_nonce(pub, local_user, freezer):
     ORIG = 'xxx'
     KEY = 'xxx'
 
     pub.site_options.add_section('api-secrets')
     pub.site_options.set('api-secrets', ORIG, KEY)
     pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w'))
@@ -2776,16 +2800,38 @@ def test_api_ics_formdata_http_auth(pub, local_user, admin_user, ics_data):
     resp = app.get('/api/forms/test/ics/foobar?email=%s' % local_user.email, status=200)
     assert resp.headers['content-type'] == 'text/calendar; charset=utf-8'
     assert resp.text.count('BEGIN:VEVENT') == 10
 
     # check it fails with a different password
     app.authorization = ('Basic', ('user', 'password2'))
     resp = app.get('/api/forms/test/ics/foobar?email=%s' % local_user.email, status=401)
 
+    # add authentication info using XML
+    parameters = """<?xml version="1.0"?>
+<parameters>
+  <api>
+    <http>
+      <auth>
+        <ics>
+          <user>password2</user>
+        </ics>
+      </auth>
+    </http>
+  </api>
+</parameters>
+"""
+    with open(os.path.join(pub.app_dir, 'site-parameters.xml'), 'w') as fd:
+        fd.write(parameters)
+
+    # check it gets the data
+    resp = app.get('/api/forms/test/ics/foobar?email=%s' % local_user.email, status=200)
+    assert resp.headers['content-type'] == 'text/calendar; charset=utf-8'
+    assert resp.text.count('BEGIN:VEVENT') == 10
+
 
 def test_api_ics_formdata_custom_view(pub, local_user, ics_data):
     role = Role.select()[0]
 
     formdef = FormDef.get_by_urlname('test')
 
     pub.custom_view_class.wipe()
     custom_view = pub.custom_view_class()
diff --git a/wcs/parameters.py b/wcs/parameters.py
new file mode 100644
index 00000000..f821f187
--- /dev/null
+++ b/wcs/parameters.py
@@ -0,0 +1,61 @@
+# w.c.s. - web application for online forms
+# Copyright (C) 2005-2020  Entr'ouvert
+#
+# 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, see <http://www.gnu.org/licenses/>.
+
+from collections import OrderedDict
+import xml.etree.ElementTree as ET
+
+from quixote import get_publisher
+
+from wcs.qommon.xml_storage import XmlStorableObject
+
+
+class Parameters(XmlStorableObject):
+
+    def __init__(self):
+        self.parameters = OrderedDict([('options', OrderedDict())])
+        self.id = None
+
+    @classmethod
+    def import_from_xml_tree(cls, tree, include_id=False, charset=None, snapshot=False):
+        if charset is None:
+            charset = get_publisher().site_charset
+        obj = cls()
+
+        # if the tree we get is actually a ElementTree for real, we get its
+        # root element and go on happily.
+        if not ET.iselement(tree):
+            tree = tree.getroot()
+
+        # parse XML with uniq tags and without attributes
+        def etree_to_dict(element):
+            d = OrderedDict()
+            for child in element.getchildren():
+                d[child.tag.lower()] = etree_to_dict(child)
+            if not d:
+                return element.text
+            return d
+
+        obj.parameters = etree_to_dict(tree) or OrderedDict()
+        return obj
+
+    def get(self, option, section='options'):
+        d = self.parameters
+        for tag in section.split('-'):
+            d = d.get(tag)
+            if not d:
+                return None
+        value = d.get(option)
+        return value
diff --git a/wcs/qommon/publisher.py b/wcs/qommon/publisher.py
index c4c48afc..355d4eab 100644
--- a/wcs/qommon/publisher.py
+++ b/wcs/qommon/publisher.py
@@ -34,17 +34,16 @@ import locale
 import random
 import re
 import socket
 import sys
 import time
 import traceback
 
 import linecache
-import xml.etree.ElementTree as ET
 
 from django.conf import settings
 from django.http import Http404
 from django.utils import six
 from django.utils import translation
 from django.utils.encoding import force_text, force_bytes
 from django.utils.six import StringIO
 
@@ -86,16 +85,17 @@ class QommonPublisher(Publisher, object):
     unpickler_class = None
 
     after_login_url = ''
     qommon_static_dir = 'static/'
     qommon_admin_css = 'css/dc2/admin.css'
     default_theme = 'default'
 
     site_options = None
+    site_parameters = None
     site_charset = 'utf-8'
     missing_appdir_redirect = None
     use_sms_feature = True
 
     gettext = lambda self, message: message
     ngettext = lambda self, msgid1, msgid2, n: msgid1
 
     app_dir = None
@@ -373,17 +373,17 @@ class QommonPublisher(Publisher, object):
             self.load_site_options()
         try:
             return (self.site_options.get('options', option) == 'true')
         except ConfigParser.NoSectionError:
             return defaults.get(option, False)
         except ConfigParser.NoOptionError:
             return defaults.get(option, False)
 
-    def get_site_option(self, option, section='options'):
+    def _get_site_option(self, option, section='options'):
         defaults = {
             'options': {
                 'unused-files-behaviour': 'remove',
                 'relatable-hosts': '',
             },
         }
         if self.site_options is None:
             self.load_site_options()
@@ -400,19 +400,43 @@ class QommonPublisher(Publisher, object):
         storages = {}
         for section, definition in self.site_options.items():
             if section.startswith('storage-') and 'label' in definition and 'class' in definition:
                 storage_id = section[8:]
                 storages[storage_id] = dict(definition)
                 storages[storage_id]['id'] = storage_id
         return storages
 
+    def load_site_parameters(self):
+        from wcs.parameters import Parameters
+
+        if not self.site_parameters:
+            self.site_parameters = Parameters()
+        filename = os.path.join(self.app_dir, 'site-parameters.xml')
+        if not os.path.exists(filename):
+            return
+        fd = open(filename, 'rb')
+        try:
+            self.site_parameters = self.site_parameters.storage_load(fd)
+        except:
+            self.get_app_logger().error('failed to read site options XML file')
+            return
+
+    def get_site_parameter(self, option, section='options'):
+        if self.site_parameters is None:
+            self.load_site_parameters()
+        return self.site_parameters.get(option, section)
+
+    def get_site_option(self, option, section='options'):
+        return self.get_site_parameter(option, section) or self._get_site_option(option, section)
+
     def set_config(self, request = None):
         self.reload_cfg()
         self.site_options = None # reset at the beginning of a request
+        self.site_parameters = None
         debug_cfg = self.cfg.get('debug', {})
         self.logger.error_email = debug_cfg.get('error_email')
         self.config.display_exceptions = debug_cfg.get('display_exceptions')
         self.config.form_tokens = True
         self.config.session_cookie_httponly = True
         self.config.allowed_methods = ['GET', 'HEAD', 'POST', 'PUT']
 
         if request:
-- 
2.29.2

