1#!/usr/local/bin/python3.8
2
3## Copyright (C) 2007, 2008, 2009, 2010, 2012, 2013, 2014 Red Hat, Inc.
4## Copyright (C) 2008 Novell, Inc.
5## Authors: Tim Waugh <twaugh@redhat.com>, Vincent Untz
6
7## This program is free software; you can redistribute it and/or modify
8## it under the terms of the GNU General Public License as published by
9## the Free Software Foundation; either version 2 of the License, or
10## (at your option) any later version.
11
12## This program is distributed in the hope that it will be useful,
13## but WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15## GNU General Public License for more details.
16
17## You should have received a copy of the GNU General Public License
18## along with this program; if not, write to the Free Software
19## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
21import cups
22import dbus
23from functools import reduce
24try:
25    gi.require_version('Gdk', '3.0')
26    from gi.repository import Gdk
27    gi.require_version('Gtk', '3.0')
28    from gi.repository import Gtk
29except:
30    pass
31import os
32import sys
33import tempfile
34import xml.etree.ElementTree
35
36import asyncipp
37from debug import *
38import debug
39
40CUPS_PK_NAME  = 'org.opensuse.CupsPkHelper.Mechanism'
41CUPS_PK_PATH  = '/'
42CUPS_PK_IFACE = 'org.opensuse.CupsPkHelper.Mechanism'
43CUPS_PK_NEED_AUTH = 'org.opensuse.CupsPkHelper.Mechanism.NotPrivileged'
44
45######
46###### A polkit-1 based asynchronous CupsPkHelper interface made to
47###### look just like a normal IPPAuthConnection class.  For method
48###### calls that have no equivalent in the CupsPkHelper API, IPP
49###### authentication is used over a CUPS connection in a separate
50###### thread.
51######
52
53_DevicesGet_uses_new_api = None
54
55###
56### A class to handle an asynchronous method call.
57###
58class _PK1AsyncMethodCall:
59    def __init__ (self, bus, conn, pk_method_name, pk_args,
60                  reply_handler, error_handler, unpack_fn,
61                  fallback_fn, args, kwds):
62        self._bus = bus
63        self._conn = conn
64        self._pk_method_name = pk_method_name
65        self._pk_args = pk_args
66        self._client_reply_handler = reply_handler
67        self._client_error_handler = error_handler
68        self._unpack_fn = unpack_fn
69        self._fallback_fn = fallback_fn
70        self._fallback_args = args
71        self._fallback_kwds = kwds
72        self._destroyed = False
73        debugprint ("+_PK1AsyncMethodCall: %s" % self)
74
75    def __del__ (self):
76        debug.debugprint ("-_PK1AsyncMethodCall: %s" % self)
77
78    def call (self):
79        object = self._bus.get_object(CUPS_PK_NAME, CUPS_PK_PATH)
80        proxy = dbus.Interface (object, CUPS_PK_IFACE)
81        pk_method = proxy.get_dbus_method (self._pk_method_name)
82
83        try:
84            debugprint ("%s: calling %s" % (self, pk_method))
85            pk_method (*self._pk_args,
86                        reply_handler=self._pk_reply_handler,
87                        error_handler=self._pk_error_handler,
88                        timeout=3600)
89        except TypeError as e:
90            debugprint ("Type error in PK call: %s" % repr (e))
91            self.call_fallback_fn ()
92
93    def _destroy (self):
94        debugprint ("DESTROY: %s" % self)
95        self._destroyed = True
96        del self._bus
97        del self._conn
98        del self._pk_method_name
99        del self._pk_args
100        del self._client_reply_handler
101        del self._client_error_handler
102        del self._unpack_fn
103        del self._fallback_fn
104        del self._fallback_args
105        del self._fallback_kwds
106
107    def _pk_reply_handler (self, error, *args):
108        if self._destroyed:
109            return
110
111        if str (error) == '':
112            try:
113                Gdk.threads_enter ()
114            except:
115                pass
116            debugprint ("%s: no error, calling reply handler %s" %
117                        (self, self._client_reply_handler))
118            self._client_reply_handler (self._conn, self._unpack_fn (*args))
119            try:
120                Gdk.threads_leave ()
121            except:
122                pass
123            self._destroy ()
124            return
125
126        debugprint ("PolicyKit method failed with: %s" % repr (error))
127        self.call_fallback_fn ()
128
129    def _pk_error_handler (self, exc):
130        if self._destroyed:
131            return
132
133        if exc.get_dbus_name () == CUPS_PK_NEED_AUTH:
134            exc = cups.IPPError (cups.IPP_NOT_AUTHORIZED, 'pkcancel')
135            try:
136                Gdk.threads_enter ()
137            except:
138                pass
139            debugprint ("%s: no auth, calling error handler %s" %
140                        (self, self._client_error_handler))
141            self._client_error_handler (self._conn, exc)
142            try:
143                Gdk.threads_leave ()
144            except:
145                pass
146            self._destroy ()
147            return
148
149        debugprint ("PolicyKit call to %s did not work: %s" %
150                    (self._pk_method_name, repr (exc)))
151        self.call_fallback_fn ()
152
153    def call_fallback_fn (self):
154        # Make the 'connection' parameter consistent with PK callbacks.
155        self._fallback_kwds["reply_handler"] = self._ipp_reply_handler
156        self._fallback_kwds["error_handler"] = self._ipp_error_handler
157        debugprint ("%s: calling %s" % (self, self._fallback_fn))
158        self._fallback_fn (*self._fallback_args, **self._fallback_kwds)
159
160    def _ipp_reply_handler (self, conn, *args):
161        if self._destroyed:
162            return
163
164        debugprint ("%s: chaining up to %s" % (self,
165                                               self._client_reply_handler))
166        self._client_reply_handler (self._conn, *args)
167        self._destroy ()
168
169    def _ipp_error_handler (self, conn, *args):
170        if self._destroyed:
171            return
172
173        debugprint ("%s: chaining up to %s" % (self,
174                                               self._client_error_handler))
175        self._client_error_handler (self._conn, *args)
176        self._destroy ()
177
178###
179### A class for handling FileGet when a temporary file is needed.
180###
181class _WriteToTmpFile:
182    def __init__ (self, kwds, reply_handler, error_handler):
183        self._reply_handler = reply_handler
184        self._error_handler = error_handler
185
186        # Create the temporary file in /tmp to ensure that
187        # cups-pk-helper-mechanism is able to write to it.
188        (tmpfd, tmpfname) = tempfile.mkstemp (dir="/tmp")
189        os.close (tmpfd)
190        self._filename = tmpfname
191        debugprint ("Created tempfile %s" % tmpfname)
192        self._kwds = kwds
193
194    def __del__ (self):
195        try:
196            os.unlink (self._filename)
197            debug.debugprint ("Removed tempfile %s" % self._filename)
198        except:
199            debug.debugprint ("No tempfile to remove")
200
201    def get_filename (self):
202        return self._filename
203
204    def reply_handler (self, conn, none):
205        tmpfd = os.open (self._filename, os.O_RDONLY)
206        tmpfile = os.fdopen (tmpfd, 'rt')
207        if "fd" in self._kwds:
208            fd = self._kwds["fd"]
209            os.lseek (fd, 0, os.SEEK_SET)
210            line = tmpfile.readline ()
211            while line != '':
212                os.write (fd, line.encode('UTF-8'))
213                line = tmpfile.readline ()
214        else:
215            file_object = self._kwds["file"]
216            file_object.seek (0)
217            line = tmpfile.readline ()
218            while line != '':
219                file_object.write (line.encode('UTF-8'))
220                line = tmpfile.readline ()
221
222        tmpfile.close ()
223        self._reply_handler (conn, none)
224
225    def error_handler (self, conn, exc):
226        self._error_handler (conn, exc)
227
228###
229### The user-visible class.
230###
231class PK1Connection:
232    def __init__(self, reply_handler=None, error_handler=None,
233                 host=None, port=None, encryption=None, parent=None):
234        self._conn = asyncipp.IPPAuthConnection  (reply_handler=reply_handler,
235                                                  error_handler=error_handler,
236                                                  host=host, port=port,
237                                                  encryption=encryption,
238                                                  parent=parent)
239
240        try:
241            self._system_bus = dbus.SystemBus()
242        except (dbus.exceptions.DBusException, AttributeError):
243            # No system D-Bus.
244            self._system_bus = None
245
246        global _DevicesGet_uses_new_api
247        if _DevicesGet_uses_new_api is None and self._system_bus:
248            try:
249                obj = self._system_bus.get_object(CUPS_PK_NAME, CUPS_PK_PATH)
250                proxy = dbus.Interface (obj, dbus.INTROSPECTABLE_IFACE)
251                api = proxy.Introspect ()
252                top = xml.etree.ElementTree.XML (api)
253                for interface in top.findall ("interface"):
254                    if interface.attrib.get ("name") != CUPS_PK_IFACE:
255                        continue
256
257                    for method in interface.findall ("method"):
258                        if method.attrib.get ("name") != "DevicesGet":
259                            continue
260
261                        num_args = 0
262                        for arg in method.findall ("arg"):
263                            direction = arg.attrib.get ("direction")
264                            if direction != "in":
265                                continue
266
267                            num_args += 1
268
269                        _DevicesGet_uses_new_api = num_args == 4
270                        debugprint ("DevicesGet new API: %s" % (num_args == 4))
271                        break
272
273                    break
274
275            except Exception as e:
276                debugprint ("Exception assessing DevicesGet API: %s" % repr (e))
277
278        methodtype = type (self._conn.getPrinters)
279        bindings = []
280        for fname in dir (self._conn):
281            if fname.startswith ('_'):
282                continue
283            fn = getattr (self._conn, fname)
284            if type (fn) != methodtype:
285                continue
286            if not hasattr (self, fname):
287                setattr (self, fname, self._make_binding (fn))
288                bindings.append (fname)
289
290        self._bindings = bindings
291        debugprint ("+%s" % self)
292
293    def __del__ (self):
294        debug.debugprint ("-%s" % self)
295
296    def _make_binding (self, fn):
297        def binding (*args, **kwds):
298            op = _PK1AsyncMethodCall (None, self, None, None,
299                                      kwds.get ("reply_handler"),
300                                      kwds.get ("error_handler"),
301                                      None, fn, args, kwds)
302            op.call_fallback_fn ()
303
304        return binding
305
306    def destroy (self):
307        debugprint ("DESTROY: %s" % self)
308        self._conn.destroy ()
309
310        for binding in self._bindings:
311            delattr (self, binding)
312
313    def _coerce (self, typ, val):
314        return typ (val)
315
316    def _args_kwds_to_tuple (self, types, params, args, kwds):
317        """Collapse args and kwds into a single tuple."""
318        leftover_kwds = kwds.copy ()
319        reply_handler = leftover_kwds.get ("reply_handler")
320        error_handler = leftover_kwds.get ("error_handler")
321        if "reply_handler" in leftover_kwds:
322            del leftover_kwds["reply_handler"]
323        if "error_handler" in leftover_kwds:
324            del leftover_kwds["error_handler"]
325        if "auth_handler" in leftover_kwds:
326            del leftover_kwds["auth_handler"]
327
328        result = [True, reply_handler, error_handler, ()]
329        if self._system_bus is None:
330            return result
331
332        tup = []
333        argindex = 0
334        for arg in args:
335            try:
336                val = self._coerce (types[argindex], arg)
337            except IndexError:
338                # More args than types.
339                kw, default = params[argindex]
340                if default != arg:
341                    return result
342
343                # It's OK, this is the default value anyway and can be
344                # ignored.  Skip to the next one.
345                argindex += 1
346                continue
347            except TypeError as e:
348                debugprint ("Error converting %s to %s" %
349                            (repr (arg), types[argindex]))
350                return result
351
352            tup.append (val)
353            argindex += 1
354
355        for kw, default in params[argindex:]:
356            if kw in leftover_kwds:
357                try:
358                    val = self._coerce (types[argindex], leftover_kwds[kw])
359                except TypeError as e:
360                    debugprint ("Error converting %s to %s" %
361                                (repr (leftover_kwds[kw]), types[argindex]))
362                    return result
363
364                tup.append (val)
365                del leftover_kwds[kw]
366            else:
367                tup.append (default)
368
369            argindex += 1
370
371        if leftover_kwds:
372            debugprint ("Leftover keywords: %s" % repr (list(leftover_kwds.keys ())))
373            return result
374
375        result[0] = False
376        result[3] = tuple (tup)
377        debugprint ("Converted %s/%s to %s" % (args, kwds, tuple (tup)))
378        return result
379
380    def _call_with_pk (self, use_pycups, pk_method_name, pk_args,
381                       reply_handler, error_handler, unpack_fn,
382                       fallback_fn, args, kwds):
383        asyncmethodcall = _PK1AsyncMethodCall (self._system_bus, self,
384                                               pk_method_name, pk_args,
385                                               reply_handler,
386                                               error_handler,
387                                               unpack_fn, fallback_fn,
388                                               args, kwds)
389
390        if not use_pycups:
391            try:
392                debugprint ("Calling PK method %s" % pk_method_name)
393                asyncmethodcall.call ()
394            except dbus.DBusException as e:
395                debugprint ("D-Bus call failed: %s" % repr (e))
396                use_pycups = True
397
398        if use_pycups:
399            return asyncmethodcall.call_fallback_fn ()
400
401    def _nothing_to_unpack (self):
402        return None
403
404    def getDevices (self, *args, **kwds):
405        global _DevicesGet_uses_new_api
406        if _DevicesGet_uses_new_api:
407            (use_pycups, reply_handler, error_handler,
408             tup) = self._args_kwds_to_tuple ([int, int, list, list],
409                                              [("timeout", 0),
410                                               ("limit", 0),
411                                               ("include_schemes", []),
412                                               ("exclude_schemes", [])],
413                                              args, kwds)
414        else:
415            (use_pycups, reply_handler, error_handler,
416             tup) = self._args_kwds_to_tuple ([int, list, list],
417                                              [("limit", 0),
418                                               ("include_schemes", []),
419                                               ("exclude_schemes", [])],
420                                              args, kwds)
421
422            if not use_pycups:
423                # Special handling for include_schemes/exclude_schemes.
424                # Convert from list to ","-separated string.
425                newtup = list (tup)
426                for paramindex in [1, 2]:
427                    if len (newtup[paramindex]) > 0:
428                        newtup[paramindex] = reduce (lambda x, y:
429                                                         x + "," + y,
430                                                     newtup[paramindex])
431                    else:
432                        newtup[paramindex] = ""
433
434                tup = tuple (newtup)
435
436        self._call_with_pk (use_pycups,
437                            'DevicesGet', tup, reply_handler, error_handler,
438                            self._unpack_getDevices_reply,
439                            self._conn.getDevices, args, kwds)
440
441    def _unpack_getDevices_reply (self, dbusdict):
442        result_str = dict()
443        for key, value in dbusdict.items ():
444            if type (key) == dbus.String:
445                result_str[str (key)] = str (value)
446            else:
447                result_str[key] = value
448
449        # cups-pk-helper returns all devices in one dictionary.
450        # Keys of different devices are distinguished by ':n' postfix.
451
452        devices = dict()
453        n = 0
454        affix = ':' + str (n)
455        device_keys = [x for x in result_str.keys () if x.endswith (affix)]
456        while len (device_keys) > 0:
457            device_uri = None
458            device_dict = dict()
459            for keywithaffix in device_keys:
460                key = keywithaffix[:len (keywithaffix) - len (affix)]
461                if key != 'device-uri':
462                    device_dict[key] = result_str[keywithaffix]
463                else:
464                    device_uri = result_str[keywithaffix]
465
466            if device_uri is not None:
467                devices[device_uri] = device_dict
468
469            n += 1
470            affix = ':' + str (n)
471            device_keys = [x for x in result_str.keys () if x.endswith (affix)]
472
473        return devices
474
475    def cancelJob (self, *args, **kwds):
476        (use_pycups, reply_handler, error_handler,
477         tup) = self._args_kwds_to_tuple ([int, bool],
478                                          [(None, None),
479                                           (None, False)], # purge_job
480                                          args, kwds)
481
482        self._call_with_pk (use_pycups,
483                            'JobCancelPurge', tup, reply_handler, error_handler,
484                            self._nothing_to_unpack,
485                            self._conn.cancelJob, args, kwds)
486
487    def setJobHoldUntil (self, *args, **kwds):
488        (use_pycups, reply_handler, error_handler,
489         tup) = self._args_kwds_to_tuple ([int, str],
490                                          [(None, None),
491                                           (None, None)],
492                                          args, kwds)
493
494        self._call_with_pk (use_pycups,
495                            'JobSetHoldUntil', tup, reply_handler,
496                            error_handler, self._nothing_to_unpack,
497                            self._conn.setJobHoldUntil, args, kwds)
498
499    def restartJob (self, *args, **kwds):
500        (use_pycups, reply_handler, error_handler,
501         tup) = self._args_kwds_to_tuple ([int],
502                                          [(None, None)],
503                                          args, kwds)
504
505        self._call_with_pk (use_pycups,
506                            'JobRestart', tup, reply_handler,
507                            error_handler, self._nothing_to_unpack,
508                            self._conn.restartJob, args, kwds)
509
510    def getFile (self, *args, **kwds):
511        (use_pycups, reply_handler, error_handler,
512         tup) = self._args_kwds_to_tuple ([str, str],
513                                          [("resource", None),
514                                           ("filename", None)],
515                                          args, kwds)
516
517        # getFile(resource, filename=None, fd=-1, file=None) -> None
518        if use_pycups:
519            if ((len (args) == 0 and 'resource' in kwds) or
520                (len (args) == 1)):
521                can_use_tempfile = True
522                for each in kwds.keys ():
523                    if each not in ['resource', 'fd', 'file',
524                                    'reply_handler', 'error_handler']:
525                        can_use_tempfile = False
526                        break
527
528                if can_use_tempfile:
529                    # We can still use PackageKit for this.
530                    if len (args) == 0:
531                        resource = kwds["resource"]
532                    else:
533                        resource = args[0]
534
535                    wrapper = _WriteToTmpFile (kwds,
536                                               reply_handler,
537                                               error_handler)
538                    self._call_with_pk (False,
539                                        'FileGet',
540                                        (resource, wrapper.get_filename ()),
541                                        wrapper.reply_handler,
542                                        wrapper.error_handler,
543                                        self._nothing_to_unpack,
544                                        self._conn.getFile, args, kwds)
545                    return
546
547        self._call_with_pk (use_pycups,
548                            'FileGet', tup, reply_handler,
549                            error_handler, self._nothing_to_unpack,
550                            self._conn.getFile, args, kwds)
551
552    ## etc
553    ## Still to implement:
554    ## putFile
555    ## addPrinter
556    ## setPrinterDevice
557    ## setPrinterInfo
558    ## setPrinterLocation
559    ## setPrinterShared
560    ## setPrinterJobSheets
561    ## setPrinterErrorPolicy
562    ## setPrinterOpPolicy
563    ## setPrinterUsersAllowed
564    ## setPrinterUsersDenied
565    ## addPrinterOptionDefault
566    ## deletePrinterOptionDefault
567    ## deletePrinter
568    ## addPrinterToClass
569    ## deletePrinterFromClass
570    ## deleteClass
571    ## setDefault
572    ## enablePrinter
573    ## disablePrinter
574    ## acceptJobs
575    ## rejectJobs
576    ## adminGetServerSettings
577    ## adminSetServerSettings
578    ## ...
579
580if __name__ == '__main__':
581    from gi.repository import GObject
582    from debug import set_debugging
583    set_debugging (True)
584    class UI:
585        def __init__ (self):
586            w = Gtk.Window ()
587            v = Gtk.VBox ()
588            w.add (v)
589            b = Gtk.Button.new_with_label ("Go")
590            v.pack_start (b, False, False, 0)
591            b.connect ("clicked", self.button_clicked)
592            b = Gtk.Button.new_with_label ("Fetch")
593            v.pack_start (b, False, False, 0)
594            b.connect ("clicked", self.fetch_clicked)
595            b.set_sensitive (False)
596            self.fetch_button = b
597            b = Gtk.Button.new_with_label ("Cancel job")
598            v.pack_start (b, False, False, 0)
599            b.connect ("clicked", self.cancel_clicked)
600            b.set_sensitive (False)
601            self.cancel_button = b
602            b = Gtk.Button.new_with_label ("Get file")
603            v.pack_start (b, False, False, 0)
604            b.connect ("clicked", self.get_file_clicked)
605            b.set_sensitive (False)
606            self.get_file_button = b
607            b = Gtk.Button.new_with_label ("Something harmless")
608            v.pack_start (b, False, False, 0)
609            b.connect ("clicked", self.harmless_clicked)
610            b.set_sensitive (False)
611            self.harmless_button = b
612            w.connect ("destroy", self.destroy)
613            w.show_all ()
614            self.conn = None
615            debugprint ("+%s" % self)
616
617        def __del__ (self):
618            debug.debugprint ("-%s" % self)
619
620        def destroy (self, window):
621            debugprint ("DESTROY: %s" % self)
622            try:
623                self.conn.destroy ()
624                del self.conn
625            except AttributeError:
626                pass
627
628            Gtk.main_quit ()
629
630        def button_clicked (self, button):
631            if self.conn:
632                self.conn.destroy ()
633
634            self.conn = PK1Connection (reply_handler=self.connected,
635                                       error_handler=self.connection_error)
636
637        def connected (self, conn, result):
638            print("Connected")
639            self.fetch_button.set_sensitive (True)
640            self.cancel_button.set_sensitive (True)
641            self.get_file_button.set_sensitive (True)
642            self.harmless_button.set_sensitive (True)
643
644        def connection_error (self, conn, error):
645            print("Failed to connect")
646            raise error
647
648        def fetch_clicked (self, button):
649            print ("fetch devices...")
650            self.conn.getDevices (reply_handler=self.got_devices,
651                                  error_handler=self.get_devices_error)
652
653        def got_devices (self, conn, devices):
654            if conn != self.conn:
655                print("Ignoring stale reply")
656                return
657
658            print("got devices: %s" % devices)
659
660        def get_devices_error (self, conn, exc):
661            if conn != self.conn:
662                print("Ignoring stale error")
663                return
664
665            print("devices error: %s" % repr (exc))
666
667        def cancel_clicked (self, button):
668            print("Cancel job...")
669            self.conn.cancelJob (1,
670                                 reply_handler=self.job_canceled,
671                                 error_handler=self.cancel_job_error)
672
673        def job_canceled (self, conn, none):
674            if conn != self.conn:
675                print("Ignoring stale reply for %s" % conn)
676                return
677
678            print("Job canceled")
679
680        def cancel_job_error (self, conn, exc):
681            if conn != self.conn:
682                print("Ignoring stale error for %s" % conn)
683                return
684
685            print("cancel error: %s" % repr (exc))
686
687        def get_file_clicked (self, button):
688            self.my_file = open ("cupsd.conf", "w")
689            self.conn.getFile ("/admin/conf/cupsd.conf", file=self.my_file,
690                               reply_handler=self.got_file,
691                               error_handler=self.get_file_error)
692
693        def got_file (self, conn, none):
694            if conn != self.conn:
695                print("Ignoring stale reply for %s" % conn)
696                return
697
698            print("Got file")
699
700        def get_file_error (self, conn, exc):
701            if conn != self.conn:
702                print("Ignoring stale error")
703                return
704
705            print("get file error: %s" % repr (exc))
706
707        def harmless_clicked (self, button):
708            self.conn.getJobs (reply_handler=self.got_jobs,
709                               error_handler=self.get_jobs_error)
710
711        def got_jobs (self, conn, result):
712            if conn != self.conn:
713                print("Ignoring stale reply from %s" % repr (conn))
714                return
715            print(result)
716
717        def get_jobs_error (self, exc):
718            print("get jobs error: %s" % repr (exc))
719
720    UI ()
721    from dbus.mainloop.glib import DBusGMainLoop
722    DBusGMainLoop (set_as_default=True)
723    Gtk.main ()
724