#!/usr/local/bin/python2.7

## Copyright (C) 2007, 2008, 2009, 2010, 2011 Red Hat, Inc.
## Copyright (C) 2008 Novell, Inc.
## Author: Tim Waugh <twaugh@redhat.com>

## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.

## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.

## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

import threading
import config
import cups
import gobject
import gtk
import Queue

cups.require ("1.9.60")

import authconn
from debug import *
import debug
from gettext import gettext as _

######
###### An asynchronous libcups API using IPP with a separate worker
###### thread.
######

###
### This is the worker thread.
###
class _IPPConnectionThread(threading.Thread):
    def __init__ (self, queue, conn, reply_handler=None, error_handler=None,
                  auth_handler=None, user=None, host=None, port=None,
                  encryption=None):
                  
        threading.Thread.__init__ (self)
        self.setDaemon (True)
        self._queue = queue
        self._conn = conn
        self.host = host
        self.port = port
        self._encryption = encryption
        self._reply_handler = reply_handler
        self._error_handler = error_handler
        self._auth_handler = auth_handler
        self._auth_queue = Queue.Queue (1)
        self.user = user
        self._destroyed = False
        debugprint ("+%s" % self)

    def __del__ (self):
        debug.debugprint ("-%s" % self)

    def set_auth_info (self, password):
        self._auth_queue.put (password)

    def run (self):
        if self.host == None:
            self.host = cups.getServer ()
        if self.port == None:
            self.port = cups.getPort ()
        if self._encryption == None:
            self._encryption = cups.getEncryption ()

        if self.user:
            cups.setUser (self.user)
        else:
            self.user = cups.getUser ()

        cups.setPasswordCB2 (self._auth)

        try:
            conn = cups.Connection (host=self.host,
                                    port=self.port,
                                    encryption=self._encryption)
            self._reply (None)
        except RuntimeError, e:
            conn = None
            self._error (e)

        while True:
            # Wait to find out what operation to try.
            debugprint ("Awaiting further instructions")
            self.idle = self._queue.empty ()
            item = self._queue.get ()
            debugprint ("Next task: %s" % repr (item))
            if item == None:
                # Our signal to quit.
                self._queue.task_done ()
                break

            self.idle = False
            (fn, args, kwds, rh, eh, ah) = item
            if rh != False:
                self._reply_handler = rh
            if eh != False:
                self._error_handler = eh
            if ah != False:
                self._auth_handler = ah

            if fn == True:
                # Our signal to change user and reconnect.
                self.user = args[0]
                cups.setUser (self.user)
                debugprint ("Set user=%s; reconnecting..." % self.user)
                cups.setPasswordCB2 (self._auth)

                try:
                    conn = cups.Connection (host=self.host,
                                            port=self.port,
                                            encryption=self._encryption)
                    debugprint ("...reconnected")

                    self._queue.task_done ()
                    self._reply (None)
                except RuntimeError, e:
                    debugprint ("...failed")
                    self._queue.task_done ()
                    self._error (e)

                continue

            # Normal IPP operation.  Try to perform it.
            try:
                debugprint ("Call %s" % fn)
                result = fn (conn, *args, **kwds)
                if fn == cups.Connection.adminGetServerSettings.__call__:
                    # Special case for a rubbish bit of API.
                    if result == {}:
                        # Authentication failed, but we aren't told that.
                        raise cups.IPPError (cups.IPP_NOT_AUTHORIZED, '')

                debugprint ("...success")
                self._reply (result)
            except Exception, e:
                debugprint ("...failure")
                self._error (e)

            self._queue.task_done ()

        debugprint ("Thread exiting")
        self._destroyed = True
        del self._conn # already destroyed
        del self._reply_handler
        del self._error_handler
        del self._auth_handler
        del self._queue
        del self._auth_queue
        del conn

        cups.setPasswordCB2 (None)

    def _auth (self, prompt, conn=None, method=None, resource=None):
        def prompt_auth (prompt):
            gtk.gdk.threads_enter ()
            if conn == None:
                self._auth_handler (prompt, self._conn)
            else:
                self._auth_handler (prompt, self._conn, method, resource)

            gtk.gdk.threads_leave ()
            return False

        if self._auth_handler == None:
            return ""

        gobject.idle_add (prompt_auth, prompt)
        password = self._auth_queue.get ()
        return password

    def _reply (self, result):
        def send_reply (handler, result):
            if not self._destroyed:
                gtk.gdk.threads_enter ()
                handler (self._conn, result)
                gtk.gdk.threads_leave ()
            return False

        if not self._destroyed and self._reply_handler:
            gobject.idle_add (send_reply, self._reply_handler, result)

    def _error (self, exc):
        def send_error (handler, exc):
            if not self._destroyed:
                gtk.gdk.threads_enter ()
                handler (self._conn, exc)
                gtk.gdk.threads_leave ()
            return False

        if not self._destroyed and self._error_handler:
            debugprint ("Add %s to idle" % self._error_handler)
            gobject.idle_add (send_error, self._error_handler, exc)

