Use JSON for settings imports/exports
This commit is contained in:
parent
db840d6fc3
commit
dd4bbdffd6
8 changed files with 292 additions and 164 deletions
|
|
@ -21,11 +21,14 @@ import androidx.core.content.ContextCompat;
|
|||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.settings.export.BackupFileLocator;
|
||||
import org.schabi.newpipe.settings.export.ImportExportManager;
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
|
|
@ -60,8 +63,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
|||
@Nullable final String rootKey) {
|
||||
final File homeDir = ContextCompat.getDataDir(requireContext());
|
||||
Objects.requireNonNull(homeDir);
|
||||
manager = new ImportExportManager(new NewPipeFileLocator(homeDir));
|
||||
manager.deleteSettingsFile();
|
||||
manager = new ImportExportManager(new BackupFileLocator(homeDir));
|
||||
|
||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||
|
||||
|
|
@ -192,9 +194,13 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
|||
}
|
||||
|
||||
// if settings file exist, ask if it should be imported.
|
||||
if (manager.extractSettings(file)) {
|
||||
final boolean hasJsonPrefs = manager.exportHasJsonPrefs(file);
|
||||
if (hasJsonPrefs || manager.exportHasSerializedPrefs(file)) {
|
||||
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.import_settings)
|
||||
.setMessage(hasJsonPrefs ? null : requireContext()
|
||||
.getString(R.string.import_settings_vulnerable_format))
|
||||
.setOnDismissListener(dialog -> finishImport(importDataUri))
|
||||
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
finishImport(importDataUri);
|
||||
|
|
@ -205,8 +211,12 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
|||
final SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
try {
|
||||
manager.loadSharedPreferences(prefs);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
if (hasJsonPrefs) {
|
||||
manager.loadJsonPrefs(file, prefs);
|
||||
} else {
|
||||
manager.loadSerializedPrefs(file, prefs);
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException | JsonParserException e) {
|
||||
showErrorSnackbar(e, "Importing preferences");
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
package org.schabi.newpipe.settings
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Locates specific files of NewPipe based on the home directory of the app.
|
||||
*/
|
||||
class NewPipeFileLocator(private val homeDir: File) {
|
||||
|
||||
val dbDir by lazy { File(homeDir, "/databases") }
|
||||
|
||||
val db by lazy { File(homeDir, "/databases/newpipe.db") }
|
||||
|
||||
val dbJournal by lazy { File(homeDir, "/databases/newpipe.db-journal") }
|
||||
|
||||
val dbShm by lazy { File(homeDir, "/databases/newpipe.db-shm") }
|
||||
|
||||
val dbWal by lazy { File(homeDir, "/databases/newpipe.db-wal") }
|
||||
|
||||
val settings by lazy { File(homeDir, "/databases/newpipe.settings") }
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package org.schabi.newpipe.settings.export
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Locates specific files of NewPipe based on the home directory of the app.
|
||||
*/
|
||||
class BackupFileLocator(private val homeDir: File) {
|
||||
companion object {
|
||||
const val FILE_NAME_DB = "newpipe.db"
|
||||
@Deprecated(
|
||||
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
|
||||
replaceWith = ReplaceWith("FILE_NAME_JSON_PREFS")
|
||||
)
|
||||
const val FILE_NAME_SERIALIZED_PREFS = "newpipe.settings"
|
||||
const val FILE_NAME_JSON_PREFS = "preferences.json"
|
||||
}
|
||||
|
||||
val dbDir by lazy { File(homeDir, "/databases") }
|
||||
|
||||
val db by lazy { File(dbDir, FILE_NAME_DB) }
|
||||
|
||||
val dbJournal by lazy { File(dbDir, "$FILE_NAME_DB-journal") }
|
||||
|
||||
val dbShm by lazy { File(dbDir, "$FILE_NAME_DB-shm") }
|
||||
|
||||
val dbWal by lazy { File(dbDir, "$FILE_NAME_DB-wal") }
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@ package org.schabi.newpipe.settings.export
|
|||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.settings.NewPipeFileLocator
|
||||
import com.grack.nanojson.JsonArray
|
||||
import com.grack.nanojson.JsonParser
|
||||
import com.grack.nanojson.JsonParserException
|
||||
import com.grack.nanojson.JsonWriter
|
||||
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.ZipHelper
|
||||
|
|
@ -11,9 +13,9 @@ import java.io.IOException
|
|||
import java.io.ObjectOutputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class ImportExportManager(private val fileLocator: NewPipeFileLocator) {
|
||||
class ImportExportManager(private val fileLocator: BackupFileLocator) {
|
||||
companion object {
|
||||
const val TAG = "ContentSetManager"
|
||||
const val TAG = "ImportExportManager"
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,27 +25,41 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) {
|
|||
@Throws(Exception::class)
|
||||
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
|
||||
file.create()
|
||||
ZipOutputStream(SharpOutputStream(file.stream).buffered())
|
||||
.use { outZip ->
|
||||
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
|
||||
ZipOutputStream(SharpOutputStream(file.stream).buffered()).use { outZip ->
|
||||
try {
|
||||
// add the database
|
||||
ZipHelper.addFileToZip(
|
||||
outZip,
|
||||
BackupFileLocator.FILE_NAME_DB,
|
||||
fileLocator.db.path,
|
||||
)
|
||||
|
||||
try {
|
||||
ObjectOutputStream(fileLocator.settings.outputStream()).use { output ->
|
||||
// add the legacy vulnerable serialized preferences (will be removed in the future)
|
||||
ZipHelper.addFileToZip(
|
||||
outZip,
|
||||
BackupFileLocator.FILE_NAME_SERIALIZED_PREFS
|
||||
) { byteOutput ->
|
||||
ObjectOutputStream(byteOutput).use { output ->
|
||||
output.writeObject(preferences.all)
|
||||
output.flush()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "Unable to exportDatabase", e)
|
||||
}
|
||||
}
|
||||
|
||||
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
|
||||
// add the JSON preferences
|
||||
ZipHelper.addFileToZip(
|
||||
outZip,
|
||||
BackupFileLocator.FILE_NAME_JSON_PREFS
|
||||
) { byteOutput ->
|
||||
JsonWriter
|
||||
.indent("")
|
||||
.on(byteOutput)
|
||||
.`object`(preferences.all)
|
||||
.done()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to export serialized settings", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSettingsFile() {
|
||||
fileLocator.settings.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -56,7 +72,12 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) {
|
|||
}
|
||||
|
||||
fun extractDb(file: StoredFileHelper): Boolean {
|
||||
val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db")
|
||||
val success = ZipHelper.extractFileFromZip(
|
||||
file,
|
||||
BackupFileLocator.FILE_NAME_DB,
|
||||
fileLocator.db.path,
|
||||
)
|
||||
|
||||
if (success) {
|
||||
fileLocator.dbJournal.delete()
|
||||
fileLocator.dbWal.delete()
|
||||
|
|
@ -66,48 +87,81 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) {
|
|||
return success
|
||||
}
|
||||
|
||||
fun extractSettings(file: StoredFileHelper): Boolean {
|
||||
return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
|
||||
@Deprecated(
|
||||
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
|
||||
replaceWith = ReplaceWith("exportHasJsonPrefs")
|
||||
)
|
||||
fun exportHasSerializedPrefs(zipFile: StoredFileHelper): Boolean {
|
||||
return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS)
|
||||
}
|
||||
|
||||
fun exportHasJsonPrefs(zipFile: StoredFileHelper): Boolean {
|
||||
return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all shared preferences from the app and load the preferences supplied to the manager.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
|
||||
replaceWith = ReplaceWith("loadJsonPrefs")
|
||||
)
|
||||
@Throws(IOException::class, ClassNotFoundException::class)
|
||||
fun loadSharedPreferences(preferences: SharedPreferences) {
|
||||
val preferenceEditor = preferences.edit()
|
||||
fun loadSerializedPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
|
||||
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS) {
|
||||
PreferencesObjectInputStream(it).use { input ->
|
||||
val editor = preferences.edit()
|
||||
editor.clear()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val entries = input.readObject() as Map<String, *>
|
||||
for ((key, value) in entries) {
|
||||
when (value) {
|
||||
is Boolean -> editor.putBoolean(key, value)
|
||||
is Float -> editor.putFloat(key, value)
|
||||
is Int -> editor.putInt(key, value)
|
||||
is Long -> editor.putLong(key, value)
|
||||
is String -> editor.putString(key, value)
|
||||
is Set<*> -> {
|
||||
// There are currently only Sets with type String possible
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
editor.putStringSet(key, value as Set<String>?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PreferencesObjectInputStream(
|
||||
fileLocator.settings.inputStream()
|
||||
).use { input ->
|
||||
preferenceEditor.clear()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val entries = input.readObject() as Map<String, *>
|
||||
for ((key, value) in entries) {
|
||||
if (!editor.commit()) {
|
||||
Log.e(TAG, "Unable to loadSerializedPrefs")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all shared preferences from the app and load the preferences supplied to the manager.
|
||||
*/
|
||||
@Throws(JsonParserException::class)
|
||||
fun loadJsonPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
|
||||
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS) {
|
||||
val editor = preferences.edit()
|
||||
editor.clear()
|
||||
|
||||
val jsonObject = JsonParser.`object`().from(it)
|
||||
for ((key, value) in jsonObject) {
|
||||
when (value) {
|
||||
is Boolean -> {
|
||||
preferenceEditor.putBoolean(key, value)
|
||||
}
|
||||
is Float -> {
|
||||
preferenceEditor.putFloat(key, value)
|
||||
}
|
||||
is Int -> {
|
||||
preferenceEditor.putInt(key, value)
|
||||
}
|
||||
is Long -> {
|
||||
preferenceEditor.putLong(key, value)
|
||||
}
|
||||
is String -> {
|
||||
preferenceEditor.putString(key, value)
|
||||
}
|
||||
is Set<*> -> {
|
||||
// There are currently only Sets with type String possible
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferenceEditor.putStringSet(key, value as Set<String>?)
|
||||
is Boolean -> editor.putBoolean(key, value)
|
||||
is Float -> editor.putFloat(key, value)
|
||||
is Int -> editor.putInt(key, value)
|
||||
is Long -> editor.putLong(key, value)
|
||||
is String -> editor.putString(key, value)
|
||||
is JsonArray -> {
|
||||
editor.putStringSet(key, value.mapNotNull { e -> e as? String }.toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
preferenceEditor.commit()
|
||||
|
||||
if (!editor.commit()) {
|
||||
Log.e(TAG, "Unable to loadJsonPrefs")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue