~deryck/groundcontrol/keyword-search-name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
#
# Copyright 2009 Martin Owens
#
# 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 3 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, see <http://www.gnu.org/licenses/>
#
"""
Small app for configuring the launchpad projects.
"""

# Import standard python libs
import logging
import webbrowser
import webkit
import bzrlib.config
from httplib2 import ServerNotFoundError

# Various required variables and locations
from GroundControl.launchpad import get_launchpad, get_browser
from GroundControl.gtkviews import (
    GtkApp, ThreadedWindow,
    ChildWindow,
    IconManager
)
from GroundControl.launchpadweb import LoginFailedError, NoUsernameError
from GroundControl.sshkeys import compare_pubkeys, add_server_key
from GroundControl.gtkcommon import PopupApp
from lazr.restfulclient.errors import HTTPError

status_icons = IconManager('status')
person_icons = IconManager('person')

FRAMES = [ 'statussplash', 'ready', 'notready' ]
STATUSES = {
  'notonline' : _('Not online!'),
  'user'      : _('Looking up Launchpad User'),
  'ssh'       : _('Looking up Secure Shell Keys'),
  'bzr'       : _('Updating Bazaar Configuration'),
  'lp'        : _('Getting Launchpad Credentials'),
}

class ConfigWindow(ThreadedWindow):
    """Configure each of the options for intergration with launchpad."""
    name = 'configure'

    def load(self, *args, **kwargs):
        """Load this gui and a thread to do work, monitor everything"""
        # Don't load any of these now, they can take time
        # Instead we'll load them on demand via properties
        # Hopefully in a threaded environment.
        self._launchpad    = None
        self._lpweb        = None
        self._conf         = None
        self.update_status('notonline')
        super(ConfigWindow, self).load(*args, **kwargs)

    def inital_thread(self):
        """What to run when we execute the loading thread."""
        self.update()

    def signals(self):
        """Add some simple signals"""
        return {
            'login'    : self.login_window,
            'logoff'   : self.logoff,
            'register' : self.register,
        }

    @property
    def launchpad(self):
        """Return the launchpad object via OAuth"""
        if not self._launchpad:
            try:
                self._launchpad = get_launchpad()
            except HTTPError:
                logging.warn(_("Unable to authorise Launchpad account."))
        return self._launchpad

    @property
    def lpweb(self):
        """Return the launchpad webbrowser"""
        if not self._lpweb:
            self._lpweb = get_browser()
        return self._lpweb

    @property
    def conf(self):
        """Return the saved configuration for bazaar"""
        if not self._conf:
            self._conf = LaunchpadBazaar()
        return self._conf

    def is_online(self):
        """Return true if this computer is online"""
        # There are no os mechanisms yet for this.
        return True

    def ssh_keys_available(self):
        """Compare available ssh keys and return true if available"""
        for key in self.lpweb.ssh_keys():
            logging.debug("Comparing SSH Key.")
            if compare_pubkeys(key):
                logging.debug("SSH Key Found.")
                return True
        logging.debug("None of the SSH keys match.")
        return False

    def ssh_auto_configure(self):
        """Fixes any ssh key problems with launchpad"""
        if self.lpweb.has_gen_key():
            logging.debug("You already have an ssh key, but it's not live.")
            return self.lpweb.upload_key()
        return False

    def ssh_key_gui(self):
        """Load the ssh gui and ask for a key password"""
        self.load_window('getssh',
            callback=self.ssh_key_generate, auth=self.lpweb)

    def ssh_key_generate(self, password, confirm):
        """Generate keys or upload existing key up"""
        self.lpweb.gen_ssh_key(password)
        self.refresh_update()

    def refresh_update(self):
        """Start a thread to deal with the updating"""
        self.start_thread(self.update)

    def update(self):
        """Update the gui to reflect changes in config status."""
        #self.call('update_status', 'starting')
        # Check if we're online and show a broken web page if not
        if not self.is_online():
            self.call('update_status', 'notonline')
            return False

        self.call('update_status', 'user')
        # Check if we have access to your online web account
        try:
            if not self.lpweb.loggedin():
                logging.debug("Not Logged in yet apparently")
                self.call('update_frame', 'notready')
                return False
        except ServerNotFoundError:
            self.call('update_status', 'notonline')
            return False

        self.call('update_status', 'ssh')
        # Check to see if you have accessable ssh keys
        add_server_key('bazaar.launchpad.net')
        if not self.ssh_keys_available():
            if not self.ssh_auto_configure():
                self.call('ssh_key_gui')
                return False

        self.call('update_status', 'bzr')
        if not self.conf.has_bazaar_config():
            self.conf.set_bazaar_config(
                self.lpweb.user,
                self.lpweb.name,
                self.lpweb.emailaddr
            )

        self.call('update_status', 'lp')
        if not self.launchpad.launchpad:
            # If nothing returned, then we failed to auth oauth. Worry
            logging.warn(_("Unable to authenticate the Launchpad OAuth"))
            return False

        self.call('update_details')
        return True

    def update_frame(self, frame):
        """Display one of the pre-defined gui modes."""
        if frame not in FRAMES:
            raise Exception("Invalid GUI frame %s" % frame)
        self.widget('modetext').show()
        for name in FRAMES:
            if frame == name:
                self.widget(name).show()
            else:
                self.widget(name).hide()
        if frame == 'ready':
            self.widget('logoffbutton').show()
        else:
            self.widget('logoffbutton').hide()

    def update_status(self, status):
        """Seperate gtk section for updating the UI"""
        if not status in STATUSES:
            raise Exception("Invalid status %s" % status)
        image = status_icons.get_icon(status)
        markup = "<big><b>%s</b></big>" % STATUSES[status]
        self.widget('statusimage').set_from_pixbuf(image)
        self.widget('statustext').set_markup(markup)
        self.update_frame('statussplash')
        self.widget('modetext').hide()

    def update_details(self):
        """Update some fields with the name/email etc"""
        self.widget("namelabel").set_text(str(self.lpweb.name))
        self.widget("userlabel").set_text(str(self.lpweb.user))
        mugshot_file = self.launchpad.me.image()
        pixbuf = person_icons.get_icon(mugshot_file)
        self.widget("avatar").set_from_pixbuf(pixbuf)
        self.update_frame('ready')

    def login_window(self, button):
        """Show the login window"""
        self.load_window('login', callback=self.refresh_update, auth=self.lpweb)

    def logoff(self, button):
        """Remove all caches and log the user off of everything"""
        # Remove any oauth tokens
        self.launchpad.logoff()
        # Remove web cookies
        self.lpweb.logoff()
        # To be sure we'll remove objects
        self._launchpad = None
        self._lpweb = None
        # Reset to login
        self.update_frame('notready')

    def register(self, button):
        """Redirect user to where they can register on launchpad"""
        webbrowser.open('https://launchpad.net/+login', autoraise=1)

    def check_login(self):
        """Login to the web-browser via lpweb"""
        # This must just check that we are logged in correctly.
        self.lpweb
        # Our update here is also threaded.
        #self.start_thread(self.update)

    def get_args(self):
        """Nothing to return to window admire"""
        return {}


