1# vim: set ts=4 sw=4 et: coding=UTF-8 2# 3# Copyright (C) 2008, 2013 Novell, Inc. 4# Copyright (C) 2008, 2009, 2010, 2012, 2014 Red Hat, Inc. 5# Copyright (C) 2008, 2009, 2010, 2012, 2014 Tim Waugh <twaugh@redhat.com> 6# 7# Authors: Vincent Untz 8# 9# This program is free software; you can redistribute it and/or modify 10# it under the terms of the GNU General Public License as published by 11# the Free Software Foundation; either version 2 of the License, or 12# (at your option) any later version. 13# 14# This program is distributed in the hope that it will be useful, 15# but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17# GNU General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with this program; if not, write to the Free Software 21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 22# 23 24# check FIXME/TODO here 25# check FIXME/TODO in cups-pk-helper 26# define fine-grained policy (more than one level of permission) 27# add missing methods 28 29import os 30import sys 31 32import tempfile 33 34import cups 35import dbus 36from debug import debugprint 37 38from dbus.mainloop.glib import DBusGMainLoop 39from functools import reduce 40DBusGMainLoop(set_as_default=True) 41 42CUPS_PK_NAME = 'org.opensuse.CupsPkHelper.Mechanism' 43CUPS_PK_PATH = '/' 44CUPS_PK_IFACE = 'org.opensuse.CupsPkHelper.Mechanism' 45 46CUPS_PK_NEED_AUTH = 'org.opensuse.CupsPkHelper.Mechanism.NotPrivileged' 47 48 49# we can't subclass cups.Connection, even when adding 50# Py_TPFLAGS_BASETYPE to cupsconnection.c 51# So we'll hack this... 52class Connection: 53 def __init__(self, host, port, encryption): 54 self._parent = None 55 56 try: 57 self._session_bus = dbus.SessionBus() 58 self._system_bus = dbus.SystemBus() 59 except dbus.exceptions.DBusException: 60 # One or other bus not running. 61 self._session_bus = self._system_bus = None 62 63 self._connection = cups.Connection(host=host, 64 port=port, 65 encryption=encryption) 66 67 self._hack_subclass() 68 69 70 def _hack_subclass(self): 71 # here's how to subclass without really subclassing. Just provide 72 # the same methods 73 methodtype = type(self._connection.getPrinters) 74 for fname in dir(self._connection): 75 if fname[0] == '_': 76 continue 77 fn = getattr(self._connection, fname) 78 if type(fn) != methodtype: 79 continue 80 if not hasattr(self, fname): 81 setattr(self, fname, fn.__call__) 82 83 84 def set_parent(self, parent): 85 self._parent = parent 86 87 88 def _get_cups_pk(self): 89 try: 90 object = self._system_bus.get_object(CUPS_PK_NAME, CUPS_PK_PATH) 91 return dbus.Interface(object, CUPS_PK_IFACE) 92 except dbus.exceptions.DBusException: 93 # Failed to get object or interface. 94 return None 95 except AttributeError: 96 # No system D-Bus 97 return None 98 99 100 def _call_with_pk_and_fallback(self, use_fallback, pk_function_name, pk_args, fallback_function, *args, **kwds): 101 pk_function = None 102 # take signature from kwds if is provided 103 dbus_args_signature = kwds.pop('signature', None) 104 105 if not use_fallback: 106 cups_pk = self._get_cups_pk() 107 if cups_pk: 108 try: 109 pk_function = cups_pk.get_dbus_method(pk_function_name) 110 except dbus.exceptions.DBusException: 111 pass 112 113 if use_fallback or not pk_function: 114 return fallback_function(*args, **kwds) 115 116 pk_retval = 'PolicyKit communication issue' 117 118 while True: 119 try: 120 # FIXME: async call or not? 121 pk_retval = pk_function(*pk_args, signature = dbus_args_signature) 122 123 # if the PK call has more than one return values, we pop the 124 # first one as the error message 125 if type(pk_retval) == tuple: 126 retval = pk_retval[1:] 127 # if there's no error, then we can safely return what we 128 # got 129 if pk_retval[0] == '': 130 # if there's only one item left in the tuple, we don't 131 # want to return the tuple, but the item 132 if len(retval) == 1: 133 return retval[0] 134 else: 135 return retval 136 break 137 except dbus.exceptions.DBusException as e: 138 if e.get_dbus_name() == CUPS_PK_NEED_AUTH: 139 debugprint ("DBus exception: %s" % e.get_dbus_message ()) 140 raise cups.IPPError(cups.IPP_NOT_AUTHORIZED, 'pkcancel') 141 142 break 143 144 # The PolicyKit call did not work (either a PK-error and we got a dbus 145 # exception that wasn't handled, or an error in the mechanism itself) 146 if pk_retval != '': 147 debugprint ('PolicyKit call to %s did not work: %s' % 148 (pk_function_name, repr (pk_retval))) 149 return fallback_function(*args, **kwds) 150 151 152 def _args_to_tuple(self, types, *args): 153 retval = [ False ] 154 155 if len(types) != len(args): 156 retval[0] = True 157 # We do this to have the right length for the returned value 158 retval.extend(types) 159 return tuple(types) 160 161 exception = False 162 163 for i in range(len(types)): 164 if type(args[i]) != types[i]: 165 if types[i] == str and type(args[i]) == int: 166 # we accept a mix between int and str 167 retval.append(str(args[i])) 168 continue 169 elif types[i] == str and type(args[i]) == float: 170 # we accept a mix between float and str 171 retval.append(str(args[i])) 172 continue 173 elif types[i] == str and type(args[i]) == bool: 174 # we accept a mix between bool and str 175 retval.append(str(args[i])) 176 continue 177 elif types[i] == str and args[i] is None: 178 # None is an empty string for dbus 179 retval.append('') 180 continue 181 elif types[i] == list and type(args[i]) == tuple: 182 # we accept a mix between list and tuple 183 retval.append(list(args[i])) 184 continue 185 elif types[i] == list and args[i] is None: 186 # None is an empty list 187 retval.append([]) 188 continue 189 else: 190 exception = True 191 retval.append(args[i]) 192 193 retval[0] = exception 194 195 return tuple(retval) 196 197 198 def _kwds_to_vars(self, names, **kwds): 199 ret = [] 200 201 for name in names: 202 if name in kwds: 203 ret.append(kwds[name]) 204 else: 205 ret.append('') 206 207 return tuple(ret) 208 209 210# getPrinters 211# getDests 212# getClasses 213# getPPDs 214# getServerPPD 215# getDocument 216 217 218 def getDevices(self, *args, **kwds): 219 use_pycups = False 220 221 limit = 0 222 include_schemes = [] 223 exclude_schemes = [] 224 timeout = 0 225 226 if len(args) == 4: 227 (use_pycups, limit, include_schemes, exclude_schemes, timeout) = self._args_to_tuple([int, str, str, int], *args) 228 else: 229 if 'timeout' in kwds: 230 timeout = kwds['timeout'] 231 232 if 'limit' in kwds: 233 limit = kwds['limit'] 234 235 if 'include_schemes' in kwds: 236 include_schemes = kwds['include_schemes'] 237 238 if 'exclude_schemes' in kwds: 239 exclude_schemes = kwds['exclude_schemes'] 240 241 pk_args = (timeout, limit, include_schemes, exclude_schemes) 242 243 try: 244 result = self._call_with_pk_and_fallback(use_pycups, 245 'DevicesGet', pk_args, 246 self._connection.getDevices, 247 *args, **kwds) 248 except TypeError: 249 debugprint ("DevicesGet API exception; using old signature") 250 if 'timeout' in kwds: 251 use_pycups = True 252 253 # Convert from list to string 254 if len (include_schemes) > 0: 255 include_schemes = reduce (lambda x, y: x + "," + y, 256 include_schemes) 257 else: 258 include_schemes = "" 259 260 if len (exclude_schemes) > 0: 261 exclude_schemes = reduce (lambda x, y: x + "," + y, 262 exclude_schemes) 263 else: 264 exclude_schemes = "" 265 266 pk_args = (limit, include_schemes, exclude_schemes) 267 result = self._call_with_pk_and_fallback(use_pycups, 268 'DevicesGet', pk_args, 269 self._connection.getDevices, 270 *args, **kwds) 271 272 # return 'result' if fallback was called 273 if len (result.keys()) > 0 and type (result[list(result.keys())[0]]) == dict: 274 return result 275 276 result_str = {} 277 if result is not None: 278 for i in result.keys(): 279 if type(i) == dbus.String: 280 result_str[str(i)] = str(result[i]) 281 else: 282 result_str[i] = result[i] 283 284 # cups-pk-helper returns all devices in one dictionary. 285 # Keys of different devices are distinguished by ':n' postfix. 286 287 devices = {} 288 n = 0 289 postfix = ':' + str (n) 290 device_keys = [x for x in result_str.keys() if x.endswith(postfix)] 291 while len (device_keys) > 0: 292 293 device_uri = None 294 device_dict = {} 295 for i in device_keys: 296 key = i[:len(i) - len(postfix)] 297 if key != 'device-uri': 298 device_dict[key] = result_str[i] 299 else: 300 device_uri = result_str[i] 301 302 if device_uri is not None: 303 devices[device_uri] = device_dict 304 305 n += 1 306 postfix = ':' + str (n) 307 device_keys = [x for x in result_str.keys() if x.endswith(postfix)] 308 309 return devices 310 311 312# getJobs 313# getJobAttributes 314 315 def cancelJob(self, *args, **kwds): 316 (use_pycups, jobid) = self._args_to_tuple([int], *args) 317 pk_args = (jobid, ) 318 319 self._call_with_pk_and_fallback(use_pycups, 320 'JobCancel', pk_args, 321 self._connection.cancelJob, 322 *args, **kwds) 323 324 325# cancelAllJobs 326# authenticateJob 327 def setJobHoldUntil(self, *args, **kwds): 328 (use_pycups, jobid, job_hold_until) = self._args_to_tuple([int, str], *args) 329 pk_args = (jobid, job_hold_until, ) 330 331 self._call_with_pk_and_fallback(use_pycups, 332 'JobSetHoldUntil', pk_args, 333 self._connection.setJobHoldUntil, 334 *args, **kwds) 335 336 def restartJob(self, *args, **kwds): 337 (use_pycups, jobid) = self._args_to_tuple([int], *args) 338 pk_args = (jobid, ) 339 340 self._call_with_pk_and_fallback(use_pycups, 341 'JobRestart', pk_args, 342 self._connection.restartJob, 343 *args, **kwds) 344 345 def getFile(self, *args, **kwds): 346 ''' Keeping this as an alternative for the code. 347 We don't use it because it's not possible to know if the call was a 348 PK-one (and so we push the content of a temporary filename to fd or 349 file) or a non-PK-one (in which case nothing should be done). 350 351 filename = None 352 fd = None 353 file = None 354 if use_pycups: 355 if len(kwds) != 1: 356 use_pycups = True 357 elif kwds.has_key('filename'): 358 filename = kwds['filename'] 359 elif kwds.has_key('fd'): 360 fd = kwds['fd'] 361 elif kwds.has_key('file'): 362 file = kwds['file'] 363 else: 364 use_pycups = True 365 366 if fd or file: 367 ''' 368 369 file_object = None 370 fd = None 371 if len(args) == 2: 372 (use_pycups, resource, filename) = self._args_to_tuple([str, str], *args) 373 else: 374 (use_pycups, resource) = self._args_to_tuple([str], *args) 375 if 'filename' in kwds: 376 filename = kwds['filename'] 377 elif 'fd' in kwds: 378 fd = kwds['fd'] 379 elif 'file' in kwds: 380 file_object = kwds['file'] 381 else: 382 if not use_pycups: 383 raise TypeError() 384 else: 385 filename = None 386 387 if (not use_pycups) and (fd is not None or file_object is not None): 388 # Create the temporary file in /tmp to ensure that 389 # cups-pk-helper-mechanism is able to write to it. 390 (tmpfd, tmpfname) = tempfile.mkstemp(dir="/tmp") 391 os.close (tmpfd) 392 393 pk_args = (resource, tmpfname) 394 self._call_with_pk_and_fallback(use_pycups, 395 'FileGet', pk_args, 396 self._connection.getFile, 397 *args, **kwds) 398 399 tmpfd = os.open (tmpfname, os.O_RDONLY) 400 tmpfile = os.fdopen (tmpfd, 'rt') 401 tmpfile.seek (0) 402 403 if fd is not None: 404 os.lseek (fd, 0, os.SEEK_SET) 405 line = tmpfile.readline() 406 while line != '': 407 os.write (fd, line.encode('UTF-8')) 408 line = tmpfile.readline() 409 else: 410 file_object.seek (0) 411 line = tmpfile.readline() 412 while line != '': 413 file_object.write (line.encode('UTF-8')) 414 line = tmpfile.readline() 415 416 tmpfile.close () 417 os.remove (tmpfname) 418 else: 419 pk_args = (resource, filename) 420 421 self._call_with_pk_and_fallback(use_pycups, 422 'FileGet', pk_args, 423 self._connection.getFile, 424 *args, **kwds) 425 426 427 def putFile(self, *args, **kwds): 428 if len(args) == 2: 429 (use_pycups, resource, filename) = self._args_to_tuple([str, str], *args) 430 else: 431 (use_pycups, resource) = self._args_to_tuple([str], *args) 432 if 'filename' in kwds: 433 filename = kwds['filename'] 434 elif 'fd' in kwds: 435 fd = kwds['fd'] 436 elif 'file' in kwds: 437 file_object = kwds['file'] 438 else: 439 if not use_pycups: 440 raise TypeError() 441 else: 442 filename = None 443 444 if (not use_pycups) and (fd is not None or file_object is not None): 445 (tmpfd, tmpfname) = tempfile.mkstemp() 446 os.lseek (tmpfd, 0, os.SEEK_SET) 447 448 if fd is not None: 449 os.lseek (fd, 0, os.SEEK_SET) 450 buf = os.read (fd, 512) 451 while buf != '' and buf != b'': 452 os.write (tmpfd, buf) 453 buf = os.read (fd, 512) 454 else: 455 file_object.seek (0) 456 line = file_object.readline () 457 while line != '': 458 os.write (tmpfd, line) 459 line = file_object.readline () 460 461 os.close (tmpfd) 462 463 pk_args = (resource, tmpfname) 464 465 self._call_with_pk_and_fallback(use_pycups, 466 'FilePut', pk_args, 467 self._connection.putFile, 468 *args, **kwds) 469 470 os.remove (tmpfname) 471 else: 472 473 pk_args = (resource, filename) 474 475 self._call_with_pk_and_fallback(use_pycups, 476 'FilePut', pk_args, 477 self._connection.putFile, 478 *args, **kwds) 479 480 481 def addPrinter(self, *args, **kwds): 482 (use_pycups, name) = self._args_to_tuple([str], *args) 483 (filename, ppdname, info, location, device, ppd) = self._kwds_to_vars(['filename', 'ppdname', 'info', 'location', 'device', 'ppd'], **kwds) 484 485 need_unlink = False 486 if not ppdname and not filename and ppd: 487 (fd, filename) = tempfile.mkstemp (text=True) 488 ppd.writeFd(fd) 489 os.close(fd) 490 need_unlink = True 491 492 if filename and not ppdname: 493 pk_args = (name, device, filename, info, location) 494 self._call_with_pk_and_fallback(use_pycups, 495 'PrinterAddWithPpdFile', pk_args, 496 self._connection.addPrinter, 497 *args, **kwds) 498 if need_unlink: 499 os.unlink(filename) 500 else: 501 pk_args = (name, device, ppdname, info, location) 502 self._call_with_pk_and_fallback(use_pycups, 503 'PrinterAdd', pk_args, 504 self._connection.addPrinter, 505 *args, **kwds) 506 507 508 def setPrinterDevice(self, *args, **kwds): 509 (use_pycups, name, device) = self._args_to_tuple([str, str], *args) 510 pk_args = (name, device) 511 512 self._call_with_pk_and_fallback(use_pycups, 513 'PrinterSetDevice', pk_args, 514 self._connection.setPrinterDevice, 515 *args, **kwds) 516 517 518 def setPrinterInfo(self, *args, **kwds): 519 (use_pycups, name, info) = self._args_to_tuple([str, str], *args) 520 pk_args = (name, info) 521 522 self._call_with_pk_and_fallback(use_pycups, 523 'PrinterSetInfo', pk_args, 524 self._connection.setPrinterInfo, 525 *args, **kwds) 526 527 528 def setPrinterLocation(self, *args, **kwds): 529 (use_pycups, name, location) = self._args_to_tuple([str, str], *args) 530 pk_args = (name, location) 531 532 self._call_with_pk_and_fallback(use_pycups, 533 'PrinterSetLocation', pk_args, 534 self._connection.setPrinterLocation, 535 *args, **kwds) 536 537 538 def setPrinterShared(self, *args, **kwds): 539 (use_pycups, name, shared) = self._args_to_tuple([str, bool], *args) 540 pk_args = (name, shared) 541 542 self._call_with_pk_and_fallback(use_pycups, 543 'PrinterSetShared', pk_args, 544 self._connection.setPrinterShared, 545 *args, **kwds) 546 547 548 def setPrinterJobSheets(self, *args, **kwds): 549 (use_pycups, name, start, end) = self._args_to_tuple([str, str, str], *args) 550 pk_args = (name, start, end) 551 552 self._call_with_pk_and_fallback(use_pycups, 553 'PrinterSetJobSheets', pk_args, 554 self._connection.setPrinterJobSheets, 555 *args, **kwds) 556 557 558 def setPrinterErrorPolicy(self, *args, **kwds): 559 (use_pycups, name, policy) = self._args_to_tuple([str, str], *args) 560 pk_args = (name, policy) 561 562 self._call_with_pk_and_fallback(use_pycups, 563 'PrinterSetErrorPolicy', pk_args, 564 self._connection.setPrinterErrorPolicy, 565 *args, **kwds) 566 567 568 def setPrinterOpPolicy(self, *args, **kwds): 569 (use_pycups, name, policy) = self._args_to_tuple([str, str], *args) 570 pk_args = (name, policy) 571 572 self._call_with_pk_and_fallback(use_pycups, 573 'PrinterSetOpPolicy', pk_args, 574 self._connection.setPrinterOpPolicy, 575 *args, **kwds) 576 577 578 def setPrinterUsersAllowed(self, *args, **kwds): 579 (use_pycups, name, users) = self._args_to_tuple([str, list], *args) 580 pk_args = (name, users) 581 582 self._call_with_pk_and_fallback(use_pycups, 583 'PrinterSetUsersAllowed', pk_args, 584 self._connection.setPrinterUsersAllowed, 585 *args, **kwds) 586 587 588 def setPrinterUsersDenied(self, *args, **kwds): 589 (use_pycups, name, users) = self._args_to_tuple([str, list], *args) 590 pk_args = (name, users) 591 592 self._call_with_pk_and_fallback(use_pycups, 593 'PrinterSetUsersDenied', pk_args, 594 self._connection.setPrinterUsersDenied, 595 *args, **kwds) 596 597 def addPrinterOptionDefault(self, *args, **kwds): 598 # The values can be either a single string, or a list of strings, so 599 # we have to handle this 600 (use_pycups, name, option, value) = self._args_to_tuple([str, str, str], *args) 601 # success 602 if not use_pycups: 603 values = (value,) 604 # okay, maybe we directly have values 605 else: 606 (use_pycups, name, option, values) = self._args_to_tuple([str, str, list], *args) 607 pk_args = (name, option, values) 608 609 self._call_with_pk_and_fallback(use_pycups, 610 'PrinterAddOptionDefault', pk_args, 611 self._connection.addPrinterOptionDefault, 612 *args, **kwds) 613 614 615 def deletePrinterOptionDefault(self, *args, **kwds): 616 (use_pycups, name, option) = self._args_to_tuple([str, str], *args) 617 pk_args = (name, option) 618 619 self._call_with_pk_and_fallback(use_pycups, 620 'PrinterDeleteOptionDefault', pk_args, 621 self._connection.deletePrinterOptionDefault, 622 *args, **kwds) 623 624 625 def deletePrinter(self, *args, **kwds): 626 (use_pycups, name) = self._args_to_tuple([str], *args) 627 pk_args = (name,) 628 629 self._call_with_pk_and_fallback(use_pycups, 630 'PrinterDelete', pk_args, 631 self._connection.deletePrinter, 632 *args, **kwds) 633 634# getPrinterAttributes 635 636 def addPrinterToClass(self, *args, **kwds): 637 (use_pycups, printer, name) = self._args_to_tuple([str, str], *args) 638 pk_args = (name, printer) 639 640 self._call_with_pk_and_fallback(use_pycups, 641 'ClassAddPrinter', pk_args, 642 self._connection.addPrinterToClass, 643 *args, **kwds) 644 645 646 def deletePrinterFromClass(self, *args, **kwds): 647 (use_pycups, printer, name) = self._args_to_tuple([str, str], *args) 648 pk_args = (name, printer) 649 650 self._call_with_pk_and_fallback(use_pycups, 651 'ClassDeletePrinter', pk_args, 652 self._connection.deletePrinterFromClass, 653 *args, **kwds) 654 655 656 def deleteClass(self, *args, **kwds): 657 (use_pycups, name) = self._args_to_tuple([str], *args) 658 pk_args = (name,) 659 660 self._call_with_pk_and_fallback(use_pycups, 661 'ClassDelete', pk_args, 662 self._connection.deleteClass, 663 *args, **kwds) 664 665# getDefault 666 667 def setDefault(self, *args, **kwds): 668 (use_pycups, name) = self._args_to_tuple([str], *args) 669 pk_args = (name,) 670 671 self._call_with_pk_and_fallback(use_pycups, 672 'PrinterSetDefault', pk_args, 673 self._connection.setDefault, 674 *args, **kwds) 675 676# getPPD 677 678 def enablePrinter(self, *args, **kwds): 679 (use_pycups, name) = self._args_to_tuple([str], *args) 680 pk_args = (name, True) 681 682 self._call_with_pk_and_fallback(use_pycups, 683 'PrinterSetEnabled', pk_args, 684 self._connection.enablePrinter, 685 *args, **kwds) 686 687 688 def disablePrinter(self, *args, **kwds): 689 (use_pycups, name) = self._args_to_tuple([str], *args) 690 pk_args = (name, False) 691 692 self._call_with_pk_and_fallback(use_pycups, 693 'PrinterSetEnabled', pk_args, 694 self._connection.disablePrinter, 695 *args, **kwds) 696 697 698 def acceptJobs(self, *args, **kwds): 699 (use_pycups, name) = self._args_to_tuple([str], *args) 700 pk_args = (name, True, '') 701 702 self._call_with_pk_and_fallback(use_pycups, 703 'PrinterSetAcceptJobs', pk_args, 704 self._connection.acceptJobs, 705 *args, **kwds) 706 707 708 def rejectJobs(self, *args, **kwds): 709 (use_pycups, name) = self._args_to_tuple([str], *args) 710 (reason,) = self._kwds_to_vars(['reason'], **kwds) 711 pk_args = (name, False, reason) 712 713 self._call_with_pk_and_fallback(use_pycups, 714 'PrinterSetAcceptJobs', pk_args, 715 self._connection.rejectJobs, 716 *args, **kwds) 717 718 719# printTestPage 720 721 def adminGetServerSettings(self, *args, **kwds): 722 use_pycups = False 723 pk_args = () 724 725 result = self._call_with_pk_and_fallback(use_pycups, 726 'ServerGetSettings', pk_args, 727 self._connection.adminGetServerSettings, 728 *args, **kwds) 729 settings = {} 730 if result is not None: 731 for i in result.keys(): 732 if type(i) == dbus.String: 733 settings[str(i)] = str(result[i]) 734 else: 735 settings[i] = result[i] 736 737 return settings 738 739 740 def adminSetServerSettings(self, *args, **kwds): 741 (use_pycups, settings) = self._args_to_tuple([dict], *args) 742 pk_args = (settings,) 743 744 self._call_with_pk_and_fallback(use_pycups, 745 'ServerSetSettings', pk_args, 746 self._connection.adminSetServerSettings, 747 *args, **kwds) 748 749 750# getSubscriptions 751# createSubscription 752# getNotifications 753# cancelSubscription 754# renewSubscription 755# printFile 756# printFiles 757