From 091780f7dd0392bd7e427a1bec83ecc8c943e250 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 30 Dec 2015 05:57:41 +0500 Subject: [PATCH] This pull request adds new functionality to the CouchPotato. The main target is **settings**. After applying this PL the CP will be able to work with *ro*, *rw* and *hidden* options and *hidden* sections. The behavior of common options is defined by internal meta-options, which you might add to the config file. These meta-options define whether the particular option will be visible/editable in the web interface. There is full backward compability. Example of usage. Lets consider this `.couchpotato/settings.conf`: ```ini [core] username = cp **username_internal_meta = ro** ssl_key = proxy_server = [updater] **section_hidden_internal_meta = True** notification = 0 ... ``` In this example option `core.username` will be immutable in the web UI, since there is meta-option `username_internal_meta`, set to value **ro** (means *read-only*). The second thing: we have made the *updater* section invisible, so you couldn't see its config values in the web interface. Allowed values for options in `.couchpotato/settings.conf` file: * \_internal_meta = ( **hidden** | **ro** | **rw** ) * section_hidden_internal_meta = ( **True** | **False** ) --- couchpotato/core/settings.py | 160 ++++++++++++++++++++++-- couchpotato/static/scripts/combined.base.min.js | 75 +++++++---- couchpotato/static/scripts/page/settings.js | 82 ++++++++---- couchpotato/static/style/combined.min.css | 85 ++++++------- 4 files changed, 299 insertions(+), 103 deletions(-) diff --git a/couchpotato/core/settings.py b/couchpotato/core/settings.py index ffc142a..b0e2932 100644 --- a/couchpotato/core/settings.py +++ b/couchpotato/core/settings.py @@ -77,7 +77,8 @@ class Settings(object): return self.p def sections(self): - return self.p.sections() + res = filter( self.isSectionReadable, self.p.sections()) + return res def connectEvents(self): addEvent('settings.options', self.addOptions) @@ -106,9 +107,21 @@ class Settings(object): self.save() def set(self, section, option, value): + if not self.isOptionWritable(section, option): + self.log.warning('set::option "%s.%s" isn\'t writable', (section, option)) + return None + if self.isOptionMeta(section, option): + self.log.warning('set::option "%s.%s" cancelled, since it is a META option', (section, option)) + return None + + return self.p.set(section, option, value) def get(self, option = '', section = 'core', default = None, type = None): + if self.isOptionMeta(section, option): + self.log.warning('set::option "%s.%s" cancelled, since it is a META option', (section, option)) + return None + try: try: type = self.types[section][option] @@ -123,6 +136,14 @@ class Settings(object): return default def delete(self, option = '', section = 'core'): + if not self.isOptionWritable(section, option): + self.log.warning('delete::option "%s.%s" isn\'t writable', (section, option)) + return None + + if self.isOptionMeta(section, option): + self.log.warning('set::option "%s.%s" cancelled, since it is a META option', (section, option)) + return None + self.p.remove_option(section, option) self.save() @@ -153,11 +174,30 @@ class Settings(object): def getValues(self): values = {} + + # TODO : There is two commented "continue" blocks (# COMMENTED_SKIPPING). They both are good... + # ... but, they omit output of values of hidden and non-readable options + # Currently, such behaviour could break the Web UI of CP... + # So, currently this two blocks are commented (but they are required to + # provide secure hidding of options. for section in self.sections(): + + # COMMENTED_SKIPPING + #if not self.isSectionReadable(section): + # continue + values[section] = {} for option in self.p.items(section): (option_name, option_value) = option + #skip meta options: + if self.isOptionMeta(section, option_name): + continue + + # COMMENTED_SKIPPING + #if not self.isOptionReadable(section, option_name): + # continue + is_password = False try: is_password = self.types[section][option_name] == 'password' except: pass @@ -189,14 +229,52 @@ class Settings(object): self.types[section][option] = type def addOptions(self, section_name, options): - + # no additional actions (related to ro-rw options) are required here if not self.options.get(section_name): self.options[section_name] = options else: self.options[section_name] = mergeDicts(self.options[section_name], options) def getOptions(self): - return self.options + """Returns dict of UI-readable options + + To check, whether the option is readable self.isOptionReadable() is used + """ + + res = {} + + if isinstance(self.options, dict): + for section_key in self.options.keys(): + section = self.options[section_key] + section_name = section.get('name') if 'name' in section else section_key + if self.isSectionReadable(section_name) and isinstance(section, dict): + s = {} + sg = [] + for section_field in section: + if section_field.lower() != 'groups': + s[section_field] = section[section_field] + else: + groups = section['groups'] + for group in groups: + g = {} + go = [] + for group_field in group: + if group_field.lower() != 'options': + g[group_field] = group[group_field] + else: + for option in group[group_field]: + option_name = option.get('name') + if self.isOptionReadable(section_name, option_name): + go.append(option) + option['writable'] = self.isOptionWritable(section_name, option_name) + if len(go)>0: + g['options'] = go + sg.append(g) + if len(sg)>0: + s['groups'] = sg + res[section_key] = s + + return res def view(self, **kwargs): return { @@ -210,19 +288,75 @@ class Settings(object): option = kwargs.get('name') value = kwargs.get('value') - # See if a value handler is attached, use that as value - new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True) + if not self.isOptionWritable(section, option): + self.log.warning('Option "%s.%s" isn\'t writable', (section, option)) + return { + 'success' : False, + } + else: + # See if a value handler is attached, use that as value + new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True) - self.set(section, option, (new_value if new_value else value).encode('unicode_escape')) - self.save() + self.set(section, option, (new_value if new_value else value).encode('unicode_escape')) + self.save() - # After save (for re-interval etc) - fireEvent('setting.save.%s.%s.after' % (section, option), single = True) - fireEvent('setting.save.%s.*.after' % section, single = True) + # After save (for re-interval etc) + fireEvent('setting.save.%s.%s.after' % (section, option), single = True) + fireEvent('setting.save.%s.*.after' % section, single = True) - return { - 'success': True, - } + return { + 'success': True, + } + + # unreachable code: + return None + + def isSectionReadable(self, section): + meta = 'section_hidden' + self.optionMetaSuffix() + try: + return not self.p.getboolean(section, meta) + except: pass + + # by default - every section is readable: + return True + + def isOptionReadable(self, section, option): + meta = option + self.optionMetaSuffix() + if self.p.has_option(section, meta): + meta_v = self.p.get(section, meta).lower() + return (meta_v == 'rw') or (meta_v == 'ro') + + # by default - all is writable: + return True + + def optionReadableCheckAndWarn(self, section, option): + x = self.isOptionReadable(section, option) + if not x: + self.log.warning('Option "%s.%s" isn\'t readable', (section, option)) + return x + + def isOptionWritable(self, section, option): + meta = option + self.optionMetaSuffix() + if self.p.has_option(section, meta): + return self.p.get(section, meta).lower() == 'rw' + + # by default - all is writable: + return True + + def optionMetaSuffix(self): + return '_internal_meta' + + def isOptionMeta(self, section, option): + """ A helper method for detecting internal-meta options in the ini-file + + For a meta options used following names: + * section_hidden_internal_meta = (True | False) - for section visibility + *