Force Nameprepping of JID domain parts

Andreas Straub created

The IDN.toAscii()/IDN.toUnicode() family only namepreps the original
domain passed to it if it contained non-ASCII characters. This means
that for all-ASCII domains, no canonicalization is performed, which
leads to issues like case-sensitivity. This workaround explicitly
namepreps domain parts before calling IDN.toAscii() on them, in order to
get a canonicalized representation (most notably, case invariance). A
basic DB migration is also included.

Change summary

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java | 87 
src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java                | 13 
2 files changed, 96 insertions(+), 4 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -4,11 +4,13 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.Roster;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
 import android.content.Context;
@@ -16,13 +18,14 @@ import android.database.Cursor;
 import android.database.sqlite.SQLiteCantOpenDatabaseException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
 
 public class DatabaseBackend extends SQLiteOpenHelper {
 
 	private static DatabaseBackend instance = null;
 
 	private static final String DATABASE_NAME = "history";
-	private static final int DATABASE_VERSION = 13;
+	private static final int DATABASE_VERSION = 14;
 
 	private static String CREATE_CONTATCS_STATEMENT = "create table "
 			+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -130,6 +133,88 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 			db.execSQL("delete from "+Contact.TABLENAME);
 			db.execSQL("update "+Account.TABLENAME+" set "+Account.ROSTERVERSION+" = NULL");
 		}
+		if (oldVersion < 14 && newVersion >= 14) {
+			// migrate db to new, canonicalized JID domainpart representation
+
+			// Conversation table
+			Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]);
+			while(cursor.moveToNext()) {
+				String newJid;
+				try {
+					newJid = Jid.fromString(
+							cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
+					).toString();
+				} catch (InvalidJidException ignored) {
+					Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID "
+							+cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
+							+": " + ignored +". Skipping...");
+					continue;
+				}
+
+				String updateArgs[] = {
+						newJid,
+						cursor.getString(cursor.getColumnIndex(Conversation.UUID)),
+				};
+				db.execSQL("update " + Conversation.TABLENAME
+						+ " set " + Conversation.CONTACTJID	+ " = ? "
+						+ " where " + Conversation.UUID + " = ?", updateArgs);
+			}
+			cursor.close();
+
+			// Contact table
+			cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]);
+			while(cursor.moveToNext()) {
+				String newJid;
+				try {
+					newJid = Jid.fromString(
+							cursor.getString(cursor.getColumnIndex(Contact.JID))
+					).toString();
+				} catch (InvalidJidException ignored) {
+					Log.e(Config.LOGTAG, "Failed to migrate Contact JID "
+							+cursor.getString(cursor.getColumnIndex(Contact.JID))
+							+": " + ignored +". Skipping...");
+					continue;
+				}
+
+				String updateArgs[] = {
+						newJid,
+						cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)),
+						cursor.getString(cursor.getColumnIndex(Contact.JID)),
+				};
+				db.execSQL("update " + Contact.TABLENAME
+						+ " set " + Contact.JID + " = ? "
+						+ " where " + Contact.ACCOUNT + " = ? "
+						+ " AND " + Contact.JID + " = ?", updateArgs);
+			}
+			cursor.close();
+
+			// Account table
+			cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]);
+			while(cursor.moveToNext()) {
+				String newServer;
+				try {
+					newServer = Jid.fromParts(
+							cursor.getString(cursor.getColumnIndex(Account.USERNAME)),
+							cursor.getString(cursor.getColumnIndex(Account.SERVER)),
+							"mobile"
+					).getDomainpart();
+				} catch (InvalidJidException ignored) {
+					Log.e(Config.LOGTAG, "Failed to migrate Account SERVER "
+							+cursor.getString(cursor.getColumnIndex(Account.SERVER))
+							+": " + ignored +". Skipping...");
+					continue;
+				}
+
+				String updateArgs[] = {
+						newServer,
+						cursor.getString(cursor.getColumnIndex(Account.UUID)),
+				};
+				db.execSQL("update " + Account.TABLENAME
+						+ " set " + Account.SERVER + " = ? "
+						+ " where " + Account.UUID + " = ?", updateArgs);
+			}
+			cursor.close();
+		}
 	}
 
 	public static synchronized DatabaseBackend getInstance(Context context) {

src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java 🔗

@@ -130,12 +130,19 @@ public final class Jid {
 			if (resourcepart.isEmpty() || resourcepart.length() > 1023) {
 				throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
 			}
-			dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES);
+			try {
+				dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, slashLoc)), IDN.USE_STD3_ASCII_RULES);
+			} catch (final StringprepException e) {
+				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+			}
 			finaljid = finaljid + dp + "/" + rp;
 		} else {
 			resourcepart = "";
-			dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()),
-					IDN.USE_STD3_ASCII_RULES);
+			try{
+				dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, jid.length())), IDN.USE_STD3_ASCII_RULES);
+			} catch (final StringprepException e) {
+				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+			}
 			finaljid = finaljid + dp;
 		}