class WebView(webkit.WebView):
    """We need to customise our interactions"""
    def get_cookies(self):
        """Hex the cookies from webkit"""
        return self.hex('document.cookie')

    def get_html(self):
        """Hex the html content from webkit"""
        return self.hex('document.documentElement.innerHTML')

    def hex(self, variable):
        """Hex (curse with magic) the widget to get various data
           Because let's be honest, this is shit and you shouldn't
           have to do magic like this to do useful things.
        """
        self.execute_script('oldtitle=document.title;document.title=%s;' % variable)
        data = self.get_main_frame().get_title()
        self.execute_script('document.title=oldtitle;')
        return data

    def insert_css(self, uri):
        """Insert a new css uri"""
        js = """var css = document.createElement('link');
                css.type = 'text/css';
                css.rel = 'stylesheet';
                css.href = '%s';
                document.getElementsByTagName("head")[0].appendChild(css);
             """ % (uri)
        self.execute_script(js)


class LpLoginWindow(ChildWindow):
    """Allows launchpad to be logged in, basically a cookie trap."""
    name = 'login'

    def load(self, auth, *args, **kwargs):
        """Load a login window"""
        self.auth = auth
        self.web  = WebView()
        self.web.connect("load_started", self.disable_webkit)
        self.web.connect("load_finished", self.loaded)
        self.widget("contentbox").add(self.web)
        self.web.open(auth.request_url('+login'))
        self.web.show()
        self.window.show_all()
        super(LpLoginWindow, self).load(*args, **kwargs)

    def get_args(self):
        """Return any results from the form."""
        return { }

    def disable_webkit(self, widget, data=None):
        """Disable the webkit"""
        self.window.set_sensitive(False)

    def loaded(self, widget, data=None):
        """Do something!!"""
        title = self.web.get_main_frame().get_title()
        # Insert our own css to pretty up the launchpad login
        widget.insert_css('http://ground-control.org/css/lp.css')
        # Get our html contents
        html = widget.get_html()
        fh = open('/tmp/content', 'w')
        fh.write(html)
        fh.close()
        if title == 'Launchpad' and '+logout' in html:
            # We should now be logged in
            self.auth.set_session(widget.get_cookies())
            self.apply_changes()
        self.window.set_sensitive(True)

    def _exiting(self, *args, **kwargs):
        """Clear out variables on exit"""
        super(LpLoginWindow, self)._exiting(*args, **kwargs)

    def is_valid(self):
        """Run any checks required to login"""
        # It might be worth logging on here and passing back true
        return True


