diff --git a/extensions/wikidata/module/scripts/dialogs/perform-edits-dialog.js b/extensions/wikidata/module/scripts/dialogs/perform-edits-dialog.js
index eed847bf6..283747b3f 100644
--- a/extensions/wikidata/module/scripts/dialogs/perform-edits-dialog.js
+++ b/extensions/wikidata/module/scripts/dialogs/perform-edits-dialog.js
@@ -42,7 +42,7 @@ PerformEditsDialog.launch = function(logged_in_username, max_severity) {
{ onDone: function() { dismiss(); } }
);
}
- }
+ };
elmts.loggedInUsername
.text(logged_in_username)
@@ -72,7 +72,7 @@ PerformEditsDialog.updateEditCount = function(edit_count) {
this._elmts.reviewYourEdits.html(
$.i18n('perform-wikidata-edits/review-your-edits')
.replace('{nb_edits}', edit_count));
-}
+};
PerformEditsDialog._updateWarnings = function(data) {
var warnings = data.warnings;
@@ -87,7 +87,7 @@ PerformEditsDialog._updateWarnings = function(data) {
var rendered = WarningsRenderer._renderWarning(warnings[i]);
rendered.appendTo(table);
}
-}
+};
PerformEditsDialog.checkAndLaunch = function () {
var self = this;
@@ -95,26 +95,24 @@ PerformEditsDialog.checkAndLaunch = function () {
this._elmts = DOM.bind(this.frame);
this.missingSchema = false;
- var onSaved = function() {
- ManageAccountDialog.ensureLoggedIn(function(logged_in_username) {
- if (logged_in_username) {
- var discardWaiter = DialogSystem.showBusy($.i18n('perform-wikidata-edits/analyzing-edits'));
- Refine.postCSRF(
- "command/wikidata/preview-wikibase-schema?" + $.param({ project: theProject.id }),
- { engine: JSON.stringify(ui.browsingEngine.getJSON()) },
- function(data) {
- discardWaiter();
- if(data['code'] != 'error') {
- PerformEditsDialog._updateWarnings(data);
- PerformEditsDialog.launch(logged_in_username, data['max_severity']);
- } else {
- SchemaAlignmentDialog.launch(
- PerformEditsDialog.checkAndLaunch);
- }
- },
- "json"
- );
- }
+ var onSaved = function () {
+ ManageAccountDialog.ensureLoggedIn(function (logged_in_username) {
+ var discardWaiter = DialogSystem.showBusy($.i18n('perform-wikidata-edits/analyzing-edits'));
+ Refine.postCSRF(
+ "command/wikidata/preview-wikibase-schema?" + $.param({project: theProject.id}),
+ {engine: JSON.stringify(ui.browsingEngine.getJSON())},
+ function (data) {
+ discardWaiter();
+ if (data['code'] != 'error') {
+ PerformEditsDialog._updateWarnings(data);
+ PerformEditsDialog.launch(logged_in_username, data['max_severity']);
+ } else {
+ SchemaAlignmentDialog.launch(
+ PerformEditsDialog.checkAndLaunch);
+ }
+ },
+ "json"
+ );
});
};
diff --git a/extensions/wikidata/module/styles/dialogs/import-schema-dialog.less b/extensions/wikidata/module/styles/dialogs/import-schema-dialog.less
index 840c04ae6..54f4842c7 100644
--- a/extensions/wikidata/module/styles/dialogs/import-schema-dialog.less
+++ b/extensions/wikidata/module/styles/dialogs/import-schema-dialog.less
@@ -6,5 +6,6 @@
}
.wikibase-schema-textarea {
- width: 100%;
- height: 100px;
+ width: 100%;
+ height: 100px;
+}
diff --git a/extensions/wikidata/module/styles/dialogs/manage-account-dialog.less b/extensions/wikidata/module/styles/dialogs/manage-account-dialog.less
index 024fc8ed0..33449f52c 100644
--- a/extensions/wikidata/module/styles/dialogs/manage-account-dialog.less
+++ b/extensions/wikidata/module/styles/dialogs/manage-account-dialog.less
@@ -37,12 +37,13 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
text-align: center;
}
-.wikibase-login-buttons {
- text-align: right;
-}
-
.wikibase-invalid-credentials {
color: red;
+ padding-bottom: 0.3rem;
+}
+
+.wikibase-user-login tr td {
+ padding-bottom: 6px;
}
.wikibase-user-login tr td:first-child {
@@ -55,10 +56,39 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
}
.wikidata-logo {
+ height: 100%;
float: left;
- margin: -10px;
+ margin-right: 50px;
+ display:flex;
+ flex-direction: column;
+ justify-content: center;
}
-.right-of-logo {
- margin-left: 110px;
+.wikidata-logo img {
+ height: 120px;
+}
+
+.wikibase-login-buttons {
+ text-align: right;
+ position: absolute;
+ right: 12px;
+ bottom: 12px;
+}
+
+
+.wikibase-perform-edits-buttons {
+ text-align: right;
+}
+
+.wikibase-import-schema-buttons {
+ text-align: right;
+}
+
+
+.wikibase-login-dialog-footer {
+ text-align: center;
+}
+
+.wikibase-login-dialog-footer span {
+ cursor: pointer;
}
diff --git a/extensions/wikidata/module/styles/theme.less b/extensions/wikidata/module/styles/theme.less
new file mode 100644
index 000000000..e04075202
--- /dev/null
+++ b/extensions/wikidata/module/styles/theme.less
@@ -0,0 +1,34 @@
+/*
+
+Copyright 2011, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+@import-less url("../../../../main/webapp/modules/core/styles/theme.less");
diff --git a/extensions/wikidata/pom.xml b/extensions/wikidata/pom.xml
index 87cf1056b..fab5d4837 100644
--- a/extensions/wikidata/pom.xml
+++ b/extensions/wikidata/pom.xml
@@ -16,7 +16,7 @@
@@ -127,17 +127,17 @@
provided
- org.wikidata.wdtk
+ org.openrefine.dependencies.wdtk
wdtk-wikibaseapi
${wdtk.version}
- org.wikidata.wdtk
+ org.openrefine.dependencies.wdtk
wdtk-datamodel
${wdtk.version}
- org.wikidata.wdtk
+ org.openrefine.dependencies.wdtk
wdtk-util
${wdtk.version}
diff --git a/extensions/wikidata/src/org/openrefine/wikidata/commands/CommandUtilities.java b/extensions/wikidata/src/org/openrefine/wikidata/commands/CommandUtilities.java
index 540efeaea..8cf0af93e 100644
--- a/extensions/wikidata/src/org/openrefine/wikidata/commands/CommandUtilities.java
+++ b/extensions/wikidata/src/org/openrefine/wikidata/commands/CommandUtilities.java
@@ -6,6 +6,7 @@ import java.io.Writer;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.core.JsonGenerator;
+import com.google.refine.commands.Command;
import com.google.refine.util.ParsingUtilities;
public class CommandUtilities {
diff --git a/extensions/wikidata/src/org/openrefine/wikidata/commands/ConnectionManager.java b/extensions/wikidata/src/org/openrefine/wikidata/commands/ConnectionManager.java
new file mode 100644
index 000000000..c66ad6778
--- /dev/null
+++ b/extensions/wikidata/src/org/openrefine/wikidata/commands/ConnectionManager.java
@@ -0,0 +1,219 @@
+/*******************************************************************************
+ * MIT License
+ *
+ * Copyright (c) 2018 Antonin Delpeuch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ ******************************************************************************/
+package org.openrefine.wikidata.commands;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.refine.ProjectManager;
+import com.google.refine.preference.PreferenceStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.wikidata.wdtk.wikibaseapi.ApiConnection;
+import org.wikidata.wdtk.wikibaseapi.BasicApiConnection;
+import org.wikidata.wdtk.wikibaseapi.LoginFailedException;
+import org.wikidata.wdtk.wikibaseapi.OAuthApiConnection;
+import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiApiErrorException;
+
+import javax.servlet.http.Cookie;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages a connection to Wikidata.
+ *
+ * The connection can be either {@link BasicApiConnection} or {@link OAuthApiConnection}.
+ *
+ * This class is also hard-coded for Wikidata,
+ * it will be generalized to other Wikibase instances soon.
+ *
+ * @author Antonin Delpeuch
+ * @author Lu Liu
+ */
+
+public class ConnectionManager {
+
+ final static Logger logger = LoggerFactory.getLogger("connection_manager");
+
+ /**
+ * We used this key to read/write credentials from/to preferences in the past, which is insecure.
+ * Now this key is kept only to delete those credentials in the preferences.
+ */
+ public static final String PREFERENCE_STORE_KEY = "wikidata_credentials";
+
+ public static final int CONNECT_TIMEOUT = 5000;
+ public static final int READ_TIMEOUT = 10000;
+
+ /**
+ * For now, this class is hard-coded for Wikidata.
+ *
+ * It will be generalized to work against other Wikibase instances in the future.
+ */
+ private static final String WIKIBASE_API_ENDPOINT = ApiConnection.URL_WIKIDATA_API;
+
+ /**
+ * The single {@link ApiConnection} instance managed by {@link ConnectionManager}.
+ *
+ * Currently, only one connection is supported at the same time.
+ */
+ private ApiConnection connection;
+
+ private static final ConnectionManager instance = new ConnectionManager();
+
+ public static ConnectionManager getInstance() {
+ return instance;
+ }
+
+ private ConnectionManager() {
+ PreferenceStore prefStore = ProjectManager.singleton.getPreferenceStore();
+ // remove credentials stored in the preferences
+ prefStore.put(PREFERENCE_STORE_KEY, null);
+ }
+
+ /**
+ * Logs in to the Wikibase instance, using username/password.
+ *
+ * If failed to login, the connection will be set to null.
+ *
+ * @param username the username to log in with
+ * @param password the password to log in with
+ * @return true if logged in successfully, false otherwise
+ */
+ public boolean login(String username, String password) {
+ connection = new BasicApiConnection(WIKIBASE_API_ENDPOINT);
+ setupConnection(connection);
+ try {
+ ((BasicApiConnection) connection).login(username, password);
+ return true;
+ } catch (LoginFailedException e) {
+ logger.error(e.getMessage());
+ connection = null;
+ return false;
+ }
+ }
+
+ /**
+ * Logs in to the Wikibase instance, using owner-only consumer.
+ *
+ * If failed to login, the connection will be set to null.
+ *
+ * @param consumerToken consumer token of an owner-only consumer
+ * @param consumerSecret consumer secret of an owner-only consumer
+ * @param accessToken access token of an owner-only consumer
+ * @param accessSecret access secret of an owner-only consumer
+ * @return true if logged in successfully, false otherwise
+ */
+ public boolean login(String consumerToken, String consumerSecret,
+ String accessToken, String accessSecret) {
+ connection = new OAuthApiConnection(WIKIBASE_API_ENDPOINT,
+ consumerToken, consumerSecret,
+ accessToken, accessSecret);
+ setupConnection(connection);
+ try {
+ // check if the credentials are valid
+ connection.checkCredentials();
+ return true;
+ } catch (IOException | MediaWikiApiErrorException e) {
+ logger.error(e.getMessage());
+ connection = null;
+ return false;
+ }
+ }
+
+
+ /**
+ * Logs in to the Wikibase instance, using cookies.
+ *
+ * If failed to login, the connection will be set to null.
+ *
+ * @param username the username
+ * @param cookies the cookies used to login
+ * @return true if logged in successfully, false otherwise
+ */
+ public boolean login(String username, List cookies) {
+ cookies.forEach(cookie -> cookie.setPath("/"));
+ Map map = new HashMap<>();
+ map.put("baseUrl", WIKIBASE_API_ENDPOINT);
+ map.put("cookies", cookies);
+ map.put("username", username);
+ map.put("loggedIn", true);
+ map.put("tokens", Collections.emptyMap());
+ map.put("connectTimeout", CONNECT_TIMEOUT);
+ map.put("readTimeout", READ_TIMEOUT);
+ try {
+ BasicApiConnection newConnection = convertToBasicApiConnection(map);
+ newConnection.checkCredentials();
+ connection = newConnection;
+ return true;
+ } catch (IOException | MediaWikiApiErrorException e) {
+ logger.error(e.getMessage());
+ connection = null;
+ return false;
+ }
+ }
+
+ /**
+ * For testability.
+ */
+ static BasicApiConnection convertToBasicApiConnection(Map map) throws JsonProcessingException {
+ ObjectMapper mapper = new ObjectMapper();
+ String json = mapper.writeValueAsString(map);
+ return mapper.readValue(json, BasicApiConnection.class);
+ }
+
+
+ public void logout() {
+ if (connection != null) {
+ try {
+ connection.logout();
+ connection = null;
+ } catch (IOException | MediaWikiApiErrorException e) {
+ logger.error(e.getMessage());
+ }
+ }
+ }
+
+ public ApiConnection getConnection() {
+ return connection;
+ }
+
+ public boolean isLoggedIn() {
+ return connection != null;
+ }
+
+ public String getUsername() {
+ if (connection != null) {
+ return connection.getCurrentUser();
+ } else {
+ return null;
+ }
+ }
+
+ private void setupConnection(ApiConnection connection) {
+ connection.setConnectTimeout(CONNECT_TIMEOUT);
+ connection.setReadTimeout(READ_TIMEOUT);
+ }
+}
diff --git a/extensions/wikidata/src/org/openrefine/wikidata/commands/LoginCommand.java b/extensions/wikidata/src/org/openrefine/wikidata/commands/LoginCommand.java
index 2983588e6..04ef78ba8 100644
--- a/extensions/wikidata/src/org/openrefine/wikidata/commands/LoginCommand.java
+++ b/extensions/wikidata/src/org/openrefine/wikidata/commands/LoginCommand.java
@@ -1,18 +1,18 @@
/*******************************************************************************
* MIT License
- *
+ *
* Copyright (c) 2018 Antonin Delpeuch
- *
+ *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
- *
+ *
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
- *
+ *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -23,56 +23,159 @@
******************************************************************************/
package org.openrefine.wikidata.commands;
-import java.io.IOException;
-import java.io.Writer;
+import com.google.refine.commands.Command;
+import org.wikidata.wdtk.wikibaseapi.ApiConnection;
+import org.wikidata.wdtk.wikibaseapi.BasicApiConnection;
import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpCookie;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
-import org.openrefine.wikidata.editing.ConnectionManager;
-
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.google.refine.commands.Command;
-import com.google.refine.util.ParsingUtilities;
+import static org.apache.commons.lang.StringUtils.isBlank;
+import static org.apache.commons.lang.StringUtils.isNotBlank;
+/**
+ * Handles login.
+ *
+ * Both logging in with username/password or owner-only consumer are supported.
+ *
+ * This command also manages cookies of login credentials.
+ */
public class LoginCommand extends Command {
+ static final String WIKIDATA_COOKIE_PREFIX = "openrefine-wikidata-";
+
+ static final String WIKIBASE_USERNAME_COOKIE_KEY = "wikibase-username";
+
+ static final String USERNAME = "wb-username";
+ static final String PASSWORD = "wb-password";
+
+ static final String CONSUMER_TOKEN = "wb-consumer-token";
+ static final String CONSUMER_SECRET = "wb-consumer-secret";
+ static final String ACCESS_TOKEN = "wb-access-token";
+ static final String ACCESS_SECRET = "wb-access-secret";
+
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- if(!hasValidCSRFToken(request)) {
- respondCSRFError(response);
- return;
- }
- respond(request, response);
- }
-
- protected void respond(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String username = request.getParameter("wb-username");
- String password = request.getParameter("wb-password");
- String remember = request.getParameter("remember-credentials");
- ConnectionManager manager = ConnectionManager.getInstance();
- if (username != null && password != null) {
- manager.login(username, password, "on".equals(remember));
- } else if ("true".equals(request.getParameter("logout"))) {
- manager.logout();
+ if (!hasValidCSRFToken(request)) {
+ respondCSRFError(response);
+ return;
}
- response.setCharacterEncoding("UTF-8");
- response.setHeader("Content-Type", "application/json");
- Writer w = response.getWriter();
- JsonGenerator writer = ParsingUtilities.mapper.getFactory().createGenerator(w);
+ ConnectionManager manager = ConnectionManager.getInstance();
- writer.writeStartObject();
- writer.writeBooleanField("logged_in", manager.isLoggedIn());
- writer.writeStringField("username", manager.getUsername());
- writer.writeEndObject();
- writer.flush();
- writer.close();
- w.flush();
- w.close();
+ if ("true".equals(request.getParameter("logout"))) {
+ manager.logout();
+ removeUsernamePasswordCookies(request, response);
+ removeOwnerOnlyConsumerCookies(request, response);
+ respond(request, response);
+ return; // return directly
+ }
+
+ boolean remember = "on".equals(request.getParameter("remember-credentials"));
+
+ // Credentials from parameters have higher priority than those from cookies.
+ String username = request.getParameter(USERNAME);
+ String password = request.getParameter(PASSWORD);
+ String consumerToken = request.getParameter(CONSUMER_TOKEN);
+ String consumerSecret = request.getParameter(CONSUMER_SECRET);
+ String accessToken = request.getParameter(ACCESS_TOKEN);
+ String accessSecret = request.getParameter(ACCESS_SECRET);
+
+ if (isBlank(username) && isBlank(password) && isBlank(consumerToken) &&
+ isBlank(consumerSecret) && isBlank(accessToken) && isBlank(accessSecret)) {
+ // In this case, we use cookie to login, and we will always remember the credentials in cookies.
+ remember = true;
+ Cookie[] cookies = request.getCookies();
+
+ for (Cookie cookie : cookies) {
+ String value = getCookieValue(cookie);
+ switch (cookie.getName()) {
+ case CONSUMER_TOKEN:
+ consumerToken = value;
+ break;
+ case CONSUMER_SECRET:
+ consumerSecret = value;
+ break;
+ case ACCESS_TOKEN:
+ accessToken = value;
+ break;
+ case ACCESS_SECRET:
+ accessSecret = value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (isBlank(consumerToken) && isBlank(consumerSecret) && isBlank(accessToken) && isBlank(accessSecret)) {
+ // Try logging in with the cookies of a password-based connection.
+ String username1 = null;
+ List cookieList = new ArrayList<>();
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().startsWith(WIKIDATA_COOKIE_PREFIX)) {
+ String cookieName = cookie.getName().substring(WIKIDATA_COOKIE_PREFIX.length());
+ Cookie newCookie = new Cookie(cookieName, getCookieValue(cookie));
+ cookieList.add(newCookie);
+ } else if (cookie.getName().equals(WIKIBASE_USERNAME_COOKIE_KEY)) {
+ username1 = getCookieValue(cookie);
+ }
+ }
+
+ if (cookieList.size() > 0 && username1 != null) {
+ removeOwnerOnlyConsumerCookies(request, response);
+ if (manager.login(username1, cookieList)) {
+ respond(request, response);
+ return;
+ } else {
+ removeUsernamePasswordCookies(request, response);
+ }
+ }
+ }
+ }
+
+ if (isNotBlank(username) && isNotBlank(password)) {
+ // Once logged in with new credentials,
+ // the old credentials in cookies should be cleared.
+ if (manager.login(username, password) && remember) {
+ ApiConnection connection = manager.getConnection();
+ List cookies = ((BasicApiConnection) connection).getCookies();
+ for (HttpCookie cookie : cookies) {
+ setCookie(response, WIKIDATA_COOKIE_PREFIX + cookie.getName(), cookie.getValue());
+ }
+
+ // Though the cookies from the connection contain some cookies of username,
+ // we cannot make sure that all Wikibase instances use the same cookie key
+ // to retrieve the username. So we choose to set the username cookie with our own cookie key.
+ setCookie(response, WIKIBASE_USERNAME_COOKIE_KEY, connection.getCurrentUser());
+ } else {
+ removeUsernamePasswordCookies(request, response);
+ }
+ removeOwnerOnlyConsumerCookies(request, response);
+ } else if (isNotBlank(consumerToken) && isNotBlank(consumerSecret) && isNotBlank(accessToken) && isNotBlank(accessSecret)) {
+ if (manager.login(consumerToken, consumerSecret, accessToken, accessSecret) && remember) {
+ setCookie(response, CONSUMER_TOKEN, consumerToken);
+ setCookie(response, CONSUMER_SECRET, consumerSecret);
+ setCookie(response, ACCESS_TOKEN, accessToken);
+ setCookie(response, ACCESS_SECRET, accessSecret);
+ } else {
+ removeOwnerOnlyConsumerCookies(request, response);
+ }
+ removeUsernamePasswordCookies(request, response);
+ }
+
+ respond(request, response);
}
@Override
@@ -80,4 +183,51 @@ public class LoginCommand extends Command {
throws ServletException, IOException {
respond(request, response);
}
+
+ protected void respond(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ ConnectionManager manager = ConnectionManager.getInstance();
+ Map jsonResponse = new HashMap<>();
+ jsonResponse.put("logged_in", manager.isLoggedIn());
+ jsonResponse.put("username", manager.getUsername());
+ respondJSON(response, jsonResponse);
+ }
+
+ private static void removeUsernamePasswordCookies(HttpServletRequest request, HttpServletResponse response) {
+ Cookie[] cookies = request.getCookies();
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().startsWith(WIKIDATA_COOKIE_PREFIX)) {
+ removeCookie(response, cookie.getName());
+ }
+ }
+ removeCookie(response, WIKIBASE_USERNAME_COOKIE_KEY);
+ }
+
+ private static void removeOwnerOnlyConsumerCookies(HttpServletRequest request, HttpServletResponse response) {
+ removeCookie(response, CONSUMER_TOKEN);
+ removeCookie(response, CONSUMER_SECRET);
+ removeCookie(response, ACCESS_TOKEN);
+ removeCookie(response, ACCESS_SECRET);
+ }
+
+ static String getCookieValue(Cookie cookie) throws UnsupportedEncodingException {
+ return URLDecoder.decode(cookie.getValue(), "utf-8");
+ }
+
+ private static void setCookie(HttpServletResponse response, String key, String value) throws UnsupportedEncodingException {
+ String encodedValue = URLEncoder.encode(value, "utf-8");
+ Cookie cookie = new Cookie(key, encodedValue);
+ cookie.setMaxAge(60 * 60 * 24 * 365); // a year
+ cookie.setPath("/");
+ // set to false because OpenRefine doesn't require HTTPS
+ cookie.setSecure(false);
+ response.addCookie(cookie);
+ }
+
+ private static void removeCookie(HttpServletResponse response, String key) {
+ Cookie cookie = new Cookie(key, "");
+ cookie.setMaxAge(0); // 0 causes the cookie to be deleted
+ cookie.setPath("/");
+ cookie.setSecure(false);
+ response.addCookie(cookie);
+ }
}
diff --git a/extensions/wikidata/src/org/openrefine/wikidata/editing/ConnectionManager.java b/extensions/wikidata/src/org/openrefine/wikidata/editing/ConnectionManager.java
deleted file mode 100644
index 0bd1dd106..000000000
--- a/extensions/wikidata/src/org/openrefine/wikidata/editing/ConnectionManager.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*******************************************************************************
- * MIT License
- *
- * Copyright (c) 2018 Antonin Delpeuch
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- ******************************************************************************/
-package org.openrefine.wikidata.editing;
-
-import java.io.IOException;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.wikidata.wdtk.wikibaseapi.ApiConnection;
-import org.wikidata.wdtk.wikibaseapi.BasicApiConnection;
-import org.wikidata.wdtk.wikibaseapi.LoginFailedException;
-import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiApiErrorException;
-
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.refine.ProjectManager;
-import com.google.refine.preference.PreferenceStore;
-import com.google.refine.util.ParsingUtilities;
-
-/**
- * Manages a connection to Wikidata, with login credentials stored in the
- * preferences.
- *
- * Ideally, we should store only the cookies and not the password. But
- * Wikidata-Toolkit does not allow for that as cookies are kept private.
- *
- * This class is also hard-coded for Wikidata: generalization to other Wikibase
- * instances should be feasible though.
- *
- * @author Antonin Delpeuch
- */
-
-public class ConnectionManager {
-
- final static Logger logger = LoggerFactory.getLogger("connection_mananger");
-
- public static final String PREFERENCE_STORE_KEY = "wikidata_credentials";
- public static final int CONNECT_TIMEOUT = 5000;
- public static final int READ_TIMEOUT = 10000;
-
- private PreferenceStore prefStore;
- private BasicApiConnection connection;
-
- private static final ConnectionManager instance = new ConnectionManager();
-
- public static ConnectionManager getInstance() {
- return instance;
- }
-
- /**
- * Creates a connection manager, which attempts to restore any
- * previous connection (from the preferences).
- */
- private ConnectionManager() {
- prefStore = ProjectManager.singleton.getPreferenceStore();
- connection = null;
- restoreSavedConnection();
- }
-
- /**
- * Logs in to the Wikibase instance, using login/password
- *
- * @param username
- * the username to log in with
- * @param password
- * the password to log in with
- * @param rememberCredentials
- * whether to store these credentials in the preferences (unencrypted!)
- */
- public void login(String username, String password, boolean rememberCredentials) {
- if (rememberCredentials) {
- ArrayNode array = ParsingUtilities.mapper.createArrayNode();
- ObjectNode obj = ParsingUtilities.mapper.createObjectNode();
- obj.put("username", username);
- obj.put("password", password);
- array.add(obj);
- prefStore.put(PREFERENCE_STORE_KEY, array);
- }
-
- connection = createNewConnection();
- try {
- connection.login(username, password);
- } catch (LoginFailedException e) {
- connection = null;
- }
- }
-
- /**
- * Restore any previously saved connection, from the preferences.
- */
- public void restoreSavedConnection() {
- ObjectNode savedCredentials = getStoredCredentials();
- if (savedCredentials != null) {
- connection = createNewConnection();
- try {
- connection.login(savedCredentials.get("username").asText(), savedCredentials.get("password").asText());
- } catch (LoginFailedException e) {
- connection = null;
- }
- }
- }
-
- public ObjectNode getStoredCredentials() {
- ArrayNode array = (ArrayNode) prefStore.get(PREFERENCE_STORE_KEY);
- if (array != null && array.size() > 0 && array.get(0) instanceof ObjectNode) {
- return (ObjectNode) array.get(0);
- }
- return null;
- }
-
- public void logout() {
- prefStore.put(PREFERENCE_STORE_KEY, ParsingUtilities.mapper.createArrayNode());
- if (connection != null) {
- try {
- connection.logout();
- connection = null;
- } catch (IOException | MediaWikiApiErrorException e) {
- logger.error(e.getMessage());
- }
- }
- }
-
- public ApiConnection getConnection() {
- return connection;
- }
-
- public boolean isLoggedIn() {
- return connection != null;
- }
-
- public String getUsername() {
- if (connection != null) {
- return connection.getCurrentUser();
- } else {
- return null;
- }
- }
-
- /**
- * Creates a fresh connection object with our
- * prefered settings.
- * @return
- */
- protected BasicApiConnection createNewConnection() {
- BasicApiConnection conn = BasicApiConnection.getWikidataApiConnection();
- conn.setConnectTimeout(CONNECT_TIMEOUT);
- conn.setReadTimeout(READ_TIMEOUT);
- return conn;
- }
-}
diff --git a/extensions/wikidata/src/org/openrefine/wikidata/operations/PerformWikibaseEditsOperation.java b/extensions/wikidata/src/org/openrefine/wikidata/operations/PerformWikibaseEditsOperation.java
index bbdd10eb1..50a573cc1 100644
--- a/extensions/wikidata/src/org/openrefine/wikidata/operations/PerformWikibaseEditsOperation.java
+++ b/extensions/wikidata/src/org/openrefine/wikidata/operations/PerformWikibaseEditsOperation.java
@@ -34,7 +34,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.Validate;
-import org.openrefine.wikidata.editing.ConnectionManager;
+import org.openrefine.wikidata.commands.ConnectionManager;
import org.openrefine.wikidata.editing.EditBatchProcessor;
import org.openrefine.wikidata.editing.NewItemLibrary;
import org.openrefine.wikidata.schema.WikibaseSchema;
diff --git a/extensions/wikidata/tests/src/org/openrefine/wikidata/commands/LoginCommandTest.java b/extensions/wikidata/tests/src/org/openrefine/wikidata/commands/LoginCommandTest.java
index 330060650..070772864 100644
--- a/extensions/wikidata/tests/src/org/openrefine/wikidata/commands/LoginCommandTest.java
+++ b/extensions/wikidata/tests/src/org/openrefine/wikidata/commands/LoginCommandTest.java
@@ -1,43 +1,466 @@
package org.openrefine.wikidata.commands;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertEquals;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-
+import com.google.refine.ProjectManager;
+import com.google.refine.commands.Command;
+import com.google.refine.preference.PreferenceStore;
+import com.google.refine.util.ParsingUtilities;
+import org.mockito.ArgumentCaptor;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import org.wikidata.wdtk.wikibaseapi.BasicApiConnection;
+import org.wikidata.wdtk.wikibaseapi.LoginFailedException;
+import org.wikidata.wdtk.wikibaseapi.OAuthApiConnection;
+import org.wikidata.wdtk.wikibaseapi.apierrors.AssertUserFailedException;
+import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiApiErrorException;
-import com.google.refine.commands.Command;
-import com.google.refine.util.TestUtils;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Constructor;
+import java.net.HttpCookie;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import static com.google.refine.util.TestUtils.assertEqualAsJson;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+import static org.openrefine.wikidata.commands.LoginCommand.*;
+import static org.powermock.api.mockito.PowerMockito.*;
+import static org.testng.Assert.*;
+
+@PrepareForTest(ConnectionManager.class)
public class LoginCommandTest extends CommandTest {
-
- @BeforeMethod
- public void SetUp() {
- command = new LoginCommand();
+
+ private static final String username = "my_username";
+ private static final String password = "my_password";
+
+ private static final String consumerToken = "my_consumer_token";
+ private static final String consumerSecret = "my_consumer_secret";
+ private static final String accessToken = "my_access_token";
+ private static final String accessSecret = "my_access_secret";
+
+ private static final Map cookieMap = new HashMap<>();
+
+ static {
+ cookieMap.put("GeoIP", "TW:TXG:Taichung:24.15:120.68:v4");
+ cookieMap.put("WMF-Last-Access", "15-Jun-2020");
+ cookieMap.put("WMF-Last-Access-Global", "15-Jun-2020");
+ cookieMap.put("centralauth_Session", "centralauth_Session123");
+ cookieMap.put("centralauth_Token", "centralauth_Token123");
+ cookieMap.put("centralauth_User", username);
+ cookieMap.put("wikidatawikiSession", "wikidatawikiSession123");
+ cookieMap.put("wikidatawikiUserID", "123456");
+ cookieMap.put("wikidatawikiUserName", username);
}
-
+
+ private static final int ONE_YEAR = 60 * 60 * 24 * 365;
+
+ private ArgumentCaptor cookieCaptor;
+
+ // used for mocking singleton
+ Constructor constructor;
+
+ @BeforeClass
+ public void initConstructor() throws NoSuchMethodException {
+ constructor = ConnectionManager.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ }
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ command = new LoginCommand();
+
+ // mock the ConnectionManager singleton
+ ConnectionManager manager = constructor.newInstance();
+ mockStatic(ConnectionManager.class);
+ given(ConnectionManager.getInstance()).willReturn(manager);
+
+ when(request.getCookies()).thenReturn(new Cookie[]{});
+ cookieCaptor = ArgumentCaptor.forClass(Cookie.class);
+ doNothing().when(response).addCookie(cookieCaptor.capture());
+ }
+
+ @Test
+ public void testClearCredentialsInPreferences() throws Exception {
+ PreferenceStore prefStore = new PreferenceStore();
+ ProjectManager.singleton = mock(ProjectManager.class);
+ when(ProjectManager.singleton.getPreferenceStore()).thenReturn(prefStore);
+ prefStore.put(ConnectionManager.PREFERENCE_STORE_KEY, ParsingUtilities.mapper.createArrayNode());
+ assertNotNull(prefStore.get(ConnectionManager.PREFERENCE_STORE_KEY));
+ constructor.newInstance();
+ assertNull(prefStore.get(ConnectionManager.PREFERENCE_STORE_KEY));
+ }
+
@Test
public void testNoCredentials() throws ServletException, IOException {
- when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
-
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
command.doPost(request, response);
-
- assertEquals("{\"logged_in\":false,\"username\":null}", writer.toString());
+ // the first param is the actual one for testng.assertEquals
+ assertEquals(writer.toString(), "{\"logged_in\":false,\"username\":null}");
}
-
+
@Test
public void testCsrfProtection() throws ServletException, IOException {
- command.doPost(request, response);
- TestUtils.assertEqualAsJson("{\"code\":\"error\",\"message\":\"Missing or invalid csrf_token parameter\"}", writer.toString());
+ command.doPost(request, response);
+ assertEqualAsJson("{\"code\":\"error\",\"message\":\"Missing or invalid csrf_token parameter\"}", writer.toString());
}
-
+
@Test
public void testGetNotCsrfProtected() throws ServletException, IOException {
- command.doGet(request, response);
- TestUtils.assertEqualAsJson("{\"logged_in\":false,\"username\":null}", writer.toString());
+ command.doGet(request, response);
+ assertEqualAsJson("{\"logged_in\":false,\"username\":null}", writer.toString());
+ }
+
+ @Test
+ public void testUsernamePasswordLogin() throws Exception {
+ BasicApiConnection connection = mock(BasicApiConnection.class);
+ whenNew(BasicApiConnection.class).withAnyArguments().thenReturn(connection);
+ when(connection.getCurrentUser()).thenReturn(username);
+ when(connection.getCookies()).thenReturn(makeResponseCookies());
+ when(connection.getCookies()).thenReturn(makeResponseCookies());
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ when(request.getParameter(USERNAME)).thenReturn(username);
+ when(request.getParameter(PASSWORD)).thenReturn(password);
+
+ command.doPost(request, response);
+
+ verify(connection).login(username, password);
+ assertTrue(ConnectionManager.getInstance().isLoggedIn());
+ assertEqualAsJson("{\"logged_in\":true,\"username\":\"" + username + "\"}", writer.toString());
+
+ Map cookies = getCookieMap(cookieCaptor.getAllValues());
+ assertEquals(cookies.size(), 5);
+ assertCookieEquals(cookies.get(WIKIBASE_USERNAME_COOKIE_KEY), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_SECRET), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_SECRET), "", 0);
+ }
+
+ @Test
+ public void testUsernamePasswordLoginRememberCredentials() throws Exception {
+ BasicApiConnection connection = mock(BasicApiConnection.class);
+ whenNew(BasicApiConnection.class).withAnyArguments().thenReturn(connection);
+ when(connection.getCurrentUser()).thenReturn(username);
+ when(connection.getCookies()).thenReturn(makeResponseCookies());
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ when(request.getParameter("remember-credentials")).thenReturn("on");
+ when(request.getParameter(USERNAME)).thenReturn(username);
+ when(request.getParameter(PASSWORD)).thenReturn(password);
+
+ command.doPost(request, response);
+
+ verify(connection).login(username, password);
+ assertTrue(ConnectionManager.getInstance().isLoggedIn());
+ assertEqualAsJson("{\"logged_in\":true,\"username\":\"" + username + "\"}", writer.toString());
+
+ Map cookies = getCookieMap(cookieCaptor.getAllValues());
+ cookieMap.forEach((key, value) -> assertCookieEquals(cookies.get(WIKIDATA_COOKIE_PREFIX + key), value, ONE_YEAR));
+ assertCookieEquals(cookies.get(WIKIBASE_USERNAME_COOKIE_KEY), username, ONE_YEAR);
+ assertCookieEquals(cookies.get(CONSUMER_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_SECRET), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_SECRET), "", 0);
+ }
+
+ @Test
+ public void testUsernamePasswordLoginWithCookies() throws Exception {
+ BasicApiConnection connection = mock(BasicApiConnection.class);
+ given(ConnectionManager.convertToBasicApiConnection(anyMap())).willReturn(connection);
+ when(connection.getCurrentUser()).thenReturn(username);
+ when(connection.getCookies()).thenReturn(makeResponseCookies());
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ when(request.getCookies()).thenReturn(makeRequestCookies());
+
+ command.doPost(request, response);
+
+ verify(connection).checkCredentials();
+ assertTrue(ConnectionManager.getInstance().isLoggedIn());
+ assertEqualAsJson("{\"logged_in\":true,\"username\":\"" + username + "\"}", writer.toString());
+
+ Map cookies = getCookieMap(cookieCaptor.getAllValues());
+ assertEquals(cookies.size(), 4);
+ assertCookieEquals(cookies.get(CONSUMER_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_SECRET), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_SECRET), "", 0);
+ }
+
+ @Test
+ public void testOwnerOnlyConsumerLogin() throws Exception {
+ OAuthApiConnection connection = mock(OAuthApiConnection.class);
+ whenNew(OAuthApiConnection.class).withAnyArguments().thenReturn(connection);
+ when(connection.getCurrentUser()).thenReturn(username);
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ when(request.getParameter(CONSUMER_TOKEN)).thenReturn(consumerToken);
+ when(request.getParameter(CONSUMER_SECRET)).thenReturn(consumerSecret);
+ when(request.getParameter(ACCESS_TOKEN)).thenReturn(accessToken);
+ when(request.getParameter(ACCESS_SECRET)).thenReturn(accessSecret);
+
+ command.doPost(request, response);
+
+ assertTrue(ConnectionManager.getInstance().isLoggedIn());
+ assertEqualAsJson("{\"logged_in\":true,\"username\":\"" + username + "\"}", writer.toString());
+
+ Map cookies = getCookieMap(cookieCaptor.getAllValues());
+ assertEquals(cookies.size(), 5);
+ assertCookieEquals(cookies.get(WIKIBASE_USERNAME_COOKIE_KEY), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_SECRET), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_SECRET), "", 0);
+ }
+
+ @Test
+ public void testOwnerOnlyConsumerLoginRememberCredentials() throws Exception {
+ OAuthApiConnection connection = mock(OAuthApiConnection.class);
+ whenNew(OAuthApiConnection.class).withAnyArguments().thenReturn(connection);
+ when(connection.getCurrentUser()).thenReturn(username);
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ when(request.getParameter("remember-credentials")).thenReturn("on");
+ when(request.getParameter(CONSUMER_TOKEN)).thenReturn(consumerToken);
+ when(request.getParameter(CONSUMER_SECRET)).thenReturn(consumerSecret);
+ when(request.getParameter(ACCESS_TOKEN)).thenReturn(accessToken);
+ when(request.getParameter(ACCESS_SECRET)).thenReturn(accessSecret);
+ when(request.getCookies()).thenReturn(makeRequestCookies());
+
+ command.doPost(request, response);
+
+ assertEqualAsJson("{\"logged_in\":true,\"username\":\"" + username + "\"}", writer.toString());
+
+ Map cookies = getCookieMap(cookieCaptor.getAllValues());
+ // If logging in with owner-only consumer,
+ // cookies for the username/password login should be cleared.
+ cookieMap.forEach((key, value) -> assertCookieEquals(cookies.get(WIKIDATA_COOKIE_PREFIX + key), "", 0));
+ assertCookieEquals(cookies.get(WIKIBASE_USERNAME_COOKIE_KEY), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_TOKEN), consumerToken, ONE_YEAR);
+ assertCookieEquals(cookies.get(CONSUMER_SECRET), consumerSecret, ONE_YEAR);
+ assertCookieEquals(cookies.get(ACCESS_TOKEN), accessToken, ONE_YEAR);
+ assertCookieEquals(cookies.get(ACCESS_SECRET), accessSecret, ONE_YEAR);
+ }
+
+ @Test
+ public void testCookieEncoding() throws Exception {
+ OAuthApiConnection connection = mock(OAuthApiConnection.class);
+ whenNew(OAuthApiConnection.class).withAnyArguments().thenReturn(connection);
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ when(request.getParameter("remember-credentials")).thenReturn("on");
+ when(request.getParameter(CONSUMER_TOKEN)).thenReturn("malformed consumer token \r\n %?");
+ when(request.getParameter(CONSUMER_SECRET)).thenReturn(consumerSecret);
+ when(request.getParameter(ACCESS_TOKEN)).thenReturn(accessToken);
+ when(request.getParameter(ACCESS_SECRET)).thenReturn(accessSecret);
+ when(request.getCookies()).thenReturn(makeRequestCookies());
+
+ command.doPost(request, response);
+
+ Map cookies = getCookieMap(cookieCaptor.getAllValues());
+ assertNotEquals(cookies.get(CONSUMER_TOKEN).getValue(), "malformed consumer token \r\n %?");
+ assertEquals(cookies.get(CONSUMER_TOKEN).getValue(), "malformed+consumer+token+%0D%0A+%25%3F");
+ }
+
+ @Test
+ public void testOwnerOnlyConsumerLoginWithCookies() throws Exception {
+ OAuthApiConnection connection = mock(OAuthApiConnection.class);
+ whenNew(OAuthApiConnection.class).withAnyArguments().thenReturn(connection);
+ when(connection.getCurrentUser()).thenReturn(username);
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ Cookie consumerTokenCookie = new Cookie(CONSUMER_TOKEN, consumerToken);
+ Cookie consumerSecretCookie = new Cookie(CONSUMER_SECRET, consumerSecret);
+ Cookie accessTokenCookie = new Cookie(ACCESS_TOKEN, accessToken);
+ Cookie accessSecretCookie = new Cookie(ACCESS_SECRET, accessSecret);
+ when(request.getCookies()).thenReturn(new Cookie[]{consumerTokenCookie, consumerSecretCookie, accessTokenCookie, accessSecretCookie});
+ command.doPost(request, response);
+
+ assertTrue(ConnectionManager.getInstance().isLoggedIn());
+ assertEqualAsJson("{\"logged_in\":true,\"username\":\"" + username + "\"}", writer.toString());
+
+ Map cookies = getCookieMap(cookieCaptor.getAllValues());
+ assertEquals(cookies.size(), 5);
+ assertCookieEquals(cookies.get(WIKIBASE_USERNAME_COOKIE_KEY), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_TOKEN), consumerToken, ONE_YEAR);
+ assertCookieEquals(cookies.get(CONSUMER_SECRET), consumerSecret, ONE_YEAR);
+ assertCookieEquals(cookies.get(ACCESS_TOKEN), accessToken, ONE_YEAR);
+ assertCookieEquals(cookies.get(ACCESS_SECRET), accessSecret, ONE_YEAR);
+ }
+
+ @Test
+ public void testCookieDecoding() throws Exception {
+ ConnectionManager manager = mock(ConnectionManager.class);
+ given(ConnectionManager.getInstance()).willReturn(manager);
+
+ OAuthApiConnection connection = mock(OAuthApiConnection.class);
+ whenNew(OAuthApiConnection.class).withAnyArguments().thenReturn(connection);
+ when(connection.getCurrentUser()).thenReturn(username);
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ Cookie consumerTokenCookie = new Cookie(CONSUMER_TOKEN, "malformed+consumer+token+%0D%0A+%25%3F");
+ Cookie consumerSecretCookie = new Cookie(CONSUMER_SECRET, consumerSecret);
+ Cookie accessTokenCookie = new Cookie(ACCESS_TOKEN, accessToken);
+ Cookie accessSecretCookie = new Cookie(ACCESS_SECRET, accessSecret);
+ when(request.getCookies()).thenReturn(new Cookie[]{consumerTokenCookie, consumerSecretCookie, accessTokenCookie, accessSecretCookie});
+
+ command.doPost(request, response);
+
+ verify(manager).login("malformed consumer token \r\n %?", consumerSecret, accessToken, accessSecret);
+ }
+
+ @Test
+ public void testLogout() throws Exception {
+ BasicApiConnection connection = mock(BasicApiConnection.class);
+ whenNew(BasicApiConnection.class).withAnyArguments().thenReturn(connection);
+ when(connection.getCurrentUser()).thenReturn(username);
+ when(connection.getCookies()).thenReturn(makeResponseCookies());
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ when(request.getParameter(USERNAME)).thenReturn(username);
+ when(request.getParameter(PASSWORD)).thenReturn(password);
+
+ // login first
+ command.doPost(request, response);
+
+ int loginCookiesSize = cookieCaptor.getAllValues().size();
+
+ assertTrue(ConnectionManager.getInstance().isLoggedIn());
+ assertEqualAsJson("{\"logged_in\":true,\"username\":\"" + username + "\"}", writer.toString());
+
+ // logout
+ when(request.getParameter("logout")).thenReturn("true");
+ when(request.getCookies()).thenReturn(makeRequestCookies()); // will be cleared
+ StringWriter logoutWriter = new StringWriter();
+ when(response.getWriter()).thenReturn(new PrintWriter(logoutWriter));
+
+ command.doPost(request, response);
+
+ assertFalse(ConnectionManager.getInstance().isLoggedIn());
+ assertEqualAsJson("{\"logged_in\":false,\"username\":null}", logoutWriter.toString());
+
+ Map cookies = getCookieMap(cookieCaptor.getAllValues().subList(loginCookiesSize, cookieCaptor.getAllValues().size()));
+ cookieMap.forEach((key, value) -> assertCookieEquals(cookies.get(WIKIDATA_COOKIE_PREFIX + key), "", 0));
+ assertCookieEquals(cookies.get(WIKIBASE_USERNAME_COOKIE_KEY), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(CONSUMER_SECRET), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_TOKEN), "", 0);
+ assertCookieEquals(cookies.get(ACCESS_SECRET), "", 0);
+ }
+
+ @Test
+ public void testUsernamePasswordLoginFailed() throws Exception {
+ BasicApiConnection connection = mock(BasicApiConnection.class);
+ whenNew(BasicApiConnection.class).withAnyArguments().thenReturn(connection);
+ doThrow(new LoginFailedException("login failed")).when(connection).login(username, password);
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ // we don't check the username/password here
+ when(request.getParameter(USERNAME)).thenReturn(username);
+ when(request.getParameter(PASSWORD)).thenReturn(password);
+
+ // login first
+ command.doPost(request, response);
+
+ verify(connection).login(username, password);
+ assertFalse(ConnectionManager.getInstance().isLoggedIn());
+ }
+
+ @Test
+ public void testUsernamePasswordWithCookiesLoginFailed() throws Exception {
+ BasicApiConnection connection = mock(BasicApiConnection.class);
+ given(ConnectionManager.convertToBasicApiConnection(anyMap())).willReturn(connection);
+ doThrow(new AssertUserFailedException("assert user login failed")).when(connection).checkCredentials();
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ // we don't check the username/password here
+ when(request.getCookies()).thenReturn(makeRequestCookies());
+
+ // login first
+ command.doPost(request, response);
+
+ verify(connection).checkCredentials();
+ assertFalse(ConnectionManager.getInstance().isLoggedIn());
+ }
+
+ @Test
+ public void testOwnerOnlyConsumerLoginFailed() throws Exception {
+ OAuthApiConnection connection = mock(OAuthApiConnection.class);
+ whenNew(OAuthApiConnection.class).withAnyArguments().thenReturn(connection);
+ doThrow(new AssertUserFailedException("assert user login failed")).when(connection).checkCredentials();
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ when(request.getParameter(CONSUMER_TOKEN)).thenReturn(consumerToken);
+ when(request.getParameter(CONSUMER_SECRET)).thenReturn(consumerSecret);
+ when(request.getParameter(ACCESS_TOKEN)).thenReturn(accessToken);
+ when(request.getParameter(ACCESS_SECRET)).thenReturn(accessSecret);
+
+ command.doPost(request, response);
+
+ verify(connection).checkCredentials();
+ assertFalse(connection.isLoggedIn());
+ }
+
+ @Test
+ public void testLogoutFailed() throws Exception {
+ BasicApiConnection connection = mock(BasicApiConnection.class);
+ whenNew(BasicApiConnection.class).withAnyArguments().thenReturn(connection);
+ when(connection.getCurrentUser()).thenReturn(username);
+
+ when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
+ when(request.getParameter(USERNAME)).thenReturn(username);
+ when(request.getParameter(PASSWORD)).thenReturn(password);
+
+ // login first
+ command.doPost(request, response);
+
+ assertTrue(ConnectionManager.getInstance().isLoggedIn());
+
+ // logout
+ when(request.getParameter("logout")).thenReturn("true");
+ doThrow(new MediaWikiApiErrorException("", "")).when(connection).logout();
+ command.doPost(request, response);
+
+ // still logged in
+ assertTrue(ConnectionManager.getInstance().isLoggedIn());
+ }
+
+ private static Cookie[] makeRequestCookies() {
+ List cookies = new ArrayList<>();
+ cookieMap.forEach((key, value) -> cookies.add(new Cookie(WIKIDATA_COOKIE_PREFIX + key, value)));
+ cookies.add(new Cookie(WIKIBASE_USERNAME_COOKIE_KEY, username));
+ return cookies.toArray(new Cookie[0]);
+ }
+
+ private static List makeResponseCookies() {
+ List cookies = new ArrayList<>();
+ cookieMap.forEach((key, value) -> cookies.add(new HttpCookie(key, value)));
+ return cookies;
+ }
+
+ private static void assertCookieEquals(Cookie cookie, String expectedValue, int expectedMaxAge) {
+ try {
+ assertEquals(getCookieValue(cookie), expectedValue);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ assertEquals(cookie.getMaxAge(), expectedMaxAge);
+ assertEquals(cookie.getPath(), "/");
+ }
+
+ private static Map getCookieMap(List cookies) {
+ Map map = new HashMap<>();
+ cookies.forEach(cookie -> map.put(cookie.getName(), cookie));
+ return map;
}
}
diff --git a/pom.xml b/pom.xml
index b5c7c8ce4..de5038fff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -215,6 +215,20 @@
see main/pom.xml, server/pom.xml and each extension.
-->
-
+
+
+
+
+ snapshots
+ https://oss.sonatype.org/content/repositories/snapshots/
+
+ false
+
+
+ true
+
+
+
+