1#!/usr/bin/env python3
2# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
3#   LightDM GTK Greeter Settings
4#   Copyright (C) 2015 Andrew P. <pan.pav.7c5@gmail.com>
5#
6#   This program is free software: you can redistribute it and/or modify it
7#   under the terms of the GNU General Public License version 3, as published
8#   by the Free Software Foundation.
9#
10#   This program is distributed in the hope that it will be useful, but
11#   WITHOUT ANY WARRANTY; without even the implied warranties of
12#   MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
13#   PURPOSE.  See the GNU General Public License for more details.
14#
15#   You should have received a copy of the GNU General Public License along
16#   with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18import configparser
19import os
20import sys
21from collections import OrderedDict
22from glob import iglob
23
24from gi.repository import GLib
25
26from lightdm_gtk_greeter_settings import helpers
27
28
29class Config:
30
31    class ConfigGroup:
32
33        def __init__(self, config):
34            self._config = config
35            self._items = OrderedDict()
36
37        def __iter__(self):
38            return iter(self._items)
39
40        def __contains__(self, item):
41            return item in self._items
42
43        def __getitem__(self, item):
44            values = self._items.get(item)
45            return values[-1][1] if values else None
46
47        def __setitem__(self, item, value):
48            if isinstance(value, tuple):
49                value, default = value
50            else:
51                default = None
52
53            values = self._items.get(item)
54
55            if values and values[-1][1] == value:
56                return
57
58            if values and values[-1][0] == self._config._output_path:
59                if len(values) > 1 and values[-2][1] == value:
60                    del values[-1]
61                elif default is not None and value == default and len(values) == 1:
62                    values.clear()
63                else:
64                    values[-1] = (self._config._output_path, value)
65            elif values is not None:
66                if default is None or value != default or (values and values[-1][1] != default):
67                    values.append((self._config._output_path, value))
68            else:
69                if default is None or value != default:
70                    self._items[item] = [(self._config._output_path, value)]
71
72        def __delitem__(self, item):
73            values = self._items.get(item)
74            if values is not None:
75                if values and values[-1][0] == self._config._output_path:
76                    del values[-1]
77                if not values:
78                    del self._items[item]
79
80    def __init__(self, base_dir='lightdm', base_name='lightdm-gtk-greeter.conf'):
81        self._base_dir = base_dir
82        self._base_name = base_name
83        self._output_path = helpers.get_config_path()
84        self._groups = OrderedDict()
85        self._key_values = helpers.SimpleDictWrapper(getter=self._get_key_values)
86
87    def read(self):
88        self._groups.clear()
89
90        pathes = []
91        pathes += GLib.get_system_data_dirs()
92        pathes += GLib.get_system_config_dirs()
93        pathes.append(os.path.dirname(os.path.dirname(self._output_path)))
94
95        files = []
96        for path in pathes:
97            files += sorted(iglob(os.path.join(path, self._base_dir,
98                                               self._base_name + '.d', '*.conf')))
99            files.append(os.path.join(path, self._base_dir, self._base_name))
100
101        for path in filter(os.path.isfile, files):
102            config_file = configparser.RawConfigParser(strict=False, allow_no_value=True)
103            try:
104                if not config_file.read(path):
105                    continue
106            except configparser.Error as e:
107                print(e, file=sys.stderr)
108                continue
109
110            for groupname, values in config_file.items():
111                if groupname == 'DEFAULT':
112                    continue
113
114                if groupname not in self._groups:
115                    self._groups[groupname] = Config.ConfigGroup(self)
116                group = self._groups[groupname]
117
118                for key, value in values.items():
119                    if value is None:
120                        print('[{group}] {key}: Keys without values are not allowed'.format(
121                            group=groupname, key=key), file=sys.stderr)
122                        continue
123                    if key.startswith('-'):
124                        key = key[1:]
125                        value = None
126
127                    if key in group._items:
128                        values = group._items[key]
129                        if value is not None or values:
130                            values.append((path, value))
131                    elif value is not None:
132                        group._items[key] = [(path, value)]
133
134    def write(self):
135        config_file = configparser.RawConfigParser(strict=False)
136
137        for groupname, group in self._groups.items():
138            config_section = None
139            for key, values in group._items.items():
140                if not values or values[-1][0] != self._output_path:
141                    continue
142
143                if values[-1][1] is not None or len(values) > 1:
144                    if not config_section:
145                        config_file.add_section(groupname)
146                        config_section = config_file[groupname]
147                    if values[-1][1] is None:
148                        config_section['-' + key] = ''
149                    else:
150                        config_section[key] = values[-1][1]
151
152        with open(self._output_path, 'w') as file:
153            config_file.write(file)
154
155    def is_writable(self):
156        if os.path.exists(self._output_path) and os.access(self._output_path, os.W_OK):
157            return True
158        return os.access(os.path.dirname(self._output_path), os.W_OK | os.X_OK)
159
160    def items(self):
161        return self._groups.items()
162
163    def allitems(self):
164        return ((g, k, items[k]) for (g, items) in self._groups.items() for k in items._items)
165
166    def add_group(self, name):
167        if name in self._groups:
168            return self._groups[name]
169        else:
170            return self._groups.setdefault(name, Config.ConfigGroup(self))
171
172    @property
173    def key_values(self):
174        return self._key_values
175
176    def _get_key_values(self, item):
177        group = self._groups.get(item[0])
178        if group:
179            values = group._items.get(item[1])
180            if values is not None:
181                return tuple(values)
182        return None
183
184    def __iter__(self):
185        return iter(self._groups)
186
187    def __getitem__(self, item):
188        if isinstance(item, tuple):
189            group = self._groups.get(item[0])
190            return group[item[1]] if group else None
191        return self._groups.get(item)
192
193    def __setitem__(self, item, value):
194        if isinstance(item, tuple):
195            if not item[0] in self._groups:
196                self._groups[item[0]] = Config.ConfigGroup(self)
197            self._groups[item[0]][item[1]] = value
198
199    def __delitem__(self, item):
200        if isinstance(item, tuple):
201            group = self._groups.get(item[0])
202            if group is not None:
203                del group[item[1]]
204            return
205
206        group = self._groups.get(item)
207        if group is not None:
208            if not group:
209                del self._groups[item]
210                return
211
212            keys_to_remove = []
213            for key, values in group._items.items():
214                if values[-1][0] == self._output_path:
215                    if len(values) == 1:
216                        keys_to_remove.append(key)
217                    else:
218                        values[-1] = (self._output_path, None)
219                elif values:
220                    values.append((self._output_path, None))
221
222            if len(keys_to_remove) < len(group._items):
223                for key in keys_to_remove:
224                    del group._items[key]
225            else:
226                del self._groups[item]
227