###
### This is the user-visible class.  Although it does not inherit from
### cups.Connection it implements the same functions.
###
class IPPConnection:
    """
    This class starts a new thread to handle IPP operations.

    Each IPP operation method takes optional reply_handler,
    error_handler and auth_handler parameters.

    If an operation requires a password to proceed, the auth_handler
    function will be called.  The operation will continue once
    set_auth_info (in this class) is called.

    Once the operation has finished either reply_handler or
    error_handler will be called.
    """

    def __init__ (self, reply_handler=None, error_handler=None,
                  auth_handler=None, user=None, host=None, port=None,
                  encryption=None, parent=None):
        debugprint ("New IPPConnection")
        self._parent = parent
        self.queue = Queue.Queue ()
        self.thread = _IPPConnectionThread (self.queue, self,
                                            reply_handler=reply_handler,
                                            error_handler=error_handler,
                                            auth_handler=auth_handler,
                                            user=user, host=host, port=port,
                                            encryption=encryption)
        self.thread.start ()

        methodtype = type (cups.Connection.getPrinters)
        bindings = []
        for fname in dir (cups.Connection):
            if fname[0] == ' ':
                continue
            fn = getattr (cups.Connection, fname)
            if type (fn) != methodtype:
                continue
            setattr (self, fname, self._make_binding (fn))
            bindings.append (fname)

        self.bindings = bindings
        debugprint ("+%s" % self)

    def __del__ (self):
        debug.debugprint ("-%s" % self)

    def destroy (self):
        debugprint ("DESTROY: %s" % self)
        for binding in self.bindings:
            delattr (self, binding)

        if self.thread.isAlive ():
            gobject.timeout_add_seconds (1, self._reap_thread)

    def _reap_thread (self):
        if self.thread.idle:
            debugprint ("Putting None on the task queue")
            self.queue.put (None)
            self.queue.join ()
            return False

        debugprint ("Thread %s still processing tasks" % self.thread)
        return True

    def set_auth_info (self, password):
        """Call this from your auth_handler function."""
        self.thread.set_auth_info (password)

    def reconnect (self, user, reply_handler=None, error_handler=None):
        debugprint ("Reconnect...")
        self.queue.put ((True, (user,), {},
                         reply_handler, error_handler, False))

    def _make_binding (self, fn):
        return lambda *args, **kwds: self._call_function (fn, *args, **kwds)

    def _call_function (self, fn, *args, **kwds):
        reply_handler = error_handler = auth_handler = False
        if kwds.has_key ("reply_handler"):
            reply_handler = kwds["reply_handler"]
            del kwds["reply_handler"]
        if kwds.has_key ("error_handler"):
            error_handler = kwds["error_handler"]
            del kwds["error_handler"]
        if kwds.has_key ("auth_handler"):
            auth_handler = kwds["auth_handler"]
            del kwds["auth_handler"]

        self.queue.put ((fn, args, kwds,
                         reply_handler, error_handler, auth_handler))

######
###### An asynchronous libcups API with graphical authentication and
###### retrying.
######

