OAuth support for the Wikidata extension (#2661)

Closes #1612 

Merges @afkbrb work to finish @wetneb protype, including:
* adding support for OAuth in addition to basic auth
* supporting registration of single user Wikidata OAuth consumers.
* adding extensive tests
* cleaning up the login dialog

This depends on the next release of the Wikidata Toolkit, but we're currently publishing our own snapshot that includes the necessary changes (thanks @wetneb!)

Co-authored-by: Antonin Delpeuch <antonin@delpeuch.eu>
This commit is contained in:
Lu Liu 2020-06-16 10:08:36 +08:00 committed by Tom Morris
parent f88c0e3657
commit 335e81a26c
30 changed files with 1244 additions and 426 deletions

View File

@ -48,7 +48,7 @@ function init() {
RefineServlet.registerCommand(module, "preview-wikibase-schema", new PreviewWikibaseSchemaCommand());
RefineServlet.registerCommand(module, "perform-wikibase-edits", new PerformWikibaseEditsCommand());
RefineServlet.registerCommand(module, "login", new LoginCommand());
/*
* Resources
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -43,9 +43,7 @@
"wikidata-account/dialog-header": "উইকিউপাত্ত অ্যাকাউন্ট",
"wikidata-account/explain-log-in": "<a href=\"https://www.wikidata.org/\" target=\"_blank\">উইকিউপাত্তে</a> প্রবেশ করলে সরাসরি ওপেনরিফাইন থেকে আপনাকে সম্পাদনা আপলোড করতে দেওয়া হবে।",
"wikidata-account/username-label": "ব্যবহারকারী নাম:",
"wikidata-account/username-placeholder": "আপনার ব্যবহারকারী নাম অন্তর্ভুক্ত করুন",
"wikidata-account/password-label": "পাসওয়ার্ড:",
"wikidata-account/password-placeholder": "আপনার পাসওয়ার্ড অন্তর্ভুক্ত করুন",
"wikidata-account/close": "বন্ধ করুন",
"wikidata-account/log-in": "প্রবেশ করুন",
"wikidata-account/log-out": "প্রস্থান করুন"

View File

@ -65,11 +65,25 @@
"wikidata-preview/new-id": "new item",
"wikidata-account/dialog-header": "Wikidata account",
"wikidata-account/explain-log-in": "Logging in to <a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a> lets you to upload edits directly from OpenRefine.",
"wikidata-account/explain-owner-only-consumer-wiki": "See this <a href=\"https://github.com/OpenRefine/OpenRefine/wiki/Wikidata-owner-only-consumer\" target=\"_blank\">wiki</a> to get your owner-only consumer if you don't have one.",
"wikidata-account/explain-password-login": "You can also <a>login with your username/password.</a>",
"wikidata-account/explain-owner-only-consumer-login": "You can also <a>login with your owner-only consumer.</a>",
"wikidata-account/invalid-credentials": "Invalid credentials",
"wikidata-account/username-label": "Username:",
"wikidata-account/username-placeholder": "Enter your username",
"wikidata-account/username-placeholder": "username",
"wikidata-account/password-label": "Password:",
"wikidata-account/password-placeholder": "Enter your password",
"wikidata-account/remember-credentials-label": "Remember credentials (stored unencrypted in OpenRefine's preferences)",
"wikidata-account/password-placeholder": "password",
"wikidata-account/consumer-token-label": "Consumer token:",
"wikidata-account/consumer-token-placeholder": "consumer token",
"wikidata-account/consumer-secret-label": "Consumer secret:",
"wikidata-account/consumer-secret-placeholder": "consumer secret",
"wikidata-account/access-token-label": "Access token:",
"wikidata-account/access-token-placeholder": "access token",
"wikidata-account/access-secret-label": "Access secret:",
"wikidata-account/access-secret-placeholder": "access secret",
"wikidata-account/remember-me": "Remember me",
"wikidata-account/password-remember-me-title": "Your password won't be stored.",
"wikidata-account/owner-only-consumer-remember-me-title": "Consumer credentials are stored encrypted in cookies.",
"wikidata-account/close": "Close",
"wikidata-account/log-in": "Log in",
"wikidata-account/logged-in-as": "You are logged in as:",

View File

@ -95,17 +95,31 @@
"perform-wikidata-edits/review-your-edits": "You are about to upload {nb_edits} edits to Wikidata. Please check them carefully. Large edit batches should be submitted for <a href=\"https://www.wikidata.org/wiki/Wikidata:Requests_for_permissions/Bot\" target=\"_blank\">bot review</a> first.",
"perform-wikidata-edits/dialog-header": "Upload edits to Wikidata",
"wikidata-account/connecting-to-wikidata": "Connecting to Wikidata…",
"wikidata-account/log-out": "Log out",
"wikidata-account/logged-in-as": "You are logged in as:",
"wikidata-account/log-in": "Log in",
"wikidata-account/close": "Close",
"wikidata-account/remember-credentials-label": "Remember credentials (stored unencrypted in OpenRefine's preferences)",
"wikidata-account/password-placeholder": "Enter your password",
"wikidata-account/password-label": "Password:",
"wikidata-account/username-placeholder": "Enter your username",
"wikidata-account/username-label": "Username:",
"wikidata-account/explain-log-in": "Logging in to <a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a> lets you to upload edits directly from OpenRefine.",
"wikidata-account/dialog-header": "Wikidata account",
"wikidata-account/explain-log-in": "Logging in to <a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a> lets you to upload edits directly from OpenRefine.",
"wikidata-account/explain-owner-only-consumer-wiki": "See this <a href=\"https://github.com/OpenRefine/OpenRefine/wiki/Wikidata-owner-only-consumer\" target=\"_blank\">wiki</a> to get your owner-only consumer if you don't have one.",
"wikidata-account/explain-password-login": "You can also <a>login with your username/password.</a>",
"wikidata-account/explain-owner-only-consumer-login": "You can also <a>login with your owner-only consumer.</a>",
"wikidata-account/invalid-credentials": "Invalid credentials",
"wikidata-account/username-label": "Username:",
"wikidata-account/username-placeholder": "username",
"wikidata-account/password-label": "Password:",
"wikidata-account/password-placeholder": "password",
"wikidata-account/consumer-token-label": "Consumer token:",
"wikidata-account/consumer-token-placeholder": "consumer token",
"wikidata-account/consumer-secret-label": "Consumer secret:",
"wikidata-account/consumer-secret-placeholder": "consumer secret",
"wikidata-account/access-token-label": "Access token:",
"wikidata-account/access-token-placeholder": "access token",
"wikidata-account/access-secret-label": "Access secret:",
"wikidata-account/access-secret-placeholder": "access secret",
"wikidata-account/remember-me": "Remember me",
"wikidata-account/password-remember-me-title": "Your password won't be stored.",
"wikidata-account/owner-only-consumer-remember-me-title": "Consumer credentials are stored encrypted in cookies.",
"wikidata-account/close": "Close",
"wikidata-account/log-in": "Log in",
"wikidata-account/logged-in-as": "You are logged in as:",
"wikidata-account/log-out": "Log out",
"wikidata-preview/new-id": "new item",
"wikidata-schema/unsaved-warning": "You have made unsaved changes to your Wikidata schema. Close anyway?",
"wikidata-schema/incomplete-schema-could-not-be-saved": "Your schema is incomplete so it cannot be saved yet.",

View File

@ -59,10 +59,7 @@
"wikidata-account/dialog-header": "compte Wikidata",
"wikidata-account/explain-log-in": "Se connecter à <a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a> vous permet de publier vos données sur Wikidata depuis OpenRefine.",
"wikidata-account/username-label": "Nom d'utilisateur :",
"wikidata-account/username-placeholder": "Entrez votre nom d'utilisateur",
"wikidata-account/password-label": "Mot de passe :",
"wikidata-account/password-placeholder": "Entrez votre mot de passe",
"wikidata-account/remember-credentials-label": "Se souvenir des identifiants (enregistrés sans chiffrement dans les préférences)",
"wikidata-account/close": "Fermer",
"wikidata-account/log-in": "Se connecter",
"wikidata-account/logged-in-as": "Vous êtes connecté·e en tant que :",

View File

@ -52,10 +52,7 @@
"wikidata-account/dialog-header": "account su Wikidata",
"wikidata-account/explain-log-in": "Accedere a <a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a> ti permette di caricare i tuoi edit direttamente tramite OpenRefine.",
"wikidata-account/username-label": "Nome utente:",
"wikidata-account/username-placeholder": "Inserisci il tuo nome utente",
"wikidata-account/password-label": "Password:",
"wikidata-account/password-placeholder": "Inserisci la tua password",
"wikidata-account/remember-credentials-label": "Ricorda questi dati (verranno mantenuti in forma criptata nelle preferenze di OpenRefine",
"wikidata-account/close": "Chiudi",
"wikidata-account/log-in": "Entra",
"wikidata-account/logged-in-as": "Sei registrato come:",

View File

@ -56,10 +56,7 @@
"wikidata-account/dialog-header": "Wikidataアカウント",
"wikidata-account/explain-log-in": "OpenRefineから直接データをアップロードするため、<a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a>にログインする.",
"wikidata-account/username-label": "ユーザー名:",
"wikidata-account/username-placeholder": "ユーザー名を入力",
"wikidata-account/password-label": "パスワード:",
"wikidata-account/password-placeholder": "パスワードを入力",
"wikidata-account/remember-credentials-label": "credentialsをOpenRefineの設定に保存する",
"wikidata-account/close": "閉じる",
"wikidata-account/log-in": "ログイン",
"wikidata-account/logged-in-as": "ログイン名:",

View File

@ -54,10 +54,7 @@
"wikidata-account/dialog-header": "Wikidata 계정",
"wikidata-account/explain-log-in": "<a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a> 로그인하세요. OpenRefine 에서 직접 편집을 업로드할 수 있습니다.",
"wikidata-account/username-label": "사용자이름:",
"wikidata-account/username-placeholder": "사용자이름을 입력하세요",
"wikidata-account/password-label": "비밀번호:",
"wikidata-account/password-placeholder": "비밀번호를 입력하세요",
"wikidata-account/remember-credentials-label": "credentials을 기억하세요 (OpenRefine의 Preferences에 암호화되지 않고 저장됩니다)",
"wikidata-account/close": "닫기",
"wikidata-account/log-in": "로그인",
"wikidata-account/logged-in-as": "다음으로 로그인 됨:",

View File

@ -54,10 +54,7 @@
"wikidata-account/dialog-header": "Wikidata-konto",
"wikidata-account/explain-log-in": "Hvis du logger inn på <a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a> kan du laste opp redigeringer direkte fra OpenRefine.",
"wikidata-account/username-label": "Brukernavn:",
"wikidata-account/username-placeholder": "Skriv inn brukernavnet ditt",
"wikidata-account/password-label": "Passord:",
"wikidata-account/password-placeholder": "Skriv inn passordet ditt",
"wikidata-account/remember-credentials-label": "Husk meg (lagres ukryptert i OpenRefines innstillinger)",
"wikidata-account/close": "Lukk",
"wikidata-account/log-in": "Logg inn",
"wikidata-account/logged-in-as": "Du er innlogget som:",

View File

@ -53,10 +53,7 @@
"wikidata-account/dialog-header": "Wikidata-account",
"wikidata-account/explain-log-in": "Aanmelden bij <a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a> laat u direct bewerkingen vanuit OpenRefine uploaden.",
"wikidata-account/username-label": "Gebruikersnaam:",
"wikidata-account/username-placeholder": "Vul uw gebruikersnaam in",
"wikidata-account/password-label": "Wachtwoord:",
"wikidata-account/password-placeholder": "Voer uw wachtwoord in",
"wikidata-account/remember-credentials-label": "Gegevens onthouden (ongecodeerd opgeslagen in de voorkeuren van OpenRefine)",
"wikidata-account/close": "Sluiten",
"wikidata-account/log-in": "Aanmelden",
"wikidata-account/logged-in-as": "U bent aangemeld als:",

View File

@ -59,10 +59,7 @@
"wikidata-preview/new-id": "nytt objekt",
"wikidata-account/explain-log-in": "Logga in på <a href=\"https://www.wikidata.org/\" target=\"_blank\">Wikidata</a> så att du kan ladda upp redigeringar direkt från OpenRefine.",
"wikidata-account/username-label": "Användarnamn:",
"wikidata-account/username-placeholder": "Skriv in ditt användarnamn",
"wikidata-account/password-label": "Lösenord:",
"wikidata-account/password-placeholder": "Skriv in ditt lösenord",
"wikidata-account/remember-credentials-label": "Kom ihåg inloggningsuppgifter (lagras okrypterade i OpenRefines inställningar)",
"wikidata-account/connecting-to-wikidata": "Anslut till Wikidata…",
"perform-wikidata-edits/dialog-header": "Överför redigeringar till Wikidata",
"perform-wikidata-edits/review-your-edits": "Du håller på att ladda upp {nb_edits} redigeringar till Wikidata. Kontrollera dem noggrant. Stora antal av ändringar bör skickas till <a href=\"https://www.wikidata.org/wiki/Wikidata:Requests_for_permissions/Bot\" target=\"_blank\">botgranskning</a> först.",

View File

@ -5,7 +5,7 @@
<label for="schema" bind="schemaLabel"></label><br />
<textarea name="schema" class="wikibase-schema-textarea" bind="schemaTextarea"></textarea><br />
<div class="wikibase-invalid-schema" bind="invalidSchema"></div>
<div class="wikibase-login-buttons">
<div class="wikibase-import-schema-buttons">
<button class="button cancel-button" bind="cancelButton"></button>
<button class="button button-primary" bind="importButton"></button>
</div>

View File

@ -0,0 +1,25 @@
<div class="dialog-frame" style="width: 800px;">
<div class="dialog-header" bind="dialogHeader"></div>
<div class="dialog-body" bind="dialogBody" style="position: relative; height: 100px">
<div class="wikidata-logo">
<a href="https://www.wikidata.org/" target="_blank">
<img src="extension/wikidata/images/wikidata.png" style="height: 100px" alt="Wikidata logo"/>
</a>
</div>
<div class="right-of-logo">
<div class="wikibase-user-management-area">
<div class="wikibase-user-logout" bind="logoutArea">
<p><span bind="loggedInAs"></span>
<a bind="loggedInUsername" target="_blank"></a></p>
<div class="wikibase-login-buttons">
<button class="button cancel-button" bind="cancelButton"></button>
<button class="button button-primary" bind="logoutButton"></button>
</div>
</div>
</div>
</div>
</div>
<div class="dialog-footer" style="text-align: center">
<span bind="explainLogIn"></span>
</div>
</div>

View File

@ -1,45 +0,0 @@
<div class="dialog-frame" style="width: 800px;">
<div class="dialog-header" bind="dialogHeader"></div>
<div class="dialog-body" bind="dialogBody">
<a href="https://www.wikidata.org/" target="_blank">
<img src="extension/wikidata/images/wikidata.png" class="wikidata-logo" alt="Wikidata logo" />
</a>
<div class="right-of-logo">
<p class="body-text" bind="explainLogIn">
</p>
<div class="wikibase-user-management-area">
<div class="wikibase-user-login" bind="loginArea">
<div bind="invalidCredentials" class="wikibase-invalid-credentials"></div>
<form bind="loginForm" class="wikibase-login-form" method="post">
<table>
<tr>
<td><label for="wb-username" bind="usernameLabel"></label></td>
<td><input name="wb-username" type="text" bind="usernameInput" /></td>
</tr>
<tr>
<td><label for="wb-password" bind="passwordLabel"></label></td>
<td><input name="wb-password" type="password" bind="passwordInput" /></td>
</tr>
<tr>
<td><input type="checkbox" name="remember-credentials" /></td>
<td><label for="remember-credentials" bind="rememberCredentialsLabel"></label></td>
</tr>
</table>
<div class="wikibase-login-buttons">
<button class="button cancel-button" type="button" bind="cancelButton1"></button>
<input class="button button-primary" type="submit" bind="loginButton"></input>
</div>
</form>
</div>
<div class="wikibase-user-logout" bind="logoutArea">
<p><span bind="loggedInAs"></span>
<a bind="loggedInUsername" target="_blank"></a></p>
<div class="wikibase-login-buttons">
<button class="button cancel-button" bind="cancelButton2"></button>
<button class="button button-primary" bind="logoutButton"></button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -2,120 +2,210 @@ var ManageAccountDialog = {};
ManageAccountDialog.firstLogin = true;
ManageAccountDialog.launch = function(logged_in_username, callback) {
$.post(
"command/core/get-all-preferences",
null,
function (preferences) {
ManageAccountDialog.display(logged_in_username, preferences.wikidata_credentials, callback);
},
"json"
);
};
ManageAccountDialog.display = function (logged_in_username, callback) {
ManageAccountDialog.display = function(logged_in_username, saved_credentials, callback) {
var self = this;
var frame = $(DOM.loadHTML("wikidata", "scripts/dialogs/manage-account-dialog.html"));
var elmts = this._elmts = DOM.bind(frame);
this._elmts.dialogHeader.text($.i18n('wikidata-account/dialog-header'));
this._elmts.explainLogIn.html($.i18n('wikidata-account/explain-log-in'));
this._elmts.usernameLabel.text($.i18n('wikidata-account/username-label'));
this._elmts.usernameInput.attr("placeholder", $.i18n('wikidata-account/username-placeholder'));
this._elmts.passwordLabel.text($.i18n('wikidata-account/password-label'));
this._elmts.passwordInput.attr("placeholder", $.i18n('wikidata-account/password-placeholder'));
this._elmts.rememberCredentialsLabel.text($.i18n('wikidata-account/remember-credentials-label'));
this._elmts.dialogHeader.text($.i18n('wikidata-account/dialog-header'));
this._elmts.cancelButton1.text($.i18n('wikidata-account/close'));
this._elmts.cancelButton2.text($.i18n('wikidata-account/close'));
this._elmts.loggedInAs.text($.i18n('wikidata-account/logged-in-as'));
this._elmts.logoutButton.text($.i18n('wikidata-account/log-out'));
this._elmts.loginButton.val($.i18n('wikidata-account/log-in'));
if (logged_in_username != null) {
elmts.loginArea.remove();
} else {
elmts.logoutArea.remove();
if (logged_in_username == null) {
logged_in_username = ManageAccountDialog.tryLoginWithCookies(callback);
}
this._level = DialogSystem.showDialog(frame);
this._elmts.usernameInput.focus();
if (logged_in_username != null) {
ManageAccountDialog.displayLoggedIn(logged_in_username, callback);
} else {
ManageAccountDialog.displayPasswordLogin(callback);
}
};
var dismiss = function() {
DialogSystem.dismissUntil(self._level - 1);
ManageAccountDialog.tryLoginWithCookies = function (callback) {
var logged_user_name = null;
$.ajaxSetup({async: false});
Refine.postCSRF(
"command/wikidata/login",
{},
function (data) {
if (data.logged_in) {
callback(data.username);
logged_user_name = data.username;
} else {
logged_user_name = null;
}
});
$.ajaxSetup({async: true});
return logged_user_name;
};
ManageAccountDialog.initCommon = function (elmts) {
elmts.dialogHeader.text($.i18n('wikidata-account/dialog-header'));
elmts.explainLogIn.html($.i18n('wikidata-account/explain-log-in'));
elmts.cancelButton.text($.i18n('wikidata-account/close'));
};
ManageAccountDialog.displayLoggedIn = function (logged_in_username, callback) {
var frame = $(DOM.loadHTML("wikidata", "scripts/dialogs/logged-in-dialog.html"));
var elmts = DOM.bind(frame);
ManageAccountDialog.initCommon(elmts);
elmts.loggedInAs.text($.i18n('wikidata-account/logged-in-as'));
elmts.logoutButton.text($.i18n('wikidata-account/log-out'));
var level = DialogSystem.showDialog(frame);
var dismiss = function () {
DialogSystem.dismissUntil(level - 1);
};
elmts.loggedInUsername
.text(logged_in_username)
.attr('href', 'https://www.wikidata.org/wiki/User:'+logged_in_username);
.text(logged_in_username)
.attr('href', 'https://www.wikidata.org/wiki/User:' + logged_in_username);
elmts.cancelButton1.click(function(e) {
dismiss();
callback(null);
});
elmts.cancelButton2.click(function(e) {
dismiss();
callback(null);
elmts.cancelButton.click(function (e) {
dismiss();
callback(null);
});
elmts.loginForm.submit(function(e) {
frame.hide();
Refine.postCSRF(
elmts.logoutButton.click(function () {
frame.hide();
Refine.postCSRF(
"command/wikidata/login",
"logout=true",
function (data) {
frame.show();
if (!data.logged_in) {
dismiss();
callback(null);
}
});
});
};
ManageAccountDialog.displayPasswordLogin = function (callback) {
const frame = $(DOM.loadHTML("wikidata", "scripts/dialogs/password-login-dialog.html"));
const elmts = DOM.bind(frame);
ManageAccountDialog.initCommon(elmts);
elmts.explainOwnerOnlyConsumerLogin.html($.i18n('wikidata-account/explain-owner-only-consumer-login'));
elmts.invalidCredentials.text($.i18n('wikidata-account/invalid-credentials'));
elmts.invalidCredentials.hide();
elmts.usernameLabel.text($.i18n('wikidata-account/username-label'));
elmts.usernameInput.attr("placeholder", $.i18n('wikidata-account/username-placeholder'));
elmts.passwordLabel.text($.i18n('wikidata-account/password-label'));
elmts.passwordInput.attr("placeholder", $.i18n('wikidata-account/password-placeholder'));
elmts.rememberMe.text($.i18n('wikidata-account/remember-me'));
elmts.passwordRememberMeTitle.attr("title", $.i18n('wikidata-account/password-remember-me-title'));
elmts.loginButton.text($.i18n('wikidata-account/log-in'));
elmts.usernameInput.focus();
var level = DialogSystem.showDialog(frame);
var dismiss = function () {
DialogSystem.dismissUntil(level - 1);
};
elmts.cancelButton.click(function (e) {
dismiss();
callback(null);
});
elmts.explainOwnerOnlyConsumerLogin.click(function (e) {
dismiss();
ManageAccountDialog.displayOwnerOnlyConsumerLogin(callback);
});
elmts.loginForm.submit(function (e) {
frame.hide();
Refine.postCSRF(
"command/wikidata/login",
elmts.loginForm.serialize(),
function(data) {
function (data) {
if (data.logged_in) {
dismiss();
callback(data.username);
} else {
frame.show();
elmts.invalidCredentials.show();
}
else {
frame.show();
elmts.invalidCredentials.text("Invalid credentials.");
}
});
e.preventDefault();
}
);
elmts.logoutButton.click(function() {
Refine.postCSRF(
"command/wikidata/login",
"logout=true",
function(data) {
if (!data.logged_in) {
dismiss();
callback(null);
}
});
});
e.preventDefault();
});
};
ManageAccountDialog.isLoggedIn = function(callback) {
var discardWaiter = function() { };
if(ManageAccountDialog.firstLogin) {
discardWaiter = DialogSystem.showBusy($.i18n('wikidata-account/connecting-to-wikidata'));
}
$.get(
"command/wikidata/login",
function(data) {
discardWaiter();
ManageAccountDialog.firstLogin = false;
callback(data.username);
});
ManageAccountDialog.displayOwnerOnlyConsumerLogin = function (callback) {
var frame = $(DOM.loadHTML("wikidata", "scripts/dialogs/owner-only-consumer-login-dialog.html"));
var elmts = DOM.bind(frame);
ManageAccountDialog.initCommon(elmts);
elmts.explainOwnerOnlyConsumerWiki.html($.i18n('wikidata-account/explain-owner-only-consumer-wiki'));
elmts.explainPasswordLogin.html($.i18n('wikidata-account/explain-password-login'));
elmts.invalidCredentials.text($.i18n('wikidata-account/invalid-credentials'));
elmts.invalidCredentials.hide();
elmts.consumerTokenLabel.text($.i18n('wikidata-account/consumer-token-label'));
elmts.consumerTokenInput.attr("placeholder", $.i18n('wikidata-account/consumer-token-placeholder'));
elmts.consumerSecretLabel.text($.i18n('wikidata-account/consumer-secret-label'));
elmts.consumerSecretInput.attr("placeholder", $.i18n('wikidata-account/consumer-secret-placeholder'));
elmts.accessTokenLabel.text($.i18n('wikidata-account/access-token-label'));
elmts.accessTokenInput.attr("placeholder", $.i18n('wikidata-account/access-token-placeholder'));
elmts.accessSecretLabel.text($.i18n('wikidata-account/access-secret-label'));
elmts.accessSecretInput.attr("placeholder", $.i18n('wikidata-account/access-secret-placeholder'));
elmts.rememberMe.text($.i18n('wikidata-account/remember-me'));
elmts.ownerOnlyConsumerRememberMeTitle.attr("title", $.i18n('wikidata-account/owner-only-consumer-remember-me-title'));
elmts.loginButton.text($.i18n('wikidata-account/log-in'));
elmts.consumerTokenInput.focus();
var level = DialogSystem.showDialog(frame);
var dismiss = function () {
DialogSystem.dismissUntil(level - 1);
};
elmts.cancelButton.click(function (e) {
dismiss();
callback(null);
});
elmts.explainPasswordLogin.click(function (e) {
dismiss();
ManageAccountDialog.displayPasswordLogin(callback);
});
elmts.loginForm.submit(function (e) {
frame.hide();
Refine.postCSRF(
"command/wikidata/login",
elmts.loginForm.serialize(),
function (data) {
if (data.logged_in) {
dismiss();
callback(data.username);
} else {
frame.show();
elmts.invalidCredentials.show();
}
});
e.preventDefault();
});
};
ManageAccountDialog.ensureLoggedIn = function(callback) {
ManageAccountDialog.isLoggedIn(function(logged_in_username) {
if (logged_in_username == null) {
ManageAccountDialog.launch(null, callback);
} else {
callback(logged_in_username);
}
});
ManageAccountDialog.isLoggedIn = function (callback) {
var discardWaiter = function () {
};
if (ManageAccountDialog.firstLogin) {
discardWaiter = DialogSystem.showBusy($.i18n('wikidata-account/connecting-to-wikidata'));
}
$.get(
"command/wikidata/login",
function (data) {
discardWaiter();
ManageAccountDialog.firstLogin = false;
callback(data.username);
});
};
ManageAccountDialog.ensureLoggedIn = function (callback) {
ManageAccountDialog.isLoggedIn(function (logged_in_username) {
if (logged_in_username == null) {
ManageAccountDialog.display(null, callback);
} else {
callback(logged_in_username);
}
});
};
ManageAccountDialog.checkAndLaunch = function () {
ManageAccountDialog.isLoggedIn(function(logged_in_username) {
ManageAccountDialog.launch(logged_in_username, function(success) { });
});
ManageAccountDialog.isLoggedIn(function (logged_in_username) {
ManageAccountDialog.display(logged_in_username, function (success) {
});
});
};

View File

@ -0,0 +1,53 @@
<div class="dialog-frame" style="width: 800px;">
<div class="dialog-header" bind="dialogHeader"></div>
<div class="dialog-body" bind="dialogBody" style="position: relative; height: 240px">
<div class="wikidata-logo">
<a href="https://www.wikidata.org/" target="_blank">
<img src="extension/wikidata/images/wikidata.png" alt="Wikidata logo"/>
</a>
</div>
<div class="right-of-logo">
<p class="body-text" bind="explainLogIn"></p>
<p class="body-text" bind="explainOwnerOnlyConsumerWiki"></p>
<div class="wikibase-user-management-area">
<div class="wikibase-user-login" bind="loginArea">
<p bind="invalidCredentials" class="wikibase-invalid-credentials"></p>
<form bind="loginForm" class="wikibase-login-form" method="post">
<table>
<tr>
<td><label for="wb-consumer-token" bind="consumerTokenLabel"></label></td>
<td><input name="wb-consumer-token" id="wb-consumer-token" type="text" bind="consumerTokenInput"/></td>
</tr>
<tr>
<td><label for="wb-consumer-secret" bind="consumerSecretLabel"></label></td>
<td><input name="wb-consumer-secret" id="wb-consumer-secret" type="password" bind="consumerSecretInput"/></td>
</tr>
<tr>
<td><label for="wb-access-token" bind="accessTokenLabel"></label></td>
<td><input name="wb-access-token" id="wb-access-token" type="text" bind="accessTokenInput"/></td>
</tr>
<tr>
<td><label for="wb-access-secret" bind="accessSecretLabel"></label></td>
<td><input name="wb-access-secret" id="wb-access-secret" type="password" bind="accessSecretInput"/></td>
</tr>
<tr>
<td><input type="checkbox" name="remember-credentials" id="remember-credentials" bind="rememberCredentials"/></td>
<td>
<span><label for="remember-credentials"bind="rememberMe"></label></span>
<span><img src="extension/wikidata/images/Information.png" style="height: 0.8rem;" alt="information" bind="ownerOnlyConsumerRememberMeTitle"/></span>
</td>
</tr>
</table>
<div class="wikibase-login-buttons">
<button class="button cancel-button" type="button" bind="cancelButton"></button>
<button class="button button-primary" type="submit" bind="loginButton"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="dialog-footer wikibase-login-dialog-footer">
<span bind="explainPasswordLogin"></span>
</div>
</div>

View File

@ -0,0 +1,46 @@
<div class="dialog-frame" style="width: 800px;">
<div class="dialog-header" bind="dialogHeader"></div>
<div class="dialog-body" bind="dialogBody" style="position: relative; height: 140px">
<div class="wikidata-logo">
<a href="https://www.wikidata.org/" target="_blank">
<img src="extension/wikidata/images/wikidata.png" alt="Wikidata logo"/>
</a>
</div>
<div class="right-of-logo">
<p class="body-text" bind="explainLogIn"></p>
<div class="wikibase-user-management-area">
<div class="wikibase-user-login" bind="loginArea">
<p bind="invalidCredentials" class="wikibase-invalid-credentials"></p>
<form bind="loginForm" class="wikibase-login-form" method="post">
<table>
<tr>
<td><label for="wb-username" bind="usernameLabel"></label></td>
<td><input name="wb-username" id="wb-username" type="text" bind="usernameInput"/></td>
</tr>
<tr>
<td><label for="wb-password" bind="passwordLabel"></label></td>
<td><input name="wb-password" id="wb-password" type="password" bind="passwordInput"/></td>
</tr>
<tr>
<tr>
<td><input type="checkbox" name="remember-credentials" id="remember-credentials" bind="rememberCredentials"/></td>
<td>
<span><label for="remember-credentials"bind="rememberMe"></label></span>
<span><img src="extension/wikidata/images/Information.png" style="height: 0.8rem;" alt="information" bind="passwordRememberMeTitle"/></span>
</td>
</tr>
</tr>
</table>
<div class="wikibase-login-buttons">
<button class="button cancel-button" type="button" bind="cancelButton"></button>
<button class="button button-primary" type="submit" bind="loginButton"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="dialog-footer wikibase-login-dialog-footer">
<span bind="explainOwnerOnlyConsumerLogin"></span>
</div>
</div>

View File

@ -7,9 +7,9 @@
</div>
<div class="wikibase-perform-edits-area">
<form id="wikibase-perform-edits-form" onsubmit="return false;" bind="performEditsForm" autocomplete="on">
<p><span bind="loggedInAs"></span> <a bind="loggedInUsername" target="_blank"></a>.</p>
<p style="margin-top: 0.5rem; margin-bottom: 0.5rem"><span bind="loggedInAs"></span> <a bind="loggedInUsername" target="_blank"></a>.</p>
<p><span bind="editSummaryLabel"></span> <input type="text" name="editSummary" bind="editSummary" class="edit-summary" value="" /></p>
<div class="wikibase-login-buttons">
<div class="wikibase-perform-edits-buttons">
<button class="button cancel-button" bind="cancelButton"></button>
<button class="button button-primary" bind="performEditsButton"></button>
</div>

View File

@ -6,5 +6,6 @@
}
.wikibase-schema-textarea {
width: 100%;
height: 100px;
width: 100%;
height: 100px;
}

View File

@ -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;
}

View File

@ -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");

View File

@ -127,17 +127,17 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.wikidata.wdtk</groupId>
<groupId>org.openrefine.dependencies.wdtk</groupId>
<artifactId>wdtk-wikibaseapi</artifactId>
<version>${wdtk.version}</version>
</dependency>
<dependency>
<groupId>org.wikidata.wdtk</groupId>
<groupId>org.openrefine.dependencies.wdtk</groupId>
<artifactId>wdtk-datamodel</artifactId>
<version>${wdtk.version}</version>
</dependency>
<dependency>
<groupId>org.wikidata.wdtk</groupId>
<groupId>org.openrefine.dependencies.wdtk</groupId>
<artifactId>wdtk-util</artifactId>
<version>${wdtk.version}</version>
</dependency>

View File

@ -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 {

View File

@ -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.
* <p>
* The connection can be either {@link BasicApiConnection} or {@link OAuthApiConnection}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<Cookie> cookies) {
cookies.forEach(cookie -> cookie.setPath("/"));
Map<String, Object> 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<String, Object> 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);
}
}

View File

@ -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,156 @@
******************************************************************************/
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.net.HttpCookie;
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.
* <p>
* Both logging in with username/password or owner-only consumer are supported.
* <p>
* 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 = cookie.getValue();
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<Cookie> 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, cookie.getValue());
cookieList.add(newCookie);
} else if (cookie.getName().equals(WIKIBASE_USERNAME_COOKIE_KEY)) {
username1 = cookie.getValue();
}
}
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<HttpCookie> 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 +180,43 @@ 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<String, Object> 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);
}
private static void setCookie(HttpServletResponse response, String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(60 * 60 * 24 * 365); // a year
cookie.setPath("/");
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("/");
response.addCookie(cookie);
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -1,43 +1,420 @@
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.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<String, String> 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<Cookie> cookieCaptor;
// used for mocking singleton
Constructor<ConnectionManager> 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<String, Cookie> 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<String, Cookie> 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<String, Cookie> 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<String, Cookie> 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<String, Cookie> 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 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<String, Cookie> 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 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<String, Cookie> 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<Cookie> 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<HttpCookie> makeResponseCookies() {
List<HttpCookie> 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) {
assertEquals(cookie.getValue(), expectedValue);
assertEquals(cookie.getMaxAge(), expectedMaxAge);
assertEquals(cookie.getPath(), "/");
}
private static Map<String, Cookie> getCookieMap(List<Cookie> cookies) {
Map<String, Cookie> map = new HashMap<>();
cookies.forEach(cookie -> map.put(cookie.getName(), cookie));
return map;
}
}

16
pom.xml
View File

@ -215,6 +215,20 @@
see main/pom.xml, server/pom.xml and each extension.
-->
</dependencies>
<!-- enabled to access our snapshots of Wikidata-Toolkit -->
<repositories>
<repository>
<id>snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>