mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 13:28:26 +00:00
Added code to migrate from SQLCipher 3 to 4
This commit is contained in:
parent
cdd2559839
commit
1a28fd2a9e
@ -95,7 +95,8 @@ dependencies {
|
||||
implementation 'com.takisoft.fix:colorpicker:1.0.1'
|
||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
||||
implementation 'androidx.sqlite:sqlite-ktx:2.2.0'
|
||||
implementation 'net.zetetic:sqlcipher-android:4.5.2@aar'
|
||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
exclude group: 'org.freemarker'
|
||||
|
@ -8,7 +8,7 @@ import androidx.annotation.WorkerThread
|
||||
import com.annimon.stream.function.Consumer
|
||||
import com.annimon.stream.function.Predicate
|
||||
import com.google.protobuf.ByteString
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
|
@ -5,7 +5,7 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.WorkerThread
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
|
@ -33,7 +33,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
@ -23,7 +23,7 @@ import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.utilities.WindowDebouncer;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
|
@ -19,7 +19,7 @@ package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.database.getStringOrNull
|
||||
import net.sqlcipher.Cursor
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.session.libsignal.utilities.Base64
|
||||
|
||||
fun <T> SQLiteDatabase.get(table: String, query: String?, arguments: Array<String>?, get: (Cursor) -> T): T? {
|
||||
|
@ -6,7 +6,7 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
@ -12,7 +11,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.session.libsession.utilities.Address;
|
||||
|
@ -1,13 +1,12 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE
|
||||
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
|
||||
|
@ -7,7 +7,7 @@ import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||
import org.session.libsession.utilities.Address;
|
||||
|
@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.Document;
|
||||
|
@ -22,8 +22,8 @@ import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
@ -6,7 +6,7 @@ import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
|
@ -11,7 +11,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.MaterialColor;
|
||||
|
@ -1,13 +1,13 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.Cursor;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.database
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import androidx.core.database.getStringOrNull
|
||||
import net.sqlcipher.Cursor
|
||||
import android.database.Cursor
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
@ -75,21 +75,6 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
||||
}
|
||||
|
||||
fun contactFromCursor(cursor: Cursor): Contact {
|
||||
val sessionID = cursor.getString(sessionID)
|
||||
val contact = Contact(sessionID)
|
||||
contact.name = cursor.getStringOrNull(name)
|
||||
contact.nickname = cursor.getStringOrNull(nickname)
|
||||
contact.profilePictureURL = cursor.getStringOrNull(profilePictureURL)
|
||||
contact.profilePictureFileName = cursor.getStringOrNull(profilePictureFileName)
|
||||
cursor.getStringOrNull(profilePictureEncryptionKey)?.let {
|
||||
contact.profilePictureEncryptionKey = Base64.decode(it)
|
||||
}
|
||||
contact.threadID = cursor.getLong(threadID)
|
||||
contact.isTrusted = cursor.getInt(isTrusted) != 0
|
||||
return contact
|
||||
}
|
||||
|
||||
fun contactFromCursor(cursor: android.database.Cursor): Contact {
|
||||
val sessionID = cursor.getString(cursor.getColumnIndexOrThrow(sessionID))
|
||||
val contact = Contact(sessionID)
|
||||
contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name))
|
||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import net.sqlcipher.Cursor
|
||||
import android.database.Cursor
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
||||
|
@ -28,8 +28,8 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
|
||||
import org.session.libsession.messaging.calls.CallMessageType;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
||||
|
@ -32,7 +32,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.session.libsession.utilities.Address;
|
||||
|
@ -1,14 +1,16 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
import net.zetetic.database.DatabaseErrorHandler;
|
||||
import net.zetetic.database.DatabaseUtils;
|
||||
import net.zetetic.database.sqlcipher.SQLiteConnection;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
|
||||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
@ -36,6 +38,8 @@ import org.thoughtcrime.securesms.database.SessionJobDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ -77,38 +81,117 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV38 = 59;
|
||||
|
||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV38;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
private static final int DATABASE_VERSION = lokiV38;
|
||||
private static final String CIPHER3_DATABASE_NAME = "signal.db";
|
||||
private static final String DATABASE_NAME = "signal_v4.db";
|
||||
|
||||
private final Context context;
|
||||
private final DatabaseSecret databaseSecret;
|
||||
|
||||
public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION, new SQLiteDatabaseHook() {
|
||||
super(context, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, DATABASE_VERSION, null, new SQLiteDatabaseHook() {
|
||||
@Override
|
||||
public void preKey(SQLiteDatabase db) {
|
||||
db.rawExecSQL("PRAGMA cipher_default_kdf_iter = 1;");
|
||||
db.rawExecSQL("PRAGMA cipher_default_page_size = 4096;");
|
||||
public void preKey(SQLiteConnection connection) {
|
||||
connection.execute("PRAGMA cipher_default_kdf_iter = 256000;", null, null);
|
||||
connection.execute("PRAGMA cipher_default_page_size = 4096;", null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postKey(SQLiteDatabase db) {
|
||||
db.rawExecSQL("PRAGMA kdf_iter = '1';");
|
||||
db.rawExecSQL("PRAGMA cipher_page_size = 4096;");
|
||||
public void postKey(SQLiteConnection connection) {
|
||||
connection.execute("PRAGMA kdf_iter = '256000';", null, null);
|
||||
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
|
||||
// if not vacuumed in a while, perform that operation
|
||||
long currentTime = System.currentTimeMillis();
|
||||
// 7 days
|
||||
if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) {
|
||||
db.rawExecSQL("VACUUM;");
|
||||
connection.execute("VACUUM;", null, null);
|
||||
TextSecurePreferences.setLastVacuumNow(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, true);
|
||||
|
||||
this.context = context.getApplicationContext();
|
||||
this.databaseSecret = databaseSecret;
|
||||
}
|
||||
|
||||
public static void migrateSqlCipher3To4IfNeeded(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) {
|
||||
String oldDbPath = context.getDatabasePath(CIPHER3_DATABASE_NAME).getPath();
|
||||
File oldDbFile = new File(oldDbPath);
|
||||
|
||||
// If the old SQLCipher3 database file doesn't exist then just return early
|
||||
if (!oldDbFile.exists()) { return; }
|
||||
|
||||
// If the new database file already exists then we probably had a failed migration and it's likely in
|
||||
// an invalid state so should delete it
|
||||
String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath();
|
||||
File newDbFile = new File(newDbPath);
|
||||
|
||||
if (newDbFile.exists()) { newDbFile.delete(); }
|
||||
|
||||
try {
|
||||
newDbFile.createNewFile();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// TODO: Communicate the error somehow???
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Open the old database
|
||||
SQLiteDatabase oldDb = SQLiteDatabase.openOrCreateDatabase(oldDbPath, databaseSecret.asString(), null, null, new SQLiteDatabaseHook() {
|
||||
@Override
|
||||
public void preKey(SQLiteConnection connection) {
|
||||
connection.execute("PRAGMA cipher_compatibility = 3;", null, null);
|
||||
connection.execute("PRAGMA kdf_iter = '1';", null, null);
|
||||
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postKey(SQLiteConnection connection) {
|
||||
connection.execute("PRAGMA cipher_compatibility = 3;", null, null);
|
||||
connection.execute("PRAGMA kdf_iter = '1';", null, null);
|
||||
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
|
||||
}
|
||||
});
|
||||
|
||||
// Export the old database to the new one (will have the default 'kdf_iter' and 'page_size' settings)
|
||||
int oldDbVersion = oldDb.getVersion();
|
||||
oldDb.rawExecSQL(
|
||||
String.format("ATTACH DATABASE '%s' AS sqlcipher4 KEY '%s'", newDbPath, databaseSecret.asString())
|
||||
);
|
||||
Cursor cursor = oldDb.rawQuery("SELECT sqlcipher_export('sqlcipher4')");
|
||||
cursor.moveToLast();
|
||||
cursor.close();
|
||||
oldDb.rawExecSQL("DETACH DATABASE sqlcipher4");
|
||||
oldDb.close();
|
||||
|
||||
// TODO: Performance testing
|
||||
|
||||
SQLiteDatabase newDb = SQLiteDatabase.openDatabase(newDbPath, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() {
|
||||
@Override
|
||||
public void preKey(SQLiteConnection connection) {
|
||||
connection.execute("PRAGMA cipher_default_kdf_iter = 256000;", null, null);
|
||||
connection.execute("PRAGMA cipher_default_page_size = 4096;", null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postKey(SQLiteConnection connection) {
|
||||
connection.execute("PRAGMA cipher_default_kdf_iter = 256000;", null, null);
|
||||
connection.execute("PRAGMA cipher_default_page_size = 4096;", null, null);
|
||||
}
|
||||
});
|
||||
newDb.setVersion(oldDbVersion);
|
||||
newDb.close();
|
||||
|
||||
// TODO: Delete 'CIPHER3_DATABASE_NAME'
|
||||
// TODO: What do we do if the deletion fails??? (The current logic will end up re-migrating...)
|
||||
// oldDbFile.delete();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// TODO: Communicate the error somehow???
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SmsDatabase.CREATE_TABLE);
|
||||
@ -195,9 +278,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
@Override
|
||||
public void onConfigure(SQLiteDatabase db) {
|
||||
super.onConfigure(db);
|
||||
// Loki - Enable write ahead logging mode and increase the cache size.
|
||||
// This should be disabled if we ever run into serious race condition bugs.
|
||||
db.enableWriteAheadLogging();
|
||||
|
||||
db.execSQL("PRAGMA cache_size = 10000");
|
||||
}
|
||||
|
||||
@ -420,14 +501,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public SQLiteDatabase getReadableDatabase() {
|
||||
return getReadableDatabase(databaseSecret.asString());
|
||||
}
|
||||
|
||||
public SQLiteDatabase getWritableDatabase() {
|
||||
return getWritableDatabase(databaseSecret.asString());
|
||||
}
|
||||
|
||||
public void markCurrent(SQLiteDatabase db) {
|
||||
db.setVersion(DATABASE_VERSION);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
@ -22,7 +22,7 @@ object DatabaseModule {
|
||||
|
||||
@JvmStatic
|
||||
fun init(context: Context) {
|
||||
SQLiteDatabase.loadLibs(context)
|
||||
System.loadLibrary("sqlcipher")
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ -33,6 +33,7 @@ object DatabaseModule {
|
||||
@Singleton
|
||||
fun provideOpenHelper(@ApplicationContext context: Context): SQLCipherOpenHelper {
|
||||
val dbSecret = DatabaseSecretProvider(context).orCreateDatabaseSecret
|
||||
SQLCipherOpenHelper.migrateSqlCipher3To4IfNeeded(context, dbSecret)
|
||||
return SQLCipherOpenHelper(context, dbSecret)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user