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) 2014 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
18
19import configparser
20import glob
21import locale
22import os
23import pwd
24import stat
25
26from collections import (
27    namedtuple,
28    OrderedDict,
29    defaultdict)
30from itertools import (
31    chain,
32    accumulate)
33from locale import gettext as _
34
35from gi.repository import (
36    GdkPixbuf,
37    GLib,
38    GObject,
39    Gtk,
40    Pango)
41
42
43__license__ = 'GPL-3'
44__version__ = 'dev'
45__data_directory__ = '../data/'
46__config_path__ = 'lightdm/lightdm-gtk-greeter.conf'
47
48
49try:
50    from . installation_config import (
51        __version__,
52        __data_directory__,
53        __config_path__)
54except ImportError:
55    pass
56
57
58__all__ = [
59    'bool2string',
60    'C_',
61    'clamp',
62    'check_path_accessibility',
63    'DefaultValueDict',
64    'file_is_readable_by_greeter',
65    'get_config_path',
66    'get_data_path',
67    'get_greeter_version'
68    'get_markup_error',
69    'get_version',
70    'ModelRowEnum',
71    'NC_',
72    'pixbuf_from_file_scaled_down',
73    'set_image_from_path',
74    'show_message',
75    'SimpleEnum',
76    'SimpleDictWrapper',
77    'string2bool',
78    'TreeStoreDataWrapper',
79    'WidgetsEnum',
80    'WidgetsWrapper']
81
82
83def C_(context, message):
84    separator = '\x04'
85    message_with_context = '{}{}{}'.format(context, separator, message)
86    result = locale.gettext(message_with_context)
87    if separator in result:
88        return message
89    return result
90
91
92def NC_(context, message):
93    return message
94
95
96def get_data_path(*parts):
97    return os.path.abspath(os.path.join(os.path.dirname(__file__),
98                                        __data_directory__, *parts))
99
100
101def get_config_path():
102    return os.path.abspath(__config_path__)
103
104
105def get_version():
106    return __version__
107
108
109def get_greeter_version():
110    try:
111        return get_greeter_version._version
112    except AttributeError:
113        try:
114            get_greeter_version._version = int(os.getenv('GTK_GREETER_VERSION', '0x010900'), 16)
115        except ValueError:
116            get_greeter_version._version = 0x010900
117
118    return get_greeter_version._version
119
120
121def bool2string(value, skip_none=False):
122    if isinstance(value, str):
123        value = string2bool(value)
124    return 'true' if value else 'false' if not skip_none or value is not None else None
125
126
127def string2bool(value, fallback=False):
128    if isinstance(value, str):
129        if value in ('true', 'yes', '1'):
130            return True
131        if value in ('false', 'no', '0'):
132            return False
133    return fallback
134
135
136def show_message(**kwargs):
137    dialog = Gtk.MessageDialog(parent=Gtk.Window.list_toplevels()[0],
138                               buttons=Gtk.ButtonsType.CLOSE, **kwargs)
139    dialog.run()
140    dialog.destroy()
141
142
143def pixbuf_from_file_scaled_down(path, width, height):
144    pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
145    scale = max(pixbuf.props.width / width, pixbuf.props.height / height)
146
147    if scale > 1:
148        return GdkPixbuf.Pixbuf.scale_simple(pixbuf,
149                                             pixbuf.props.width / scale,
150                                             pixbuf.props.height / scale,
151                                             GdkPixbuf.InterpType.BILINEAR)
152    return pixbuf
153
154
155def set_image_from_path(image, path):
156    if not path or not os.path.isfile(path):
157        image.props.icon_name = 'unknown'
158    else:
159        try:
160            width, height = image.get_size_request()
161            if -1 in (width, height):
162                width, height = 64, 64
163            pixbuf = pixbuf_from_file_scaled_down(path, width, height)
164            image.set_from_pixbuf(pixbuf)
165            return True
166        except GLib.Error:
167            image.props.icon_name = 'file-broken'
168    return False
169
170
171def check_path_accessibility(path, file=True, executable=False):
172    """Return None  if file is readable by greeter and error message otherwise"""
173
174    # LP: #1709864, Support gtk-3.* themes
175    if "gtk-3.*" in path:
176        for x in range(0, 40):
177            if os.path.exists(path.replace("gtk-3.*", "gtk-3.%i" % x)):
178                path = path.replace("gtk-3.*", "gtk-3.%i" % x)
179                return check_path_accessibility(path, file, executable)
180
181    if not os.path.exists(path):
182        return _('File not found: {path}').format(path=path)
183
184    try:
185        uid, gids = check_path_accessibility.id_cached_data
186    except AttributeError:
187        files = glob.glob('/usr/local/etc/lightdm/lightdm.d/*.conf')
188        files += ['/usr/local/etc/lightdm/lightdm.conf']
189        config = configparser.RawConfigParser(strict=False)
190        config.read(files)
191        username = config.get('LightDM', 'greeter-user', fallback='lightdm')
192
193        pw = pwd.getpwnam(username)
194        uid = pw.pw_uid
195        gids = set(os.getgrouplist(username, pw.pw_gid))
196        check_path_accessibility.id_cached_data = uid, gids
197
198    parts = os.path.normpath(path).split(os.path.sep)
199    if not parts[0]:
200        parts[0] = os.path.sep
201
202    def check(p):
203        try:
204            st = os.stat(p)
205        except OSError as e:
206            return _('Failed to check permissions: {error}'.format(error=e.strerror))
207
208        if stat.S_ISDIR(st.st_mode) and not stat.S_IREAD:
209            return _('Directory is not readable: {path}'.format(path=p))
210        if st.st_uid == uid:
211            return not (st.st_mode & stat.S_IRUSR) and \
212                _('LightDM does not have permissions to read path: {path}'.format(path=p))
213        if st.st_gid in gids:
214            return not (st.st_mode & stat.S_IRGRP) and \
215                _('LightDM does not have permissions to read path: {path}'.format(path=p))
216        return not (st.st_mode & stat.S_IROTH) and \
217            _('LightDM does not have permissions to read path: {path}'.format(path=p))
218
219    errors = (check(p) for p in accumulate(parts, os.path.join))
220    error = next((error for error in errors if error), None)
221
222    if not error and file and not os.path.isfile(path):
223        return _('Path is not a regular file: {path}'.format(path=path))
224
225    if not error and executable:
226        st = os.stat(path)
227        if st.st_uid == uid:
228            if not st.st_mode & stat.S_IXUSR:
229                return _('LightDM does not have permissions to execute file: {path}'
230                         .format(path=path))
231        elif st.st_gid in gids:
232            if not st.st_mode & stat.S_IXGRP:
233                return _('LightDM does not have permissions to execute file: {path}'
234                         .format(path=path))
235        elif not st.st_mode & stat.S_IXOTH:
236            return _('LightDM does not have permissions to execute file: {path}'.format(path=path))
237
238    return error
239
240
241def get_markup_error(markup):
242    try:
243        Pango.parse_markup(markup, -1, '\0')
244    except GLib.Error as e:
245        return e.message
246    return None
247
248
249def clamp(v, a, b):
250    if v < a:
251        return a
252    if v > b:
253        return b
254    return v
255
256
257class DefaultValueDict(defaultdict):
258
259    def __init__(self, *items, default=None, factory=None, source=None):
260        super().__init__(None, source or items)
261        self._value = default
262        self._factory = factory
263
264    def __missing__(self, key):
265        return self._factory(key) if self._factory else self._value
266
267
268class SimpleEnumMeta(type):
269
270    @classmethod
271    def __prepare__(mcs, *args, **kwargs):
272        return OrderedDict()
273
274    def __new__(self, cls, bases, classdict):
275        obj = super().__new__(self, cls, bases, classdict)
276        obj._dict = OrderedDict((k, v)
277                                for k, v in classdict.items() if obj._accept_member_(k, v))
278        obj._tuple_type = namedtuple(obj.__class__.__name__ + 'Tuple', obj._dict.keys())
279        keys = list(obj._dict.keys())
280        for i in range(len(keys)):
281            if obj._dict[keys[i]] is ():
282                v = 0 if i == 0 else obj._dict[keys[i - 1]] + 1
283                setattr(obj, keys[i], v)
284                obj._dict[keys[i]] = v
285        return obj
286
287    def __contains__(self, value):
288        return value in self._dict.values()
289
290    def __iter__(self):
291        return iter(self._dict.values())
292
293    def _make(self, *args, **kwargs):
294        return self._tuple_type._make(self._imake(*args, **kwargs))
295
296    def _imake(self, *args, **kwargs):
297        if args:
298            return args
299        elif kwargs:
300            return (kwargs.get(k, v) for k, v in self._dict.items())
301        else:
302            return self._dict.values()
303
304
305class SimpleEnum(metaclass=SimpleEnumMeta):
306    _dict = None
307
308    def __init__(self, *args, **kwargs):
309        if kwargs:
310            self.__dict__.update(kwargs)
311        else:
312            self.__dict__.update((k, args[i]) for i, k in enumerate(self._dict))
313
314    def __iter__(self):
315        return (self.__dict__[k] for k in self._dict)
316
317    def __repr__(self):
318        return repr(tuple((k, self.__dict__[k]) for k in self._dict))
319
320    @classmethod
321    def _accept_member_(cls, name, value):
322        return not name.startswith('_') and not name.endswith('_')
323
324
325class WidgetsEnum(SimpleEnum):
326
327    def __init__(self, wrapper=None, builder=None):
328        getter = wrapper.__getitem__ if wrapper else builder.get_object
329        for k, v in self._dict.items():
330            if isinstance(v, type) and issubclass(v, WidgetsEnum):
331                self.__dict__[k] = v(WidgetsWrapper(wrapper or builder, k))
332            else:
333                self.__dict__[k] = getter(v or k)
334
335
336class WidgetsWrapper:
337    _builder = None
338    _prefixes = None
339
340    def __init__(self, source, *prefixes):
341        if source is None:
342            return
343        if isinstance(source, Gtk.Builder):
344            self._builder = source
345            self._prefixes = tuple(prefixes)
346        elif isinstance(source, WidgetsWrapper):
347            self._builder = source._builder
348            self._prefixes = source._prefixes + tuple(prefixes)
349        else:
350            raise TypeError(source)
351
352    def __getitem__(self, args):
353        if not self._builder:
354            return None
355        if not isinstance(args, tuple):
356            args = (args,)
357        return self._builder.get_object('_'.join(chain(self._prefixes, args)))
358
359    @property
360    def path(self):
361        return '_'.join(self._prefixes) if self._prefixes else ''
362
363
364class TreeStoreDataWrapper(GObject.Object):
365
366    def __init__(self, data):
367        super().__init__()
368        self.data = data
369
370
371class SimpleDictWrapper:
372
373    def __init__(self, getter=None, setter=None, add=None, deleter=None, itergetter=None):
374        self._getter = getter
375        self._setter = setter
376        self._deleter = deleter
377        self._itergetter = itergetter
378        self._add = add
379
380    def __getitem__(self, key):
381        return self._getter(key)
382
383    def __setitem__(self, key, value):
384        return self._setter(key, value)
385
386    def __delitem__(self, key):
387        return self._deleter(key)
388
389    def __iter__(self):
390        return self._itergetter()
391
392    def add(self, value):
393        return self._add(value)
394