[gnome-maps/wip/osm-edit: 6/9] osmEdit: WIP, implement OAuth sign in support



commit 8c8b5dd774e22caae71ab951b4548ac6667af952
Author: Marcus Lundblad <ml update uu se>
Date:   Wed Nov 11 23:18:20 2015 +0100

    osmEdit: WIP, implement OAuth sign in support

 configure.ac                    |    1 +
 data/org.gnome.Maps.gschema.xml |    5 +
 src/osmConnection.js            |  235 +++++++++++++++++++++++++++++++++++++++
 src/osmEdit.js                  |   55 +++++++++
 4 files changed, 296 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 74ab942..5af1165 100644
--- a/configure.ac
+++ b/configure.ac
@@ -38,6 +38,7 @@ PKG_CHECK_MODULES(GNOME_MAPS, [
     gobject-introspection-1.0    >= $GOBJECT_INTROSPECTION_MIN_VERSION
     gtk+-3.0                     >= $GTK_MIN_VERSION
     geoclue-2.0                  >= $GEOCLUE_MIN_VERSION
+    webkit2gtk-4.0
 ])
 
 FOLKS_MIN_VERSION=0.10.0
diff --git a/data/org.gnome.Maps.gschema.xml b/data/org.gnome.Maps.gschema.xml
index a9dcfd9..aa083af 100644
--- a/data/org.gnome.Maps.gschema.xml
+++ b/data/org.gnome.Maps.gschema.xml
@@ -55,5 +55,10 @@
       <summary>Foursquare check-in Twitter broadcasting</summary>
       <description>Indicates if Foursquare should broadcast the check-in as a tweet in the Twitter account 
associated with the Foursquare account.</description>
     </key>
+    <key name="osm-username" type="s">
+      <default>""</default>
+      <summary>OpenStreetMap username or e-mail address</summary>
+      <description>Indicates if the user has signed in to edit OpenStreetMap data.</description>
+    </key>
   </schema>
 </schemalist>
diff --git a/src/osmConnection.js b/src/osmConnection.js
index 34de7d6..9ea289d 100644
--- a/src/osmConnection.js
+++ b/src/osmConnection.js
@@ -27,17 +27,33 @@ const Utils = imports.utils;
 const Lang = imports.lang;
 const GLib = imports.gi.GLib;
 const Maps = imports.gi.GnomeMaps;
+const Rest = imports.gi.Rest;
+const Secret = imports.gi.Secret;
 const Soup = imports.gi.Soup;
 
 const BASE_URL = 'https://api.openstreetmap.org/api';
 const TEST_BASE_URL = 'http://api06.dev.openstreetmap.org/api';
 const API_VERSION = '0.6';
 
