[gnome-maps/wip/osm-edit: 1/2] osmEdit: WIP, implement OAuth sign in support
- From: Marcus Lundblad <mlundblad src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-maps/wip/osm-edit: 1/2] osmEdit: WIP, implement OAuth sign in support
- Date: Mon, 23 Nov 2015 20:26:19 +0000 (UTC)
commit 7b38d6200bbc7e38fc03f4bdb9d398694fdaecea
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]