1package eu.siacs.conversations.ui;
2
3import android.app.Activity;
4import android.app.Dialog;
5import android.os.Bundle;
6import android.text.Editable;
7import android.text.TextWatcher;
8import android.view.View;
9import android.widget.ArrayAdapter;
10import androidx.annotation.NonNull;
11import androidx.appcompat.app.AlertDialog;
12import androidx.databinding.DataBindingUtil;
13import androidx.fragment.app.DialogFragment;
14import com.google.android.material.dialog.MaterialAlertDialogBuilder;
15import com.google.common.base.Strings;
16import eu.siacs.conversations.R;
17import eu.siacs.conversations.databinding.DialogEnterJidBinding;
18import eu.siacs.conversations.services.XmppConnectionService;
19import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
20import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
21import eu.siacs.conversations.ui.util.DelayedHintHelper;
22import eu.siacs.conversations.xmpp.Jid;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Collection;
26import java.util.Collections;
27import java.util.List;
28
29public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher {
30
31 private static final List<String> SUSPICIOUS_DOMAINS =
32 Arrays.asList("conference", "muc", "room", "rooms", "chat");
33
34 private OnEnterJidDialogPositiveListener mListener = null;
35
36 private static final String TITLE_KEY = "title";
37 private static final String POSITIVE_BUTTON_KEY = "positive_button";
38 private static final String PREFILLED_JID_KEY = "prefilled_jid";
39 private static final String ACCOUNT_KEY = "account";
40 private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid";
41 private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list";
42 private static final String SANITY_CHECK_JID = "sanity_check_jid";
43
44 private KnownHostsAdapter knownHostsAdapter;
45 private Collection<String> whitelistedDomains = Collections.emptyList();
46
47 private DialogEnterJidBinding binding;
48 private AlertDialog dialog;
49 private boolean sanityCheckJid = false;
50
51 private boolean issuedWarning = false;
52
53 public static EnterJidDialog newInstance(
54 final ArrayList<String> activatedAccounts,
55 final String title,
56 final String positiveButton,
57 final String prefilledJid,
58 final String account,
59 boolean allowEditJid,
60 final boolean sanity_check_jid) {
61 final EnterJidDialog dialog = new EnterJidDialog();
62 Bundle bundle = new Bundle();
63 bundle.putString(TITLE_KEY, title);
64 bundle.putString(POSITIVE_BUTTON_KEY, positiveButton);
65 bundle.putString(PREFILLED_JID_KEY, prefilledJid);
66 bundle.putString(ACCOUNT_KEY, account);
67 bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid);
68 bundle.putStringArrayList(ACCOUNTS_LIST_KEY, activatedAccounts);
69 bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid);
70 dialog.setArguments(bundle);
71 return dialog;
72 }
73
74 @Override
75 public void onActivityCreated(Bundle savedInstanceState) {
76 super.onActivityCreated(savedInstanceState);
77 setRetainInstance(true);
78 }
79
80 @Override
81 public void onStart() {
82 super.onStart();
83 final Activity activity = getActivity();
84 if (activity instanceof XmppActivity
85 && ((XmppActivity) activity).xmppConnectionService != null) {
86 refreshKnownHosts();
87 }
88 }
89
90 @NonNull
91 @Override
92 public Dialog onCreateDialog(final Bundle savedInstanceState) {
93 final var arguments = getArguments();
94 final MaterialAlertDialogBuilder builder =
95 new MaterialAlertDialogBuilder(requireActivity());
96 builder.setTitle(arguments.getString(TITLE_KEY));
97 binding =
98 DataBindingUtil.inflate(
99 requireActivity().getLayoutInflater(),
100 R.layout.dialog_enter_jid,
101 null,
102 false);
103 this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.item_autocomplete);
104 binding.jid.setAdapter(this.knownHostsAdapter);
105 binding.jid.addTextChangedListener(this);
106 final String prefilledJid = arguments.getString(PREFILLED_JID_KEY);
107 if (prefilledJid != null) {
108 binding.jid.append(prefilledJid);
109 if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) {
110 binding.jid.setFocusable(false);
111 binding.jid.setFocusableInTouchMode(false);
112 binding.jid.setClickable(false);
113 binding.jid.setCursorVisible(false);
114 }
115 }
116 sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false);
117
118 DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
119
120 final String account = getArguments().getString(ACCOUNT_KEY);
121 if (Strings.isNullOrEmpty(account)) {
122 StartConversationActivity.populateAccountSpinner(
123 getActivity(),
124 arguments.getStringArrayList(ACCOUNTS_LIST_KEY),
125 binding.account);
126 } else {
127 final ArrayAdapter<String> adapter =
128 new ArrayAdapter<>(
129 requireActivity(), R.layout.item_autocomplete, new String[] {account});
130 binding.account.setText(account);
131 binding.account.setEnabled(false);
132 adapter.setDropDownViewResource(R.layout.item_autocomplete);
133 binding.account.setAdapter(adapter);
134 }
135
136 builder.setView(binding.getRoot());
137 builder.setNegativeButton(R.string.cancel, null);
138 builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null);
139 this.dialog = builder.create();
140
141 View.OnClickListener dialogOnClick = v -> handleEnter(binding, account);
142
143 binding.jid.setOnEditorActionListener(
144 (v, actionId, event) -> {
145 handleEnter(binding, account);
146 return true;
147 });
148
149 dialog.show();
150 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick);
151 return dialog;
152 }
153
154 private void handleEnter(DialogEnterJidBinding binding, String account) {
155 final Jid accountJid;
156 if (!binding.account.isEnabled() && account == null) {
157 return;
158 }
159 try {
160 accountJid = Jid.of(binding.account.getEditableText().toString());
161 } catch (final IllegalArgumentException e) {
162 return;
163 }
164 final Jid contactJid;
165 try {
166 contactJid = Jid.ofUserInput(binding.jid.getText().toString().trim());
167 } catch (final IllegalArgumentException e) {
168 binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid));
169 return;
170 }
171
172 if (!issuedWarning && sanityCheckJid) {
173 if (contactJid.isDomainJid()) {
174 binding.jidLayout.setError(
175 getActivity().getString(R.string.this_looks_like_a_domain));
176 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
177 issuedWarning = true;
178 return;
179 }
180 if (suspiciousSubDomain(contactJid.getDomain().toString())) {
181 binding.jidLayout.setError(
182 getActivity().getString(R.string.this_looks_like_channel));
183 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
184 issuedWarning = true;
185 return;
186 }
187 }
188
189 if (mListener != null) {
190 try {
191 if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) {
192 dialog.dismiss();
193 }
194 } catch (JidError error) {
195 binding.jidLayout.setError(error.toString());
196 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add);
197 issuedWarning = false;
198 }
199 }
200 }
201
202 public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) {
203 this.mListener = listener;
204 }
205
206 @Override
207 public void onBackendConnected() {
208 refreshKnownHosts();
209 }
210
211 private void refreshKnownHosts() {
212 final Activity activity = getActivity();
213 if (activity instanceof XmppActivity) {
214 final XmppConnectionService service = ((XmppActivity) activity).xmppConnectionService;
215 if (service == null) {
216 return;
217 }
218 final Collection<String> hosts = service.getKnownHosts();
219 this.knownHostsAdapter.refresh(hosts);
220 this.whitelistedDomains = hosts;
221 }
222 }
223
224 @Override
225 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
226
227 @Override
228 public void onTextChanged(CharSequence s, int start, int before, int count) {}
229
230 @Override
231 public void afterTextChanged(Editable s) {
232 if (issuedWarning) {
233 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add);
234 binding.jidLayout.setError(null);
235 issuedWarning = false;
236 }
237 }
238
239 public interface OnEnterJidDialogPositiveListener {
240 boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError;
241 }
242
243 public static class JidError extends Exception {
244 final String msg;
245
246 public JidError(final String msg) {
247 this.msg = msg;
248 }
249
250 @NonNull
251 public String toString() {
252 return msg;
253 }
254 }
255
256 @Override
257 public void onDestroyView() {
258 Dialog dialog = getDialog();
259 if (dialog != null && getRetainInstance()) {
260 dialog.setDismissMessage(null);
261 }
262 super.onDestroyView();
263 }
264
265 private boolean suspiciousSubDomain(String domain) {
266 if (this.whitelistedDomains.contains(domain)) {
267 return false;
268 }
269 final String[] parts = domain.split("\\.");
270 return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]);
271 }
272}