Skip to content

Commit

Permalink
Use JSON for settings imports/exports
Browse files Browse the repository at this point in the history
  • Loading branch information
Stypox committed Mar 30, 2024
1 parent 6afdbd6 commit d842349
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
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;
Expand Down Expand Up @@ -60,8 +63,7 @@ public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@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);

Expand Down Expand Up @@ -192,9 +194,13 @@ private void importDatabase(final StoredFileHelper file, final Uri importDataUri
}

// 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);
Expand All @@ -205,8 +211,12 @@ private void importDatabase(final StoredFileHelper file, final Uri importDataUri
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;
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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") }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ 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
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"
}

/**
Expand All @@ -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()
}
}

/**
Expand All @@ -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()
Expand All @@ -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()

PreferencesObjectInputStream(
fileLocator.settings.inputStream()
).use { input ->
preferenceEditor.clear()
@Suppress("UNCHECKED_CAST")
val entries = input.readObject() as Map<String, *>
for ((key, value) in entries) {
when (value) {
is Boolean -> {
preferenceEditor.putBoolean(key, value)
}
is Float -> {
preferenceEditor.putFloat(key, value)
}
is Int -> {
preferenceEditor.putInt(key, value)
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>?)
}
}
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>?)
}

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 -> 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

0 comments on commit d842349

Please sign in to comment.