###
### A class to take care of an individual operation.
###
class _IPPAuthOperation:
    def __init__ (self, reply_handler, error_handler, conn,
                  user=None, fn=None, args=None, kwds=None):
        self._auth_called = False
        self._dialog_shown = False
        self._use_password = ''
        self._cancel = False
        self._reconnect = False
        self._reconnected = False
        self._user = user
        self._conn = conn
        self._try_as_root = self._conn.try_as_root
        self._client_fn = fn
        self._client_args = args
        self._client_kwds = kwds
        self._client_reply_handler = reply_handler
        self._client_error_handler = error_handler

        debugprint ("+%s" % self)

    def __del__ (self):
        debug.debugprint ("-%s" % self)

    def _destroy (self):
        del self._conn
        del self._client_fn
        del self._client_args
        del self._client_kwds
        del self._client_reply_handler
        del self._client_error_handler

    def error_handler (self, conn, exc):
        if self._client_fn == None:
            # This is the initial "connection" operation, or a
            # subsequent reconnection attempt.
            debugprint ("Connection/reconnection failed")
            return self._reconnect_error (conn, exc)

        if self._cancel:
            return self._error (exc)

        if self._reconnect:
            self._reconnect = False
            self._reconnected = True
            conn.reconnect (self._user,
                            reply_handler=self._reconnect_reply,
                            error_handler=self._reconnect_error)
            return

        forbidden = False
        if type (exc) == cups.IPPError:
            (e, m) = exc.args
            if (e == cups.IPP_NOT_AUTHORIZED or
                e == cups.IPP_FORBIDDEN or
                e == cups.IPP_AUTHENTICATION_CANCELED):
                forbidden = (e == cups.IPP_FORBIDDEN)
            elif e == cups.IPP_SERVICE_UNAVAILABLE:
                return self._reconnect_error (conn, exc)
            else:
                return self._error (exc)
        elif type (exc) == cups.HTTPError:
            (s,) = exc.args
            if (s == cups.HTTP_UNAUTHORIZED or
                s == cups.HTTP_FORBIDDEN):
                forbidden = (s == cups.HTTP_FORBIDDEN)
            else:
                return self._error (exc)
        else:
            return self._error (exc)

        # Not authorized.

        if (self._try_as_root and
            self._user != 'root' and
            (self._conn.thread.host[0] == '/' or forbidden)):
            # This is a UNIX domain socket connection so we should
            # not have needed a password (or it is not a UDS but
            # we got an HTTP_FORBIDDEN response), and so the
            # operation must not be something that the current
            # user is authorised to do.  They need to try as root,
            # and supply the password.  However, to get the right
            # prompt, we need to try as root but with no password
            # first.
            debugprint ("Authentication: Try as root")
            self._user = "root"
            conn.reconnect (self._user,
                            reply_handler=self._reconnect_reply,
                            error_handler=self._reconnect_error)
            # Don't submit the task until we've connected.
            return

        if not self._auth_called:
            # We aren't even getting a chance to supply credentials.
            return self._error (exc)

        # Now reconnect and retry.
        host = conn.thread.host
        port = conn.thread.port
        authconn.global_authinfocache.remove_auth_info (host=host,
                                                        port=port)
        self._use_password = ''
        conn.reconnect (self._user,
                        reply_handler=self._reconnect_reply,
                        error_handler=self._reconnect_error)

    def auth_handler (self, prompt, conn, method=None, resource=None):
        if self._auth_called == False:
            if self._user == None:
                self._user = cups.getUser()
            if self._user:
                host = conn.thread.host
                port = conn.thread.port
                creds = authconn.global_authinfocache.lookup_auth_info (host=host,
                                                                        port=port)
                if creds:
                    if creds[0] == self._user:
                        self._use_password = creds[1]
                        self._reconnected = True
                    del creds
        else:
            host = conn.thread.host
            port = conn.thread.port
            authconn.global_authinfocache.remove_auth_info (host=host,
                                                            port=port)
            self._use_password = ''

        self._auth_called = True
        if self._reconnected:
            debugprint ("Supplying password after reconnection")
            self._reconnected = False
            conn.set_auth_info (self._use_password)
            return

        self._reconnected = False
        if not conn.prompt_allowed:
            conn.set_auth_info (self._use_password)
            return

        # If we've previously prompted, explain why we're prompting again.
        if self._dialog_shown:
            d = gtk.MessageDialog (self._conn.parent,
                                   gtk.DIALOG_MODAL |
                                   gtk.DIALOG_DESTROY_WITH_PARENT,
                                   gtk.MESSAGE_ERROR,
                                   gtk.BUTTONS_CLOSE,
                                   _("Not authorized"))
            d.format_secondary_text (_("The password may be incorrect."))
            d.run ()
            d.destroy ()

        op = None
        if conn.semantic:
            op = conn.semantic.current_operation ()

        if op == None:
            d = authconn.AuthDialog (parent=conn.parent)
        else:
            title = _("Authentication (%s)") % op
            d = authconn.AuthDialog (title=title,
                                     parent=conn.parent)

        d.set_prompt (prompt)
        if self._user == None:
            self._user = cups.getUser()
        d.set_auth_info ([self._user, ''])
        d.field_grab_focus ('password')
        d.set_keep_above (True)
        d.show_all ()
        d.connect ("response", self._on_auth_dialog_response)
        self._dialog_shown = True

    def submit_task (self):
        self._auth_called = False
        self._conn.queue.put ((self._client_fn, self._client_args,
                               self._client_kwds,
                               self._client_reply_handler,
                               
                               # Use our own error and auth handlers.
                               self.error_handler,
                               self.auth_handler))

    def _on_auth_dialog_response (self, dialog, response):
        (user, password) = dialog.get_auth_info ()
        if user == '':
            user = self._user;
        authconn.global_authinfocache.cache_auth_info ((user,
                                                        password),
                                                       host=self._conn.thread.host,
                                                       port=self._conn.thread.port)
        self._dialog = dialog
        dialog.hide ()

        if (response == gtk.RESPONSE_CANCEL or
            response == gtk.RESPONSE_DELETE_EVENT):
            self._cancel = True
            self._conn.set_auth_info ('')
            authconn.global_authinfocache.remove_auth_info (host=self._conn.thread.host,
                                                            port=self._conn.thread.port)
            debugprint ("Auth canceled")
            return

        if user == self._user:
            self._use_password = password
            self._conn.set_auth_info (password)
            debugprint ("Password supplied.")
            return

        self._user = user
        self._use_password = password
        self._reconnect = True
        self._conn.set_auth_info ('')
        debugprint ("Will try as %s" % self._user)

    def _reconnect_reply (self, conn, result):
        # A different username was given in the authentication dialog,
        # so we've reconnected as that user.  Alternatively, the
        # connection has failed and we're retrying.
        debugprint ("Connected as %s" % self._user)
        if self._client_fn != None:
            self.submit_task ()

    def _reconnect_error (self, conn, exc):
        debugprint ("Failed to connect as %s" % self._user)
        if not self._conn.prompt_allowed:
            self._error (exc)
            return

        op = None
        if conn.semantic:
            op = conn.semantic.current_operation ()

        if op == None:
            msg = _("CUPS server error")
        else:
            msg = _("CUPS server error (%s)") % op

        d = gtk.MessageDialog (self._conn.parent,
                               gtk.DIALOG_MODAL |
                               gtk.DIALOG_DESTROY_WITH_PARENT,
                               gtk.MESSAGE_ERROR,
                               gtk.BUTTONS_NONE,
                               msg)

        if self._client_fn == None and type (exc) == RuntimeError:
            # This was a connection failure.
            message = 'service-error-service-unavailable'
        elif type (exc) == cups.IPPError:
            message = exc.args[1]
        else:
            message = repr (exc)

        d.format_secondary_text (_("There was an error during the "
                                   "CUPS operation: '%s'." % message))
        d.add_buttons (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                       _("Retry"), gtk.RESPONSE_OK)
        d.set_default_response (gtk.RESPONSE_OK)
        d.connect ("response", self._on_retry_server_error_response)
        d.show ()

    def _on_retry_server_error_response (self, dialog, response):
        dialog.destroy ()
        if response == gtk.RESPONSE_OK:
            self._conn.reconnect (self._conn.thread.user,
                                  reply_handler=self._reconnect_reply,
                                  error_handler=self._reconnect_error)
        else:
            self._error (cups.IPPError (0, _("Operation canceled")))

    def _error (self, exc):
        if self._client_error_handler:
            self._client_error_handler (self._conn, exc)
            self._destroy ()

