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