class LpSshWindow(ChildWindow):
    """Gathers a password from the user."""
    name = 'getssh'

    def load(self, auth, *args, **kwargs):
        """Load the SSH password window"""
        self.auth = auth
        super(LpSshWindow, self).load(*args, **kwargs)

    def get_args(self):
        """Return any results from the form."""
        password = self.widget('password1').get_text()
        confirm = self.widget('password2').get_text()
        return { 'password': password, 'confirm': confirm }

    def is_valid(self):
        """Run any checks required to login"""
        data = self.get_args()
        if data['password'] == data['confirm']:
            return True
        return False


class ConfigGui(GtkApp):
    """Application for sorting out launchpad intergration"""
    gtkfile = 'configure.glade'
    windows = [
        ConfigWindow,   # This is primary
        LpLoginWindow,  # Child Window
        LpSshWindow,    # Child Window
    ]


class LaunchpadBazaar(object):
    """Control launchpad's own bazaar configuration, automate the creation."""
    def __init__(self):
        self.config = bzrlib.config.GlobalConfig()
        self.auth   = bzrlib.config.AuthenticationConfig()

    def has_bazaar_config(self):
        """Checks to see if bazaar config is complete"""
        if not self.config.get_user_option('email'):
            logging.debug("No email address set")
            return False
        if not self.config.get_user_option('launchpad_username'):
            logging.debug("No Launchpad username set")
            return False
        if not self.auth.get_credentials('ssh', '.launchpad.net'):
            logging.debug("No authentication credentials for Launchpad")
            return False
        return True

    def set_bazaar_config(self, user, name, email):
        """Sets all the right values for your launchpad account"""
        self.config.set_user_option('email', "%s <%s>" % (name, email))
        self.config.set_user_option('launchpad_username', user)
        self.auth.set_credentials('Launchpad', '.launchpad.net', user, scheme='ssh')
        logging.debug("Launchpad Bazaar configuration changed to %s:%s:%s" % (user, name, email))