###
### The user-visible class.
###
class IPPAuthConnection(IPPConnection):
    def __init__ (self, reply_handler=None, error_handler=None,
                  auth_handler=None, host=None, port=None, encryption=None,
                  parent=None, try_as_root=True, prompt_allowed=True,
                  semantic=None):
        self.parent = parent
        self.prompt_allowed = prompt_allowed
        self.try_as_root = try_as_root
        self.semantic = semantic

        user = None
        creds = authconn.global_authinfocache.lookup_auth_info (host=host,
                                                                port=port)
        if creds:
            if creds[0] != 'root' or try_as_root:
                user = creds[0]
            del creds

        # The "connect" operation.
        op = _IPPAuthOperation (reply_handler, error_handler, self)
        IPPConnection.__init__ (self, reply_handler=reply_handler,
                                error_handler=op.error_handler,
                                auth_handler=op.auth_handler, user=user,
                                host=host, port=port, encryption=encryption)

    def destroy (self):
        self.semantic = None
        IPPConnection.destroy (self)

    def _call_function (self, fn, *args, **kwds):
        reply_handler = error_handler = auth_handler = False
        if kwds.has_key ("reply_handler"):
            reply_handler = kwds["reply_handler"]
            del kwds["reply_handler"]
        if kwds.has_key ("error_handler"):
            error_handler = kwds["error_handler"]
            del kwds["error_handler"]
        if kwds.has_key ("auth_handler"):
            auth_handler = kwds["auth_handler"]
            del kwds["auth_handler"]

        # Store enough information about the current operation to
        # restart it if necessary.
        op = _IPPAuthOperation (reply_handler, error_handler, self,
                                self.thread.user, fn, args, kwds)

        # Run the operation but use our own error and auth handlers.
        op.submit_task ()

