diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 553062e856df3b30bcac5fa8fdeca02349e839f4..1ff10b156d65eefc24f9e0f14553217d6d5c8ea6 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -188,16 +188,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq generateSetPassword(final Account account, final String newPassword) { - final Iq packet = new Iq(Iq.Type.SET); - packet.setTo(account.getDomain()); - final Element query = packet.addChild("query", Namespace.REGISTER); - final Jid jid = account.getJid(); - query.addChild("username").setContent(jid.getLocal()); - query.addChild("password").setContent(newPassword); - return packet; - } - public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) { List jids = new ArrayList<>(); jids.add(jid); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index e5a98a71c00c0f5c946bfaaf7bf84545b6045029..1a3b8a296a1894336dd69897ad74ef89d2fea43f 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -39,7 +39,6 @@ import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.security.KeyChain; import android.text.TextUtils; -import android.util.DisplayMetrics; import android.util.Log; import android.util.LruCache; import android.util.Pair; @@ -141,6 +140,7 @@ import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManage import eu.siacs.conversations.xmpp.manager.NickManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; +import eu.siacs.conversations.xmpp.manager.RegistrationManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.manager.VCardManager; import eu.siacs.conversations.xmpp.pep.Avatar; @@ -2809,41 +2809,10 @@ public class XmppConnectionService extends Service { } } - public void updateAccountPasswordOnServer( - final Account account, - final String newPassword, - final OnAccountPasswordChanged callback) { - final Iq iq = getIqGenerator().generateSetPassword(account, newPassword); - sendIqPacket( - account, - iq, - (packet) -> { - if (packet.getType() == Iq.Type.RESULT) { - account.setPassword(newPassword); - account.setOption(Account.OPTION_MAGIC_CREATE, false); - databaseBackend.updateAccount(account); - callback.onPasswordChangeSucceeded(); - } else { - callback.onPasswordChangeFailed(); - } - }); - } - - public void unregisterAccount(final Account account, final Consumer callback) { - final Iq iqPacket = new Iq(Iq.Type.SET); - final Element query = iqPacket.addChild("query", Namespace.REGISTER); - query.addChild("remove"); - sendIqPacket( - account, - iqPacket, - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - deleteAccount(account); - callback.accept(true); - } else { - callback.accept(false); - } - }); + public ListenableFuture updateAccountPasswordOnServer( + final Account account, final String newPassword) { + final var connection = account.getXmppConnection(); + return connection.getManager(RegistrationManager.class).setPassword(newPassword); } public void deleteAccount(final Account account) { @@ -4664,21 +4633,24 @@ public class XmppConnectionService extends Service { } } - public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) { - if (mOnCaptchaRequested.size() > 0) { - DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics(); - Bitmap scaled = - Bitmap.createScaledBitmap( - captcha, - (int) (captcha.getWidth() * metrics.scaledDensity), - (int) (captcha.getHeight() * metrics.scaledDensity), - false); - for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) { - listener.onCaptchaRequested(account, id, data, scaled); - } - return true; + public boolean displayCaptchaRequest( + final Account account, + final im.conversations.android.xmpp.model.data.Data data, + final Bitmap captcha) { + if (mOnCaptchaRequested.isEmpty()) { + return false; } - return false; + final var metrics = getApplicationContext().getResources().getDisplayMetrics(); + Bitmap scaled = + Bitmap.createScaledBitmap( + captcha, + (int) (captcha.getWidth() * metrics.scaledDensity), + (int) (captcha.getHeight() * metrics.scaledDensity), + false); + for (final OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) { + listener.onCaptchaRequested(account, data, scaled); + } + return true; } public void updateBlocklistUi(final OnUpdateBlocklist.Status status) { @@ -5027,14 +4999,6 @@ public class XmppConnectionService extends Service { } } - public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) { - final XmppConnection connection = account.getXmppConnection(); - if (connection == null) { - return; - } - connection.sendCreateAccountWithCaptchaPacket(id, data); - } - public ListenableFuture sendIqPacket(final Account account, final Iq request) { final XmppConnection connection = account.getXmppConnection(); if (connection == null) { @@ -5401,12 +5365,6 @@ public class XmppConnectionService extends Service { void informUser(int r); } - public interface OnAccountPasswordChanged { - void onPasswordChangeSucceeded(); - - void onPasswordChangeFailed(); - } - public interface OnRoomDestroy { void onRoomDestroySucceeded(); @@ -5440,7 +5398,10 @@ public class XmppConnectionService extends Service { } public interface OnCaptchaRequested { - void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha); + void onCaptchaRequested( + Account account, + im.conversations.android.xmpp.model.data.Data data, + Bitmap captcha); } public interface OnRosterUpdate { diff --git a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java index fba273b0aa2efb49e8cdf085c61053bef54187ae..6dc4921e6978b75ee960d4b51a4007a3f44af200 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java @@ -2,119 +2,130 @@ package eu.siacs.conversations.ui; import android.content.Intent; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.Toast; - +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; - import com.google.android.material.textfield.TextInputLayout; - +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityChangePasswordBinding; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.widget.DisabledActionModeCallback; -public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged { - - private ActivityChangePasswordBinding binding; - - private final View.OnClickListener mOnChangePasswordButtonClicked = new View.OnClickListener() { - @Override - public void onClick(final View view) { - final var account = mAccount; - if (account == null) { - return; - } - final String currentPassword = binding.currentPassword.getText().toString(); - final String newPassword = binding.newPassword.getText().toString(); - if (!account.isOptionSet(Account.OPTION_MAGIC_CREATE) && !currentPassword.equals(account.getPassword())) { - binding.currentPassword.requestFocus(); - binding.currentPasswordLayout.setError(getString(R.string.account_status_unauthorized)); - removeErrorsOnAllBut(binding.currentPasswordLayout); - } else if (newPassword.trim().isEmpty()) { - binding.newPassword.requestFocus(); - binding.newPasswordLayout.setError(getString(R.string.password_should_not_be_empty)); - removeErrorsOnAllBut(binding.newPasswordLayout); - } else { - binding.currentPasswordLayout.setError(null); - binding.newPasswordLayout.setError(null); - xmppConnectionService.updateAccountPasswordOnServer(account, newPassword, ChangePasswordActivity.this); - binding.changePasswordButton.setEnabled(false); - binding.changePasswordButton.setText(R.string.updating); - } - } - }; - - - - private Account mAccount; - - @Override +public class ChangePasswordActivity extends XmppActivity { + + private ActivityChangePasswordBinding binding; + + private final FutureCallback passwordChangedCallback = + new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + Toast.makeText( + ChangePasswordActivity.this, + R.string.password_changed, + Toast.LENGTH_LONG) + .show(); + finish(); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "could not change password", t); + binding.newPasswordLayout.setError( + getString(R.string.could_not_change_password)); + binding.changePasswordButton.setEnabled(true); + binding.changePasswordButton.setText(R.string.change_password); + } + }; + private final View.OnClickListener mOnChangePasswordButtonClicked = + new View.OnClickListener() { + @Override + public void onClick(final View view) { + final var account = mAccount; + if (account == null) { + return; + } + final String currentPassword = binding.currentPassword.getText().toString(); + final String newPassword = binding.newPassword.getText().toString(); + if (!account.isOptionSet(Account.OPTION_MAGIC_CREATE) + && !currentPassword.equals(account.getPassword())) { + binding.currentPassword.requestFocus(); + binding.currentPasswordLayout.setError( + getString(R.string.account_status_unauthorized)); + removeErrorsOnAllBut(binding.currentPasswordLayout); + } else if (newPassword.trim().isEmpty()) { + binding.newPassword.requestFocus(); + binding.newPasswordLayout.setError( + getString(R.string.password_should_not_be_empty)); + removeErrorsOnAllBut(binding.newPasswordLayout); + } else { + binding.currentPasswordLayout.setError(null); + binding.newPasswordLayout.setError(null); + final var future = + xmppConnectionService.updateAccountPasswordOnServer( + account, newPassword); + Futures.addCallback( + future, + ChangePasswordActivity.this.passwordChangedCallback, + ContextCompat.getMainExecutor(getApplication())); + binding.changePasswordButton.setEnabled(false); + binding.changePasswordButton.setText(R.string.updating); + } + } + }; + + private Account mAccount; + + @Override protected void onBackendConnected() { - this.mAccount = extractAccount(getIntent()); - if (this.mAccount != null && this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { - this.binding.currentPasswordLayout.setVisibility(View.GONE); - } else { - this.binding.currentPasswordLayout.setVisibility(View.VISIBLE); - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_change_password); - Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); - setSupportActionBar(binding.toolbar); - configureActionBar(getSupportActionBar()); - binding.cancelButton.setOnClickListener(view -> finish()); - binding.changePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked); - binding.currentPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback()); - binding.newPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback()); - } - - @Override - public void onStart() { - super.onStart(); - Intent intent = getIntent(); - String password = intent != null ? intent.getStringExtra("password") : null; - if (password != null) { - binding.newPassword.getEditableText().clear(); - binding.newPassword.getEditableText().append(password); - } - } - - @Override - public void onPasswordChangeSucceeded() { - runOnUiThread(() -> { - Toast.makeText(ChangePasswordActivity.this,R.string.password_changed,Toast.LENGTH_LONG).show(); - finish(); - }); - } - - @Override - public void onPasswordChangeFailed() { - runOnUiThread(() -> { - binding.newPasswordLayout.setError(getString(R.string.could_not_change_password)); - binding.changePasswordButton.setEnabled(true); - binding.changePasswordButton.setText(R.string.change_password); - }); - - } - - private void removeErrorsOnAllBut(TextInputLayout exception) { - if (this.binding.currentPasswordLayout != exception) { - this.binding.currentPasswordLayout.setErrorEnabled(false); - this.binding.currentPasswordLayout.setError(null); - } - if (this.binding.newPasswordLayout != exception) { - this.binding.newPasswordLayout.setErrorEnabled(false); - this.binding.newPasswordLayout.setError(null); - } - - } - - public void refreshUiReal() { - - } + this.mAccount = extractAccount(getIntent()); + if (this.mAccount != null && this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { + this.binding.currentPasswordLayout.setVisibility(View.GONE); + } else { + this.binding.currentPasswordLayout.setVisibility(View.VISIBLE); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_change_password); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar()); + binding.cancelButton.setOnClickListener(view -> finish()); + binding.changePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked); + binding.currentPassword.setCustomSelectionActionModeCallback( + new DisabledActionModeCallback()); + binding.newPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback()); + } + + @Override + public void onStart() { + super.onStart(); + Intent intent = getIntent(); + String password = intent != null ? intent.getStringExtra("password") : null; + if (password != null) { + binding.newPassword.getEditableText().clear(); + binding.newPassword.getEditableText().append(password); + } + } + + private void removeErrorsOnAllBut(TextInputLayout exception) { + if (this.binding.currentPasswordLayout != exception) { + this.binding.currentPasswordLayout.setErrorEnabled(false); + this.binding.currentPasswordLayout.setError(null); + } + if (this.binding.newPasswordLayout != exception) { + this.binding.newPasswordLayout.setErrorEnabled(false); + this.binding.newPasswordLayout.setError(null); + } + } + + public void refreshUiReal() {} } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 95f25e8ab2bb73b7abf6062c062d6879e8c79983..f6cf8d329758e9282fe31a6c3e1cb0313244ee03 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -80,8 +80,9 @@ import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection.Features; -import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.manager.CarbonsManager; +import eu.siacs.conversations.xmpp.manager.RegistrationManager; +import im.conversations.android.xmpp.model.data.Data; import im.conversations.android.xmpp.model.stanza.Presence; import java.util.Arrays; import java.util.List; @@ -788,10 +789,10 @@ public class EditAccountActivity extends OmemoActivity showBlocklist.setVisible(false); } - if (!mAccount.getXmppConnection().getFeatures().register()) { - changePassword.setVisible(false); - deleteAccount.setVisible(false); - } + final var registration = + mAccount.getXmppConnection().getManager(RegistrationManager.class).hasFeature(); + changePassword.setVisible(registration); + deleteAccount.setVisible(registration); mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam()); changePresence.setVisible(!mInitMode); } else { @@ -1601,8 +1602,7 @@ public class EditAccountActivity extends OmemoActivity } @Override - public void onCaptchaRequested( - final Account account, final String id, final Data data, final Bitmap captcha) { + public void onCaptchaRequested(final Account account, final Data data, final Bitmap captcha) { runOnUiThread( () -> { if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) { @@ -1624,34 +1624,15 @@ public class EditAccountActivity extends OmemoActivity builder.setPositiveButton( getString(R.string.ok), - (dialog, which) -> { - String rc = input.getText().toString(); - data.put("username", account.getUsername()); - data.put("password", account.getPassword()); - data.put("ocr", rc); - data.submit(); - - if (xmppConnectionServiceBound) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket( - account, id, data); - } - }); + (dialog, which) -> + account.getXmppConnection() + .register(data, input.getText().toString())); builder.setNegativeButton( getString(R.string.cancel), - (dialog, which) -> { - if (xmppConnectionService != null) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket( - account, null, null); - } - }); + (dialog, which) -> account.getXmppConnection().cancelRegistration()); builder.setOnCancelListener( - dialog -> { - if (xmppConnectionService != null) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket( - account, null, null); - } - }); + dialog -> account.getXmppConnection().cancelRegistration()); mCaptchaDialog = builder.create(); mCaptchaDialog.show(); input.requestFocus(); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 7d899b4ee7fa923af30e0feb7db9ed4e542147c2..0aaabdfdd43105b204aef53e7610c23d765f7bb9 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -56,6 +56,9 @@ import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; @@ -86,6 +89,7 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.manager.PresenceManager; +import eu.siacs.conversations.xmpp.manager.RegistrationManager; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -390,92 +394,96 @@ public abstract class XmppActivity extends ActionBarActivity { protected void deleteAccount(final Account account, final Runnable postDelete) { final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null); - final CheckBox deleteFromServer = dialogView.findViewById(R.id.delete_from_server); builder.setView(dialogView); builder.setTitle(R.string.mgmt_account_delete); builder.setPositiveButton(getString(R.string.delete), null); builder.setNegativeButton(getString(R.string.cancel), null); final AlertDialog dialog = builder.create(); dialog.setOnShowListener( - dialogInterface -> { - final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); - button.setOnClickListener( - v -> { - final boolean unregister = deleteFromServer.isChecked(); - if (unregister) { - if (account.isOnlineAndConnected()) { - deleteFromServer.setEnabled(false); - button.setText(R.string.please_wait); - button.setEnabled(false); - xmppConnectionService.unregisterAccount( - account, - result -> { - runOnUiThread( - () -> { - if (result) { - dialog.dismiss(); - if (postDelete != null) { - postDelete.run(); - } - if (xmppConnectionService - .getAccounts() - .size() - == 0 - && Config - .MAGIC_CREATE_DOMAIN - != null) { - final Intent intent = - SignupUtils - .getSignUpIntent( - this); - intent.setFlags( - Intent - .FLAG_ACTIVITY_NEW_TASK - | Intent - .FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); - } - } else { - deleteFromServer.setEnabled( - true); - button.setText(R.string.delete); - button.setEnabled(true); - Toast.makeText( - this, - R.string - .could_not_delete_account_from_server, - Toast - .LENGTH_LONG) - .show(); - } - }); - }); - } else { - Toast.makeText( - this, - R.string.not_connected_try_again, - Toast.LENGTH_LONG) - .show(); - } - } else { - xmppConnectionService.deleteAccount(account); - dialog.dismiss(); - if (xmppConnectionService.getAccounts().size() == 0 - && Config.MAGIC_CREATE_DOMAIN != null) { - final Intent intent = SignupUtils.getSignUpIntent(this); - intent.setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); - } else if (postDelete != null) { - postDelete.run(); - } - } - }); - }); + dialogInterface -> onShowDeleteDialog(dialogInterface, account, postDelete)); dialog.show(); } + private void onShowDeleteDialog( + final DialogInterface dialogInterface, + final Account account, + final Runnable postDelete) { + final AlertDialog alertDialog; + if (dialogInterface instanceof AlertDialog dialog) { + alertDialog = dialog; + } else { + throw new IllegalStateException("DialogInterface was not of type AlertDialog"); + } + final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener( + v -> onDeleteDialogButtonClicked(alertDialog, account, postDelete)); + } + + private void onDeleteDialogButtonClicked( + final AlertDialog dialog, final Account account, final Runnable postDelete) { + final CheckBox deleteFromServer = dialog.findViewById(R.id.delete_from_server); + if (deleteFromServer == null) { + throw new IllegalStateException("AlertDialog did not have button"); + } + final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + final boolean unregister = deleteFromServer.isChecked(); + if (unregister) { + if (account.isOnlineAndConnected()) { + final var connection = account.getXmppConnection(); + deleteFromServer.setEnabled(false); + button.setText(R.string.please_wait); + button.setEnabled(false); + final var future = connection.getManager(RegistrationManager.class).unregister(); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + runOnUiThread( + () -> onAccountDeletedSuccess(account, dialog, postDelete)); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "could not unregister account", t); + runOnUiThread(() -> onAccountDeletionFailure(dialog, postDelete)); + } + }, + MoreExecutors.directExecutor()); + } else { + Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_LONG).show(); + } + } else { + onAccountDeletedSuccess(account, dialog, postDelete); + } + } + + private void onAccountDeletedSuccess( + final Account account, final AlertDialog dialog, final Runnable postDelete) { + xmppConnectionService.deleteAccount(account); + dialog.dismiss(); + if (xmppConnectionService.getAccounts().isEmpty() && Config.MAGIC_CREATE_DOMAIN != null) { + final Intent intent = SignupUtils.getSignUpIntent(this); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } else if (postDelete != null) { + postDelete.run(); + } + } + + private void onAccountDeletionFailure(final AlertDialog dialog, final Runnable postDelete) { + final CheckBox deleteFromServer = dialog.findViewById(R.id.delete_from_server); + if (deleteFromServer == null) { + throw new IllegalStateException("AlertDialog did not have button"); + } + final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + deleteFromServer.setEnabled(true); + button.setText(R.string.delete); + button.setEnabled(true); + Toast.makeText(this, R.string.could_not_delete_account_from_server, Toast.LENGTH_LONG) + .show(); + } + protected abstract void onBackendConnected(); protected void registerListeners() { diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index ee336a7abeb68111e9ac7ca43c184161c7efe1bd..00514d99db638daf8db9c2fb62be521cfc009331 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.xml; public final class Namespace { public static final String ADDRESSING = "http://jabber.org/protocol/address"; public static final String AXOLOTL = "eu.siacs.conversations.axolotl"; + public static final String BOB = "urn:xmpp:bob"; public static final String PGP_SIGNED = "jabber:x:signed"; public static final String PGP_ENCRYPTED = "jabber:x:encrypted"; public static final String AXOLOTL_BUNDLES = AXOLOTL + ".bundles"; @@ -98,7 +99,7 @@ public final class Namespace { public static final String MUC_USER = "http://jabber.org/protocol/muc#user"; public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:1"; public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat"; - public static final String INVITE = "urn:xmpp:invite"; + public static final String PRE_AUTHENTICATED_IN_BAND_REGISTRATION = "urn:xmpp:ibr-token:0"; public static final String PARS = "urn:xmpp:pars:0"; public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite"; public static final String OMEMO_DTLS_SRTP_VERIFICATION = @@ -110,6 +111,7 @@ public final class Namespace { public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam"; public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264"; public static final String HASHES = "urn:xmpp:hashes:2"; + public static final String MEDIA_ELEMENT = "urn:xmpp:media-element"; public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0"; public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 000b66dd8100268e5652adda23f392b7e751e02c..66935fe06d2030bcc720900b0eafa845c733b502 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -3,8 +3,6 @@ package eu.siacs.conversations.xmpp; import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.os.Build; import android.os.SystemClock; import android.security.KeyChain; @@ -44,8 +42,6 @@ import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.crypto.sasl.ScramMechanism; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.generator.IqGenerator; -import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.parser.MessageParser; import eu.siacs.conversations.parser.PresenceParser; @@ -70,13 +66,13 @@ import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.XmlReader; import eu.siacs.conversations.xmpp.bind.Bind2; -import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.manager.AbstractManager; import eu.siacs.conversations.xmpp.manager.BlockingManager; import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.manager.PingManager; +import eu.siacs.conversations.xmpp.manager.RegistrationManager; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.model.AuthenticationFailure; @@ -120,9 +116,7 @@ import im.conversations.android.xmpp.model.tls.StartTls; import im.conversations.android.xmpp.processor.AccountStateProcessor; import im.conversations.android.xmpp.processor.BindProcessor; import im.conversations.android.xmpp.processor.MessageAcknowledgedProcessor; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.net.ConnectException; import java.net.IDN; import java.net.InetAddress; @@ -135,7 +129,6 @@ import java.security.Principal; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -260,9 +253,13 @@ public class XmppConnection implements Runnable { } } - private void changeStatus(final Account.State nextStatus) { + private void changeState(final Account.State nextStatus) { + this.changeState(nextStatus, true); + } + + private void changeState(final Account.State nextStatus, final boolean skipOnInterrupt) { synchronized (this) { - if (Thread.currentThread().isInterrupted()) { + if (skipOnInterrupt && Thread.currentThread().isInterrupted()) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -290,6 +287,14 @@ public class XmppConnection implements Runnable { this.accountStateProcessor.accept(nextStatus); } + private void changeStateTerminal(final Account.State state) { + // interrupt needs to be called before status change; otherwise we interrupt the newly + // created thread + this.interrupt(); + this.forceCloseSocket(); + this.changeState(state, false); + } + public Jid getJidForCommand(final String node) { synchronized (this.commands) { return this.commands.get(node); @@ -301,7 +306,7 @@ public class XmppConnection implements Runnable { this.lastPingSent = SystemClock.elapsedRealtime(); this.lastDiscoStarted = Long.MAX_VALUE; this.mWaitingForSmCatchup.set(false); - this.changeStatus(Account.State.CONNECTING); + this.changeState(Account.State.CONNECTING); } public boolean isWaitingForSmCatchup() { @@ -331,7 +336,7 @@ public class XmppConnection implements Runnable { try { Socket localSocket; shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER); - this.changeStatus(Account.State.CONNECTING); + this.changeState(Account.State.CONNECTING); final boolean useTorSetting = appSettings.isUseTor(); final boolean extended = appSettings.isExtendedConnectionOptions(); final boolean useTor = useTorSetting || account.isOnion(); @@ -533,27 +538,27 @@ public class XmppConnection implements Runnable { } processStream(); } catch (final SecurityException e) { - this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION); + this.changeState(Account.State.MISSING_INTERNET_PERMISSION); } catch (final StateChangingException e) { - this.changeStatus(e.state); + this.changeState(e.state); } catch (final UnknownHostException | ConnectException | SocksSocketFactory.HostNotFoundException e) { - this.changeStatus(Account.State.SERVER_NOT_FOUND); + this.changeState(Account.State.SERVER_NOT_FOUND); } catch (final SocksSocketFactory.SocksProxyNotFoundException e) { - this.changeStatus(Account.State.TOR_NOT_AVAILABLE); + this.changeState(Account.State.TOR_NOT_AVAILABLE); } catch (final IOException | XmlPullParserException e) { Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage()); - this.changeStatus(Account.State.OFFLINE); + this.changeState(Account.State.OFFLINE); this.attempt = Math.max(0, this.attempt - 1); } finally { - if (!Thread.currentThread().isInterrupted()) { - forceCloseSocket(); - } else { + if (Thread.currentThread().isInterrupted()) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": not force closing socket because thread was interrupted"); + } else { + forceCloseSocket(); } } } @@ -1189,7 +1194,7 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": awaiting disco results after resume"); - changeStatus(Account.State.CONNECTING); + changeState(Account.State.CONNECTING); } else { changeStatusToOnline(); } @@ -1199,7 +1204,7 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": online with resource " + account.getResource()); - changeStatus(Account.State.ONLINE); + changeState(Account.State.ONLINE); } private void processFailed(final Failed failed, final boolean sendBindRequest) { @@ -1539,12 +1544,12 @@ public class XmppConnection implements Runnable { mXmppConnectionService.databaseBackend.updateAccount(account); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - if (streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) - && account.isOptionSet(Account.OPTION_REGISTER)) { - register(); - } else if (!streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) - && account.isOptionSet(Account.OPTION_REGISTER)) { - throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); + if (account.isOptionSet(Account.OPTION_REGISTER)) { + if (this.streamFeatures.register()) { + this.register(); + } else { + throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); + } } else if (streamFeatures.hasStreamFeature(Authentication.class) && shouldAuthenticate && this.loginInfo == null) { @@ -1847,167 +1852,95 @@ public class XmppConnection implements Runnable { } private void register() { - final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN); - if (preAuth != null && features.invite()) { - final Iq preAuthRequest = new Iq(Iq.Type.SET); - preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); - sendUnmodifiedIqPacket( - preAuthRequest, - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - sendRegistryRequest(); + final String preAuthToken = + Strings.emptyToNull(account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN)); + final ListenableFuture registrationFuture; + if (preAuthToken != null && streamFeatures.preAuthenticatedInBandRegistration()) { + registrationFuture = + getManager(RegistrationManager.class).getRegistration(preAuthToken); + } else { + registrationFuture = getManager(RegistrationManager.class).getRegistration(); + } + // TODO should we store this future and cancel it during disconnect or something + Futures.addCallback( + registrationFuture, + new FutureCallback<>() { + @Override + public void onSuccess(final RegistrationManager.Registration registration) { + if (registration instanceof RegistrationManager.SimpleRegistration) { + final var future = getManager(RegistrationManager.class).register(); + awaitRegistrationResponse(future); + } else if (registration + instanceof RegistrationManager.ExtendedRegistration er) { + mXmppConnectionService.displayCaptchaRequest( + account, er.getData(), er.getCaptcha()); + } else if (registration + instanceof + RegistrationManager.RedirectRegistration redirectRegistration) { + XmppConnection.this.redirectionUrl = redirectRegistration.getURL(); + changeStateTerminal(Account.State.REGISTRATION_WEB); } else { - final String error = response.getErrorCondition(); Log.d( Config.LOGTAG, - account.getJid().asBareJid() - + ": failed to pre auth. " - + error); - throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN); + "got registration: " + registration.getClass().getName()); + changeStateTerminal(Account.State.REGISTRATION_NOT_SUPPORTED); } - }, - true); - } else { - sendRegistryRequest(); - } - } - - private void sendRegistryRequest() { - final Iq register = new Iq(Iq.Type.GET); - register.query(Namespace.REGISTER); - register.setTo(account.getDomain()); - sendUnmodifiedIqPacket( - register, - (packet) -> { - if (packet.getType() == Iq.Type.TIMEOUT) { - return; - } - if (packet.getType() == Iq.Type.ERROR) { - throw new StateChangingError(Account.State.REGISTRATION_FAILED); } - final Element query = packet.query(Namespace.REGISTER); - if (query.hasChild("username") && (query.hasChild("password"))) { - final Iq register1 = new Iq(Iq.Type.SET); - final Element username = - new Element("username").setContent(account.getUsername()); - final Element password = - new Element("password").setContent(account.getPassword()); - register1.query(Namespace.REGISTER).addChild(username); - register1.query().addChild(password); - register1.setFrom(account.getJid().asBareJid()); - sendUnmodifiedIqPacket(register1, this::processRegistrationResponse, true); - } else if (query.hasChild("x", Namespace.DATA)) { - final Data data = Data.parse(query.findChild("x", Namespace.DATA)); - final Element blob = query.findChild("data", "urn:xmpp:bob"); - final String id = packet.getId(); - InputStream is; - if (blob != null) { - try { - final String base64Blob = blob.getContent(); - final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT); - is = new ByteArrayInputStream(strBlob); - } catch (Exception e) { - is = null; - } - } else { - final boolean useTor = this.appSettings.isUseTor() || account.isOnion(); - try { - final String url = data.getValue("url"); - final String fallbackUrl = data.getValue("captcha-fallback-url"); - if (url != null) { - is = HttpConnectionManager.open(url, useTor); - } else if (fallbackUrl != null) { - is = HttpConnectionManager.open(fallbackUrl, useTor); - } else { - is = null; - } - } catch (final IOException e) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": unable to fetch captcha", - e); - is = null; - } - } - if (is != null) { - Bitmap captcha = BitmapFactory.decodeStream(is); - try { - if (mXmppConnectionService.displayCaptchaRequest( - account, id, data, captcha)) { - return; - } - } catch (Exception e) { - throw new StateChangingError(Account.State.REGISTRATION_FAILED); - } + @Override + public void onFailure(@NonNull Throwable t) { + if (t instanceof TimeoutException) { + return; } - throw new StateChangingError(Account.State.REGISTRATION_FAILED); - } else if (query.hasChild("instructions") - || query.hasChild("x", Namespace.OOB)) { - final String instructions = query.findChildContent("instructions"); - final Element oob = query.findChild("x", Namespace.OOB); - final String url = oob == null ? null : oob.findChildContent("url"); - if (url != null) { - setAccountCreationFailed(url); - } else if (instructions != null) { - final Matcher matcher = Patterns.URI_HTTP.matcher(instructions); - if (matcher.find()) { - setAccountCreationFailed( - instructions.substring(matcher.start(), matcher.end())); - } + if (t instanceof RegistrationManager.InvalidTokenException) { + changeStateTerminal(Account.State.REGISTRATION_INVALID_TOKEN); + } else { + changeStateTerminal(Account.State.REGISTRATION_FAILED); } - throw new StateChangingError(Account.State.REGISTRATION_FAILED); } }, - true); + MoreExecutors.directExecutor()); } - public void sendCreateAccountWithCaptchaPacket(final String id, final Data data) { - final Iq request = IqGenerator.generateCreateAccountWithCaptcha(account, id, data); - this.sendUnmodifiedIqPacket(request, this::processRegistrationResponse, true); + public void register( + final im.conversations.android.xmpp.model.data.Data data, final String ocr) { + final var future = getManager(RegistrationManager.class).register(data, ocr); + awaitRegistrationResponse(future); } - private void processRegistrationResponse(final Iq response) { - if (response.getType() == Iq.Type.RESULT) { - account.setOption(Account.OPTION_REGISTER, false); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully registered new account on server"); - throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); - } else { - final Account.State state = getRegistrationFailedState(response); - throw new StateChangingError(state); - } - } - - @NonNull - private static Account.State getRegistrationFailedState(final Iq response) { - final List PASSWORD_TOO_WEAK_MESSAGES = - Arrays.asList("The password is too weak", "Please use a longer password."); - final var error = response.getError(); - final var condition = error == null ? null : error.getCondition(); - final Account.State state; - if (condition instanceof Condition.Conflict) { - state = Account.State.REGISTRATION_CONFLICT; - } else if (condition instanceof Condition.ResourceConstraint) { - state = Account.State.REGISTRATION_PLEASE_WAIT; - } else if (condition instanceof Condition.NotAcceptable - && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) { - state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; - } else { - state = Account.State.REGISTRATION_FAILED; - } - return state; + private void awaitRegistrationResponse(final ListenableFuture registration) { + Futures.addCallback( + registration, + new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + account.setOption(Account.OPTION_REGISTER, false); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully registered new account on server"); + changeStateTerminal(Account.State.REGISTRATION_SUCCESSFUL); + } + + @Override + public void onFailure(@NonNull Throwable t) { + if (t instanceof TimeoutException) { + return; + } + if (t + instanceof + RegistrationManager.RegistrationFailedException exception) { + changeStateTerminal(exception.asAccountState()); + } else { + changeStateTerminal(Account.State.REGISTRATION_FAILED); + } + } + }, + MoreExecutors.directExecutor()); } - private void setAccountCreationFailed(final String url) { - final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url); - if (httpUrl != null && httpUrl.isHttps()) { - this.redirectionUrl = httpUrl; - throw new StateChangingError(Account.State.REGISTRATION_WEB); - } - throw new StateChangingError(Account.State.REGISTRATION_FAILED); + public void cancelRegistration() { + this.changeStateTerminal(Account.State.REGISTRATION_FAILED); } public HttpUrl getRedirectionUrl() { @@ -2608,6 +2541,7 @@ public class XmppConnection implements Runnable { return; } synchronized (this.mStanzaQueue) { + // TODO should we fail IQs for unbound streams? if (force || isBound) { tagWriter.writeStanzaAsync(packet); } else { @@ -2843,6 +2777,9 @@ public class XmppConnection implements Runnable { } public void triggerConnectionTimeout() { + + // TODO not triggering timeout while waiting for captcha input + final var duration = getConnectionDuration(); Log.d( Config.LOGTAG, @@ -2851,11 +2788,7 @@ public class XmppConnection implements Runnable { // last connection time gets reset so time to next attempt is calculated correctly this.lastConnectionStarted = SystemClock.elapsedRealtime(); - // interrupt needs to be called before status change; otherwise we interrupt the newly - // created thread - this.interrupt(); - this.forceCloseSocket(); - this.changeStatus(Account.State.CONNECTION_TIMEOUT); + this.changeStateTerminal(Account.State.CONNECTION_TIMEOUT); } public Account getAccount() { @@ -3088,15 +3021,6 @@ public class XmppConnection implements Runnable { account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL); } - public boolean register() { - return hasDiscoFeature(account.getDomain(), Namespace.REGISTER); - } - - public boolean invite() { - return connection.streamFeatures != null - && connection.streamFeatures.hasChild("register", Namespace.INVITE); - } - public boolean sm() { return streamId != null || (connection.streamFeatures != null @@ -3108,6 +3032,7 @@ public class XmppConnection implements Runnable { && connection.streamFeatures.clientStateIndication(); } + // TODO move to manager public boolean pep() { final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid()); return infoQuery != null && infoQuery.hasIdentityWithCategoryAndType("pubsub", "pep"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/RegistrationManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/RegistrationManager.java index 244eda5b81ed970ca4ff4592b72f09b0426c7ec8..e39e14ac375bbcff1219ab18380dfa08396a35db 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/RegistrationManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/RegistrationManager.java @@ -1,14 +1,24 @@ package eu.siacs.conversations.xmpp.manager; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; import android.util.Patterns; import androidx.annotation.NonNull; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.error.Condition; import im.conversations.android.xmpp.model.oob.OutOfBandData; import im.conversations.android.xmpp.model.pars.PreAuth; import im.conversations.android.xmpp.model.register.Instructions; @@ -18,6 +28,7 @@ import im.conversations.android.xmpp.model.register.Remove; import im.conversations.android.xmpp.model.register.Username; import im.conversations.android.xmpp.model.stanza.Iq; import java.util.Arrays; +import java.util.List; import java.util.regex.Matcher; import okhttp3.HttpUrl; @@ -35,7 +46,63 @@ public class RegistrationManager extends AbstractManager { register.addUsername(account.getJid().getLocal()); register.addPassword(password); return Futures.transform( - connection.sendIqPacket(iq), r -> null, MoreExecutors.directExecutor()); + connection.sendIqPacket(iq), + r -> { + account.setPassword(password); + account.setOption(Account.OPTION_MAGIC_CREATE, false); + getDatabase().updateAccount(account); + return null; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture register() { + final var account = getAccount(); + final var iq = new Iq(Iq.Type.SET); + iq.setTo(account.getJid().getDomain()); + final var register = iq.addExtension(new Register()); + register.addUsername(account.getJid().getLocal()); + register.addPassword(account.getPassword()); + final ListenableFuture future = + Futures.transform( + connection.sendIqPacket(iq, true), + result -> null, + MoreExecutors.directExecutor()); + return Futures.catchingAsync( + future, + IqErrorException.class, + ex -> + Futures.immediateFailedFuture( + new RegistrationFailedException(ex.getResponse())), + MoreExecutors.directExecutor()); + } + + public ListenableFuture register(final Data data, final String ocr) { + final var account = getAccount(); + final var submission = + data.submit( + ImmutableMap.of( + "username", + account.getJid().getLocal(), + "password", + account.getPassword(), + "ocr", + ocr)); + final var iq = new Iq(Iq.Type.SET); + final var register = iq.addExtension(new Register()); + register.addExtension(submission); + final ListenableFuture future = + Futures.transform( + connection.sendIqPacket(iq, true), + result -> null, + MoreExecutors.directExecutor()); + return Futures.catchingAsync( + future, + IqErrorException.class, + ex -> + Futures.immediateFailedFuture( + new RegistrationFailedException(ex.getResponse())), + MoreExecutors.directExecutor()); } public ListenableFuture unregister() { @@ -54,7 +121,7 @@ public class RegistrationManager extends AbstractManager { iq.setTo(account.getDomain()); iq.addExtension(new Register()); final var future = connection.sendIqPacket(iq, true); - return Futures.transform( + return Futures.transformAsync( future, result -> { final var register = result.getExtension(Register.class); @@ -64,15 +131,18 @@ public class RegistrationManager extends AbstractManager { } if (register.hasExtension(Username.class) && register.hasExtension(Password.class)) { - return new SimpleRegistration(); + return Futures.immediateFuture(new SimpleRegistration()); } + + // find bits of binary and get captcha from there + final var data = register.getExtension(Data.class); // note that the captcha namespace is incorrect here. That namespace is only // used in message challenges. ejabberd uses the incorrect namespace though if (data != null && Arrays.asList(Namespace.REGISTER, Namespace.CAPTCHA) .contains(data.getFormType())) { - return new ExtendedRegistration(data); + return getExtendedRegistration(register, data); } final var oob = register.getExtension(OutOfBandData.class); final var instructions = register.getExtension(Instructions.class); @@ -80,14 +150,15 @@ public class RegistrationManager extends AbstractManager { instructions == null ? null : instructions.getContent(); final String redirectUrl = oob == null ? null : oob.getURL(); if (redirectUrl != null) { - return RedirectRegistration.ifValid(redirectUrl); + return Futures.immediateFuture(RedirectRegistration.ifValid(redirectUrl)); } if (instructionsText != null) { final Matcher matcher = Patterns.WEB_URL.matcher(instructionsText); if (matcher.find()) { final String instructionsUrl = instructionsText.substring(matcher.start(), matcher.end()); - return RedirectRegistration.ifValid(instructionsUrl); + return Futures.immediateFuture( + RedirectRegistration.ifValid(instructionsUrl)); } } throw new IllegalStateException("No supported registration method found"); @@ -95,6 +166,59 @@ public class RegistrationManager extends AbstractManager { MoreExecutors.directExecutor()); } + private ListenableFuture getExtendedRegistration( + final Register register, final Data data) { + final var ocr = data.getFieldByName("ocr"); + if (ocr == null) { + throw new IllegalArgumentException("Missing OCR form field"); + } + final var ocrMedia = ocr.getMedia(); + if (ocrMedia == null) { + throw new IllegalArgumentException("OCR form field missing media"); + } + final var uris = ocrMedia.getUris(); + final var bobUri = Iterables.find(uris, u -> "cid".equals(u.getScheme()), null); + final Optional bob; + if (bobUri != null) { + bob = im.conversations.android.xmpp.model.bob.Data.get(register, bobUri.getPath()); + } else { + bob = Optional.absent(); + } + if (bob.isPresent()) { + final var bytes = bob.get().asBytes(); + final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + return Futures.immediateFuture(new ExtendedRegistration(bitmap, data)); + } + final var captchaFallbackUrl = data.getValue("captcha-fallback-url"); + if (captchaFallbackUrl == null) { + throw new IllegalStateException("No captcha fallback URL provided"); + } + final var captchFallbackHttpUrl = HttpUrl.parse(captchaFallbackUrl); + Log.d(Config.LOGTAG, "fallback url: " + captchFallbackHttpUrl); + throw new IllegalStateException("Not implemented"); + } + + public ListenableFuture getRegistration(final String token) { + final var preAuthentication = sendPreAuthentication(token); + final var caught = + Futures.catchingAsync( + preAuthentication, + IqErrorException.class, + ex -> { + final var error = ex.getError(); + final var condition = error == null ? null : error.getCondition(); + if (condition instanceof Condition.ItemNotFound) { + return Futures.immediateFailedFuture( + new InvalidTokenException(ex.getResponse())); + } else { + return Futures.immediateFuture(ex); + } + }, + MoreExecutors.directExecutor()); + return Futures.transformAsync( + caught, v -> getRegistration(), MoreExecutors.directExecutor()); + } + public ListenableFuture sendPreAuthentication(final String token) { final var account = getAccount(); final var iq = new Iq(Iq.Type.GET); @@ -105,6 +229,10 @@ public class RegistrationManager extends AbstractManager { return Futures.transform(future, result -> null, MoreExecutors.directExecutor()); } + public boolean hasFeature() { + return getManager(DiscoManager.class).hasServerFeature(Namespace.REGISTER); + } + public abstract static class Registration {} // only requires Username + Password @@ -112,12 +240,18 @@ public class RegistrationManager extends AbstractManager { // Captcha as shown here: https://xmpp.org/extensions/xep-0158.html#register public static class ExtendedRegistration extends Registration { + private final Bitmap captcha; private final Data data; - public ExtendedRegistration(Data data) { + public ExtendedRegistration(final Bitmap captcha, final Data data) { + this.captcha = captcha; this.data = data; } + public Bitmap getCaptcha() { + return this.captcha; + } + public Data getData() { return this.data; } @@ -144,4 +278,36 @@ public class RegistrationManager extends AbstractManager { "A URL found the registration instructions is not valid"); } } + + public static class InvalidTokenException extends IqErrorException { + + public InvalidTokenException(final Iq response) { + super(response); + } + } + + public static class RegistrationFailedException extends IqErrorException { + + private final List PASSWORD_TOO_WEAK_MESSAGES = + Arrays.asList("The password is too weak", "Please use a longer password."); + + public RegistrationFailedException(final Iq response) { + super(response); + } + + public Account.State asAccountState() { + final var error = getError(); + final var condition = error == null ? null : error.getCondition(); + if (condition instanceof Condition.Conflict) { + return Account.State.REGISTRATION_CONFLICT; + } else if (condition instanceof Condition.ResourceConstraint) { + return Account.State.REGISTRATION_PLEASE_WAIT; + } else if (condition instanceof Condition.NotAcceptable + && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) { + return Account.State.REGISTRATION_PASSWORD_TOO_WEAK; + } else { + return Account.State.REGISTRATION_FAILED; + } + } + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/bob/Data.java b/src/main/java/im/conversations/android/xmpp/model/bob/Data.java new file mode 100644 index 0000000000000000000000000000000000000000..fcbc069807a7f92248e66b85ae05fa904169e06f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bob/Data.java @@ -0,0 +1,27 @@ +package im.conversations.android.xmpp.model.bob; + +import com.google.common.base.Optional; +import com.google.common.collect.Iterables; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Data extends Extension implements ByteContent { + + public Data() { + super(Data.class); + } + + public String getCid() { + return this.getAttribute("cid"); + } + + public String getType() { + return this.getAttribute("type"); + } + + public static Optional get(final Extension stanza, final String cid) { + return Iterables.tryFind(stanza.getExtensions(Data.class), d -> cid.equals(d.getCid())); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bob/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bob/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..12128f5c6c4bf42ba91c8b6443bc6c4905423167 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bob/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BOB) +package im.conversations.android.xmpp.model.bob; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/src/main/java/im/conversations/android/xmpp/model/data/Field.java index 0c6b96dff7f5c8c37590fe212937560314e99a2d..3244ff9c3836d076b801e1d1a3a761346f03021f 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Field.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Field.java @@ -5,6 +5,7 @@ import com.google.common.collect.Iterables; import eu.siacs.conversations.xml.Element; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.media.Media; import java.util.Collection; @XmlElement @@ -32,4 +33,8 @@ public class Field extends Extension { public void setType(String type) { this.setAttribute("type", type); } + + public Media getMedia() { + return getOnlyExtension(Media.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/media/Media.java b/src/main/java/im/conversations/android/xmpp/model/media/Media.java new file mode 100644 index 0000000000000000000000000000000000000000..94d8eb5b5a0e68f54d5cf4fc0ced78e9f3d842bd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/media/Media.java @@ -0,0 +1,25 @@ +package im.conversations.android.xmpp.model.media; + +import com.google.common.collect.Collections2; +import de.gultsch.common.MiniUri; +import eu.siacs.conversations.xml.Element; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Media extends Extension { + + public Media() { + super(Media.class); + } + + public Collection getUris() { + final var uris = + Collections2.filter( + Collections2.transform(this.getExtensions(Uri.class), Element::getContent), + Objects::nonNull); + return Collections2.transform(uris, MiniUri::new); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/media/Uri.java b/src/main/java/im/conversations/android/xmpp/model/media/Uri.java new file mode 100644 index 0000000000000000000000000000000000000000..455bd55b92ff0099de86d51169db282191066848 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/media/Uri.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.media; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Uri extends Extension { + + public Uri() { + super(Uri.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/media/package-info.java b/src/main/java/im/conversations/android/xmpp/model/media/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..927f473f24f444e474e37f23c86029f573286989 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/media/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.MEDIA_ELEMENT) +package im.conversations.android.xmpp.model.media; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java b/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java index cd22f2a3a8427cd0d58ff511a6daa2d5525215c3..f6cc77d15a3197775f5ec62f729820bb56a09ad4 100644 --- a/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java +++ b/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java @@ -1,7 +1,9 @@ package im.conversations.android.xmpp.model.register; +import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +@XmlElement public class Instructions extends Extension { public Instructions() { diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Password.java b/src/main/java/im/conversations/android/xmpp/model/register/Password.java index 9da687c213e2cc26dfbc6eaec8710a84a8ff71f7..8303d80b0fbba76f387b4d68bddeb3468df38b2e 100644 --- a/src/main/java/im/conversations/android/xmpp/model/register/Password.java +++ b/src/main/java/im/conversations/android/xmpp/model/register/Password.java @@ -1,7 +1,9 @@ package im.conversations.android.xmpp.model.register; +import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +@XmlElement public class Password extends Extension { public Password() { diff --git a/src/main/java/im/conversations/android/xmpp/model/register/RegisterStreamFeature.java b/src/main/java/im/conversations/android/xmpp/model/register/RegisterStreamFeature.java new file mode 100644 index 0000000000000000000000000000000000000000..d4ad4e8eb36c8d659ac810f91eefc9370edf2c22 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/RegisterStreamFeature.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.register; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamFeature; + +@XmlElement(name = "register", namespace = Namespace.REGISTER_STREAM_FEATURE) +public class RegisterStreamFeature extends StreamFeature { + + public RegisterStreamFeature() { + super(RegisterStreamFeature.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Remove.java b/src/main/java/im/conversations/android/xmpp/model/register/Remove.java index bbd327bfd66130779102cadb4817602e1979c40a..56afd69a171f34eefac4848fd2c970d9165817d2 100644 --- a/src/main/java/im/conversations/android/xmpp/model/register/Remove.java +++ b/src/main/java/im/conversations/android/xmpp/model/register/Remove.java @@ -1,7 +1,9 @@ package im.conversations.android.xmpp.model.register; +import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +@XmlElement public class Remove extends Extension { public Remove() { diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/Features.java b/src/main/java/im/conversations/android/xmpp/model/streams/Features.java index 0597c2241cfb809a46d7747ce309fc01a01fe15c..0c8879e9500a16359016145f0c66022198c60bc4 100644 --- a/src/main/java/im/conversations/android/xmpp/model/streams/Features.java +++ b/src/main/java/im/conversations/android/xmpp/model/streams/Features.java @@ -1,12 +1,13 @@ package im.conversations.android.xmpp.model.streams; +import eu.siacs.conversations.xml.Namespace; import im.conversations.android.annotation.XmlElement; -import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.StreamElement; import im.conversations.android.xmpp.model.StreamFeature; import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; -import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.register.RegisterStreamFeature; import im.conversations.android.xmpp.model.sm.StreamManagement; +import im.conversations.android.xmpp.model.token.Register; @XmlElement public class Features extends StreamElement implements EntityCapabilities { @@ -18,14 +19,17 @@ public class Features extends StreamElement implements EntityCapabilities { return hasStreamFeature(StreamManagement.class); } - public boolean invite() { - return this.hasChild("register", Namespace.INVITE); - } - public boolean clientStateIndication() { return this.hasChild("csi", Namespace.CSI); } + public boolean register() { + return hasStreamFeature(RegisterStreamFeature.class); + } + + public boolean preAuthenticatedInBandRegistration() { + return hasStreamFeature(Register.class); + } public boolean hasStreamFeature(final Class clazz) { return hasExtension(clazz); diff --git a/src/main/java/im/conversations/android/xmpp/model/token/Register.java b/src/main/java/im/conversations/android/xmpp/model/token/Register.java new file mode 100644 index 0000000000000000000000000000000000000000..b4b9eb0e4b142635388b8ad8d3afcbceca553db5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/token/Register.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.token; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamFeature; + +@XmlElement +public class Register extends StreamFeature { + + public Register() { + super(Register.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/token/package-info.java b/src/main/java/im/conversations/android/xmpp/model/token/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..51fbc0437f695fbd7cdc6987994ebf8a34e08536 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/token/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PRE_AUTHENTICATED_IN_BAND_REGISTRATION) +package im.conversations.android.xmpp.model.token; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage;