1package eu.siacs.conversations.xmpp.manager;
  2
  3import android.content.Context;
  4import android.util.Patterns;
  5import androidx.annotation.NonNull;
  6import com.google.common.util.concurrent.Futures;
  7import com.google.common.util.concurrent.ListenableFuture;
  8import com.google.common.util.concurrent.MoreExecutors;
  9import eu.siacs.conversations.xml.Namespace;
 10import eu.siacs.conversations.xmpp.XmppConnection;
 11import im.conversations.android.xmpp.model.data.Data;
 12import im.conversations.android.xmpp.model.oob.OutOfBandData;
 13import im.conversations.android.xmpp.model.pars.PreAuth;
 14import im.conversations.android.xmpp.model.register.Instructions;
 15import im.conversations.android.xmpp.model.register.Password;
 16import im.conversations.android.xmpp.model.register.Register;
 17import im.conversations.android.xmpp.model.register.Remove;
 18import im.conversations.android.xmpp.model.register.Username;
 19import im.conversations.android.xmpp.model.stanza.Iq;
 20import java.util.Arrays;
 21import java.util.regex.Matcher;
 22import okhttp3.HttpUrl;
 23
 24public class RegistrationManager extends AbstractManager {
 25
 26    public RegistrationManager(Context context, XmppConnection connection) {
 27        super(context, connection);
 28    }
 29
 30    public ListenableFuture<Void> setPassword(final String password) {
 31        final var account = getAccount();
 32        final var iq = new Iq(Iq.Type.SET);
 33        iq.setTo(account.getJid().getDomain());
 34        final var register = iq.addExtension(new Register());
 35        register.addUsername(account.getJid().getLocal());
 36        register.addPassword(password);
 37        return Futures.transform(
 38                connection.sendIqPacket(iq), r -> null, MoreExecutors.directExecutor());
 39    }
 40
 41    public ListenableFuture<Void> unregister() {
 42        final var account = getAccount();
 43        final var iq = new Iq(Iq.Type.SET);
 44        iq.setTo(account.getJid().getDomain());
 45        final var register = iq.addExtension(new Register());
 46        register.addExtension(new Remove());
 47        return Futures.transform(
 48                connection.sendIqPacket(iq), r -> null, MoreExecutors.directExecutor());
 49    }
 50
 51    public ListenableFuture<Registration> getRegistration() {
 52        final var account = getAccount();
 53        final var iq = new Iq(Iq.Type.GET);
 54        iq.setTo(account.getDomain());
 55        iq.addExtension(new Register());
 56        final var future = connection.sendIqPacket(iq, true);
 57        return Futures.transform(
 58                future,
 59                result -> {
 60                    final var register = result.getExtension(Register.class);
 61                    if (register == null) {
 62                        throw new IllegalStateException(
 63                                "Server did not include register in response");
 64                    }
 65                    if (register.hasExtension(Username.class)
 66                            && register.hasExtension(Password.class)) {
 67                        return new SimpleRegistration();
 68                    }
 69                    final var data = register.getExtension(Data.class);
 70                    // note that the captcha namespace is incorrect here. That namespace is only
 71                    // used in message challenges. ejabberd uses the incorrect namespace though
 72                    if (data != null
 73                            && Arrays.asList(Namespace.REGISTER, Namespace.CAPTCHA)
 74                                    .contains(data.getFormType())) {
 75                        return new ExtendedRegistration(data);
 76                    }
 77                    final var oob = register.getExtension(OutOfBandData.class);
 78                    final var instructions = register.getExtension(Instructions.class);
 79                    final String instructionsText =
 80                            instructions == null ? null : instructions.getContent();
 81                    final String redirectUrl = oob == null ? null : oob.getURL();
 82                    if (redirectUrl != null) {
 83                        return RedirectRegistration.ifValid(redirectUrl);
 84                    }
 85                    if (instructionsText != null) {
 86                        final Matcher matcher = Patterns.WEB_URL.matcher(instructionsText);
 87                        if (matcher.find()) {
 88                            final String instructionsUrl =
 89                                    instructionsText.substring(matcher.start(), matcher.end());
 90                            return RedirectRegistration.ifValid(instructionsUrl);
 91                        }
 92                    }
 93                    throw new IllegalStateException("No supported registration method found");
 94                },
 95                MoreExecutors.directExecutor());
 96    }
 97
 98    public ListenableFuture<Void> sendPreAuthentication(final String token) {
 99        final var account = getAccount();
100        final var iq = new Iq(Iq.Type.GET);
101        iq.setTo(account.getJid().getDomain());
102        final var preAuthentication = iq.addExtension(new PreAuth());
103        preAuthentication.setToken(token);
104        final var future = connection.sendIqPacket(iq, true);
105        return Futures.transform(future, result -> null, MoreExecutors.directExecutor());
106    }
107
108    public abstract static class Registration {}
109
110    // only requires Username + Password
111    public static class SimpleRegistration extends Registration {}
112
113    // Captcha as shown here: https://xmpp.org/extensions/xep-0158.html#register
114    public static class ExtendedRegistration extends Registration {
115        private final Data data;
116
117        public ExtendedRegistration(Data data) {
118            this.data = data;
119        }
120
121        public Data getData() {
122            return this.data;
123        }
124    }
125
126    // Redirection as show here: https://xmpp.org/extensions/xep-0077.html#redirect
127    public static class RedirectRegistration extends Registration {
128        private final HttpUrl url;
129
130        private RedirectRegistration(@NonNull HttpUrl url) {
131            this.url = url;
132        }
133
134        public @NonNull HttpUrl getURL() {
135            return this.url;
136        }
137
138        public static RedirectRegistration ifValid(final String url) {
139            final HttpUrl httpUrl = HttpUrl.parse(url);
140            if (httpUrl != null && httpUrl.isHttps()) {
141                return new RedirectRegistration(httpUrl);
142            }
143            throw new IllegalStateException(
144                    "A URL found the registration instructions is not valid");
145        }
146    }
147}