+/* OAuth constants */
+const CONSUMER_KEY = '2lbpDoED0ZspGssTBAJ8zOCtrtmUoX4KnmZUIWIK';
+const CONSUMER_SECRET = 'AO9BhDl9sJ33DjaZgQmYcNIuM3ZSml4xtugai6gE';
+const OAUTH_ENDPOINT_URL = 'https://www.openstreetmap.org/oauth';
+const LOGIN_URL = 'https://www.openstreetmap.org/login';
+
+const SECRET_SCHEMA = new Secret.Schema("org.gnome.Maps",
+    Secret.SchemaFlags.NONE,
+    {
+    }
+);
+
 const OSMConnection = new Lang.Class({
     Name: 'OSMConnection',
 
     _init: function(params) {
         this._session = new Soup.Session();
+        this._oauthProxy = Rest.OAuthProxy.new(CONSUMER_KEY, CONSUMER_SECRET,
+                                               OAUTH_ENDPOINT_URL, false);
 
         /* TODO: stopgap to supply username/password
            to use with HTTP basic auth, should be
@@ -232,10 +248,229 @@ const OSMConnection = new Lang.Class({
     },
 
     _authenticate: function(session, msg, auth, retrying, user_data) {
+        Utils.debug('authenticate triggered');
         if (retrying)
             session.cancel_message(msg, Soup.Status.UNAUTHORIZED);
 
         auth.authenticate(this._username, this._password);
+    },
+
+    requestOAuthToken: function(callback) {
+        this._oauthProxy.request_token_async('request_token', 'oob',
+                                             function(proxy, error, weakObject,
+                                                      userData) {
+            this._onRequestOAuthToken(error, callback)}.bind(this),
+                                             this._oauthProxy, callback);
+    },
+
+    _onRequestOAuthToken: function(error, callback) {
+        Utils.debug('OAuth request token callback');
+        Utils.debug('callback: ' + callback);
+
+        if (error) {
+            Utils.debug('error message: ' + error.message);
+            callback(false);
+        }
+
+        Utils.debug('request token: ' + this._oauthProxy.get_token());
+        Utils.debug('request secret: ' + this._oauthProxy.get_token_secret());
+        this._oauthToken = this._oauthProxy.get_token();
+        this._oauthTokenSecret = this._oauthProxy.get_token_secret();
+        callback(true);
+    },
+
+    authorizeOAuthToken: function(username, password, callback) {
+        /* get login session ID */
+        let loginUrl = LOGIN_URL + '?cookie_test=true';
+        let uri = new Soup.URI(loginUrl);
+        let msg = new Soup.Message({method: 'GET', uri: uri});
+
+        Utils.debug('calling login form URL: ' + loginUrl);
+
+        this._session.queue_message(msg, (function(obj, message) {
+            this._onLoginFormReceived(message, username, password, callback);
+        }).bind(this));
+    },
+
+    _onLoginFormReceived: function(message, username, password, callback) {
+        Utils.debug('status: ' + message.status_code);
+
+        if (message.status_code !== Soup.Status.OK) {
+            Utils.debug('Failed to load login form');
+            callback(false);
+            return;
+        }
+
+        let osmSessionID =
+            this._extractOSMSessionID(message.response_headers);
+        let osmSessionToken =
+            this._extractToken(message.response_body.data);
+        Utils.debug('session ID: ' + osmSessionID);
+        Utils.debug('session token: ' + osmSessionToken);
+
+        if (osmSessionID === null || osmSessionToken === null) {
+            Utils.debug('Failed to extract OSM session');
+            callback(false, null);
+            return;
+        }
+
+        this._login(username, password, osmSessionID, osmSessionToken, callback);
+    },
+
+    _login: function(username, password, sessionId, token, callback) {
+        /* post login form */
+        let msg = Soup.form_request_new_from_hash('POST', LOGIN_URL,
+                                                  {username: username,
+                                                   password: password,
+                                                   referer: '/',
+                                                   commit: 'Login',
+                                                   authenticity_token: token});
+        let requestHeaders = msg.request_headers;
+
+        requestHeaders.append('Content-Type',
+                              'application/x-www-form-urlencoded');
+        requestHeaders.append('Cookie', '_osm_session=' + sessionId);
+        msg.flags |= Soup.MessageFlags.NO_REDIRECT;
+
+        this._session.queue_message(msg, (function(obj, message) {
+            if (message.status_code === Soup.Status.MOVED_TEMPORARILY)
+                this._fetchAuthorizeForm(username, sessionId, callback);
+            else
+                callback(false, null);
+        }).bind(this));
+
+    },
+
+    _fetchAuthorizeForm: function(username, sessionId, callback) {
+        let authorizeUrl =
+            OAUTH_ENDPOINT_URL + '/authorize?oauth_token=' +
+            this._oauthToken;
+        let uri = new Soup.URI(authorizeUrl);
+        let msg = new Soup.Message({uri: uri, method: 'GET'});
+
+        msg.request_headers.append('Cookie',
+                                   '_osm_session=' + sessionId +
+                                   '; _osm_username=' + username);
+        this._session.queue_message(msg, (function(obj, message) {
+            if (message.status_code === Soup.Status.OK) {
+                let token = this._extractToken(message.response_body.data);
+                Utils.debug('token: ' + token);
+                Utils.debug('body: ' + message.response_body.data);
+                this._postAuthorizeForm(username, sessionId, token, callback);
+            } else {
+                callback(false, null);
+            }
+        }).bind(this));
+    },
+
+    _postAuthorizeForm: function(username, sessionId, token, callback) {
+        let authorizeUrl = OAUTH_ENDPOINT_URL + '/authorize';
+        let msg =
+            Soup.form_request_new_from_hash('POST', authorizeUrl,
+                                            {oauth_token: this._oauthToken,
+                                             oauth_callback: '',
+                                             authenticity_token: token,
+                                             allow_write_api: 'yes',
+                                             commit: 'Save changes'});
+        let requestHeaders = msg.request_headers;
+
+        requestHeaders.append('Content-Type',
+                              'application/x-www-form-urlencoded');
+        requestHeaders.append('Cookie',
+                              '_osm_session=' + sessionId +
+                              '; _osm_username=' + username);
+
+        this._session.queue_message(msg, (function(obj, message) {
+            if (msg.status_code === Soup.Status.OK) {
+                Utils.debug('body2: ' + message.response_body.data);
+                callback(true, message.response_body.data);
+            } else
+                callback(false, null);
+        }).bind(this));
+    },
+
+    requestOAuthAccessToken: function(verificationCode, callback) {
+        this._oauthProxy.access_token_async('access_token', verificationCode,
+                                             function(proxy, error, weakObject,
+                                                      userData) {
+            this._onAccessOAuthToken(error, callback)}.bind(this),
+                                             this._oauthProxy, callback);
+    },
+
+    _onAccessOAuthToken: function(error, callback) {
+        if (error) {
+            Utils.debug('error message: ' + error.message);
+            callback(false);
+            return;
+        }
+        Utils.debug('access_token: ' + this._oauthProxy.token);
+        Utils.debug('access token secret: ' + this._oauthProxy.token_secret);
+        Secret.password_store(SECRET_SCHEMA, {}, Secret.COLLECTION_DEFAULT,
+                              "OSM OAuth access token and secret",
+                              this._oauthProxy.token + ":" +
+                              this._oauthProxy.token_secret, null,
+                              function(source, result, userData) {
+                                this._onPasswordStored(result, callback);
+                              }.bind(this));
+    },
+
+    _onPasswordStored: function(result, callback) {
+        let res = false;
+        if (result)
+            res = Secret.password_store_finish(result);
+        callback(res);
+    },
+
+    signOut: function() {
+        Secret.password_clear(SECRET_SCHEMA, {}, null,
+            this._onPasswordCleared.bind(this));
+    },
+
+    _onPasswordCleared: function(source, result) {
+        Secret.password_clear_finish(result);
+    },
+
+    /* extract the session ID from the login form response headers */
+    _extractOSMSessionID: function(responseHeaders) {
+        let cookie = responseHeaders.get('Set-Cookie');
+
+        Utils.debug('cookie: ' + cookie);
+
+        if (cookie === null)
+            return null;
+
+        let cookieParts = cookie.split(';');
+        for (let index in cookieParts) {
+            let kvPair = cookieParts[index].trim();
+            let kv = kvPair.split('=');
+
+            Utils.debug('kv: ' + kv);
+
+            if (kv.length !== 2) {
+                continue;
+            } else if (kv[0] === '_osm_session') {
+                return kv[1];
+            }
+        }
+
+        return null;
+    },
+
+    /* extract the authenticity token from the hidden input field of the login
+       form */
+    _extractToken: function(messageBody) {
+        let regex = /.*authenticity_token.*value=\"([^\"]+)\".*/;
+        let lines = messageBody.split('\n');
+
+        for (let i in lines) {
+            let line = lines[i];
+            let match = line.match(regex);
+
+            if (match && match.length === 2)
+                return match[1];
+        }
+
+        return null;
     }
 });
 
diff --git a/src/osmEdit.js b/src/osmEdit.js
index f7318bd..5f93ce0 100644
--- a/src/osmEdit.js
+++ b/src/osmEdit.js
@@ -23,6 +23,8 @@
 const GObject = imports.gi.GObject;
 const Lang = imports.lang;
 
+const Application = imports.application;
+const OSMAccountDialog = imports.osmAccountDialog;
 const OSMEditDialog = imports.osmEditDialog;
 const OSMConnection = imports.osmConnection;
 const Utils = imports.utils;
@@ -34,6 +36,8 @@ const OSMEdit = new Lang.Class({
     _init: function() {
         this._osmConnection = new OSMConnection.OSMConnection();
         this._osmObject = null; // currently edited object
+        this._username = Application.settings.get('osm-username');
+        this._isSignedIn = this._username !== null && this._username.length > 0;
     },
 
     get useTestApi() {
@@ -128,5 +132,56 @@ const OSMEdit = new Lang.Class({
 
     _closeChangeset: function(changesetId, callback) {
         this._osmConnection.closeChangeset(changesetId, callback);
+    },
+
+    performOAuthSignIn: function(username, password, callback) {
+        this._osmConnection.requestOAuthToken(function(success) {
+            Utils.debug('requested token');
+            if (success)
+                this._onOAuthTokenRequested(username, password, callback);
+            else
+                callback(false, null);
+        }.bind(this));
+    },
+
+    _onOAuthTokenRequested: function(username, password, callback) {
+        /* keep track of authorizing username */
+        this._username = username;
+        this._osmConnection.authorizeOAuthToken(username, password, callback);
+    },
+
+    requestOAuthAccessToken: function(verificationCode, callback) {
+        this._osmConnection.requestOAuthAccessToken(verificationCode,
+                                                    (function(success, token) {
+            this._onOAuthAccessTokenRequested(success, callback);
+                                                             }).bind(this));
+    },
+
+    _onOAuthAccessTokenRequested: function(success, callback) {
+        if (success) {
+            this._signedIn = true;
+            Application.settings.set('osm-username', this._username);
+        } else {
+            /* clear out username if verification was unsuccessful */
+            this._username = null;
+        }
+
+        callback(success);
+    },
+
+    signOut: function() {
+        this._username = null;
+        this._signedIn = false;
+
+        Application.settings.set('osm-username', '');
+        this._osmConnection.signOut();
+    },
+
+    get isSignedIn() {
+        return this._isSignedIn;
+    },
+
+    get username() {
+        return this._username;
     }
 });


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]