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