if __name__ == "__main__":
    # Demo
    import gtk
    set_debugging (True)
    gobject.threads_init ()
    class UI:
        def __init__ (self):
            w = gtk.Window ()
            w.connect ("destroy", self.destroy)
            b = gtk.Button ("Connect")
            b.connect ("clicked", self.connect_clicked)
            vbox = gtk.VBox ()
            vbox.pack_start (b)
            w.add (vbox)
            self.get_devices_button = gtk.Button ("Get Devices")
            self.get_devices_button.connect ("clicked", self.get_devices)
            self.get_devices_button.set_sensitive (False)
            vbox.pack_start (self.get_devices_button)
            self.conn = None
            w.show_all ()

        def destroy (self, window):
            try:
                self.conn.destroy ()
            except AttributeError:
                pass

            gtk.main_quit ()

        def connect_clicked (self, button):
            if self.conn:
                self.conn.destroy ()

            self.conn = IPPAuthConnection (reply_handler=self.connected,
                                           error_handler=self.connect_failed)

        def connected (self, conn, result):
            debugprint ("Success: %s" % result)
            self.get_devices_button.set_sensitive (True)

        def connect_failed (self, conn, exc):
            debugprint ("Exc %s" % exc)
            self.get_devices_button.set_sensitive (False)
            self.conn.destroy ()

        def get_devices (self, button):
            button.set_sensitive (False)
            debugprint ("Getting devices")
            self.conn.getDevices (reply_handler=self.get_devices_reply,
                                  error_handler=self.get_devices_error)

        def get_devices_reply (self, conn, result):
            if conn != self.conn:
                debugprint ("Ignoring stale reply")
                return

            debugprint ("Got devices: %s" % result)
            self.get_devices_button.set_sensitive (True)

        def get_devices_error (self, conn, exc):
            if conn != self.conn:
                debugprint ("Ignoring stale error")
                return

            debugprint ("Error getting devices: %s" % exc)
            self.get_devices_button.set_sensitive (True)

    UI ()
    gtk.main ()
