Skip to content

Commit d842349

Browse files
committed
Use JSON for settings imports/exports
1 parent 6afdbd6 commit d842349

8 files changed

Lines changed: 292 additions & 164 deletions

File tree

app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
import androidx.preference.Preference;
2222
import androidx.preference.PreferenceManager;
2323

24+
import com.grack.nanojson.JsonParserException;
25+
2426
import org.schabi.newpipe.NewPipeDatabase;
2527
import org.schabi.newpipe.R;
2628
import org.schabi.newpipe.error.ErrorInfo;
2729
import org.schabi.newpipe.error.ErrorUtil;
2830
import org.schabi.newpipe.error.UserAction;
31+
import org.schabi.newpipe.settings.export.BackupFileLocator;
2932
import org.schabi.newpipe.settings.export.ImportExportManager;
3033
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
3134
import org.schabi.newpipe.streams.io.StoredFileHelper;
@@ -60,8 +63,7 @@ public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
6063
@Nullable final String rootKey) {
6164
final File homeDir = ContextCompat.getDataDir(requireContext());
6265
Objects.requireNonNull(homeDir);
63-
manager = new ImportExportManager(new NewPipeFileLocator(homeDir));
64-
manager.deleteSettingsFile();
66+
manager = new ImportExportManager(new BackupFileLocator(homeDir));
6567

6668
importExportDataPathKey = getString(R.string.import_export_data_path);
6769

@@ -192,9 +194,13 @@ private void importDatabase(final StoredFileHelper file, final Uri importDataUri
192194
}
193195

194196
// if settings file exist, ask if it should be imported.
195-
if (manager.extractSettings(file)) {
197+
final boolean hasJsonPrefs = manager.exportHasJsonPrefs(file);
198+
if (hasJsonPrefs || manager.exportHasSerializedPrefs(file)) {
196199
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
197200
.setTitle(R.string.import_settings)
201+
.setMessage(hasJsonPrefs ? null : requireContext()
202+
.getString(R.string.import_settings_vulnerable_format))
203+
.setOnDismissListener(dialog -> finishImport(importDataUri))
198204
.setNegativeButton(R.string.cancel, (dialog, which) -> {
199205
dialog.dismiss();
200206
finishImport(importDataUri);
@@ -205,8 +211,12 @@ private void importDatabase(final StoredFileHelper file, final Uri importDataUri
205211
final SharedPreferences prefs = PreferenceManager
206212
.getDefaultSharedPreferences(context);
207213
try {
208-
manager.loadSharedPreferences(prefs);
209-
} catch (IOException | ClassNotFoundException e) {
214+
if (hasJsonPrefs) {
215+
manager.loadJsonPrefs(file, prefs);
216+
} else {
217+
manager.loadSerializedPrefs(file, prefs);
218+
}
219+
} catch (IOException | ClassNotFoundException | JsonParserException e) {
210220
showErrorSnackbar(e, "Importing preferences");
211221
return;
212222
}

app/src/main/java/org/schabi/newpipe/settings/NewPipeFileLocator.kt

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.schabi.newpipe.settings.export
2+
3+
import java.io.File
4+
5+
/**
6+
* Locates specific files of NewPipe based on the home directory of the app.
7+
*/
8+
class BackupFileLocator(private val homeDir: File) {
9+
companion object {
10+
const val FILE_NAME_DB = "newpipe.db"
11+
@Deprecated(
12+
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
13+
replaceWith = ReplaceWith("FILE_NAME_JSON_PREFS")
14+
)
15+
const val FILE_NAME_SERIALIZED_PREFS = "newpipe.settings"
16+
const val FILE_NAME_JSON_PREFS = "preferences.json"
17+
}
18+
19+
val dbDir by lazy { File(homeDir, "/databases") }
20+
21+
val db by lazy { File(dbDir, FILE_NAME_DB) }
22+
23+
val dbJournal by lazy { File(dbDir, "$FILE_NAME_DB-journal") }
24+
25+
val dbShm by lazy { File(dbDir, "$FILE_NAME_DB-shm") }
26+
27+
val dbWal by lazy { File(dbDir, "$FILE_NAME_DB-wal") }
28+
}

app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt

Lines changed: 105 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ package org.schabi.newpipe.settings.export
22

33
import android.content.SharedPreferences
44
import android.util.Log
5-
import org.schabi.newpipe.MainActivity.DEBUG
6-
import org.schabi.newpipe.settings.NewPipeFileLocator
5+
import com.grack.nanojson.JsonArray
6+
import com.grack.nanojson.JsonParser
7+
import com.grack.nanojson.JsonParserException
8+
import com.grack.nanojson.JsonWriter
79
import org.schabi.newpipe.streams.io.SharpOutputStream
810
import org.schabi.newpipe.streams.io.StoredFileHelper
911
import org.schabi.newpipe.util.ZipHelper
1012
import java.io.IOException
1113
import java.io.ObjectOutputStream
1214
import java.util.zip.ZipOutputStream
1315

14-
class ImportExportManager(private val fileLocator: NewPipeFileLocator) {
16+
class ImportExportManager(private val fileLocator: BackupFileLocator) {
1517
companion object {
16-
const val TAG = "ContentSetManager"
18+
const val TAG = "ImportExportManager"
1719
}
1820

1921
/**
@@ -23,27 +25,41 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) {
2325
@Throws(Exception::class)
2426
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
2527
file.create()
26-
ZipOutputStream(SharpOutputStream(file.stream).buffered())
27-
.use { outZip ->
28-
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
28+
ZipOutputStream(SharpOutputStream(file.stream).buffered()).use { outZip ->
29+
try {
30+
// add the database
31+
ZipHelper.addFileToZip(
32+
outZip,
33+
BackupFileLocator.FILE_NAME_DB,
34+
fileLocator.db.path,
35+
)
2936

30-
try {
31-
ObjectOutputStream(fileLocator.settings.outputStream()).use { output ->
37+
// add the legacy vulnerable serialized preferences (will be removed in the future)
38+
ZipHelper.addFileToZip(
39+
outZip,
40+
BackupFileLocator.FILE_NAME_SERIALIZED_PREFS
41+
) { byteOutput ->
42+
ObjectOutputStream(byteOutput).use { output ->
3243
output.writeObject(preferences.all)
3344
output.flush()
3445
}
35-
} catch (e: IOException) {
36-
if (DEBUG) {
37-
Log.e(TAG, "Unable to exportDatabase", e)
38-
}
3946
}
4047

41-
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
48+
// add the JSON preferences
49+
ZipHelper.addFileToZip(
50+
outZip,
51+
BackupFileLocator.FILE_NAME_JSON_PREFS
52+
) { byteOutput ->
53+
JsonWriter
54+
.indent("")
55+
.on(byteOutput)
56+
.`object`(preferences.all)
57+
.done()
58+
}
59+
} catch (e: Exception) {
60+
Log.e(TAG, "Unable to export serialized settings", e)
4261
}
43-
}
44-
45-
fun deleteSettingsFile() {
46-
fileLocator.settings.delete()
62+
}
4763
}
4864

4965
/**
@@ -56,7 +72,12 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) {
5672
}
5773

5874
fun extractDb(file: StoredFileHelper): Boolean {
59-
val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db")
75+
val success = ZipHelper.extractFileFromZip(
76+
file,
77+
BackupFileLocator.FILE_NAME_DB,
78+
fileLocator.db.path,
79+
)
80+
6081
if (success) {
6182
fileLocator.dbJournal.delete()
6283
fileLocator.dbWal.delete()
@@ -66,48 +87,81 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) {
6687
return success
6788
}
6889

69-
fun extractSettings(file: StoredFileHelper): Boolean {
70-
return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
90+
@Deprecated(
91+
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
92+
replaceWith = ReplaceWith("exportHasJsonPrefs")
93+
)
94+
fun exportHasSerializedPrefs(zipFile: StoredFileHelper): Boolean {
95+
return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS)
96+
}
97+
98+
fun exportHasJsonPrefs(zipFile: StoredFileHelper): Boolean {
99+
return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS)
71100
}
72101

73102
/**
74103
* Remove all shared preferences from the app and load the preferences supplied to the manager.
75104
*/
105+
@Deprecated(
106+
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
107+
replaceWith = ReplaceWith("loadJsonPrefs")
108+
)
76109
@Throws(IOException::class, ClassNotFoundException::class)
77-
fun loadSharedPreferences(preferences: SharedPreferences) {
78-
val preferenceEditor = preferences.edit()
79-
80-
PreferencesObjectInputStream(
81-
fileLocator.settings.inputStream()
82-
).use { input ->
83-
preferenceEditor.clear()
84-
@Suppress("UNCHECKED_CAST")
85-
val entries = input.readObject() as Map<String, *>
86-
for ((key, value) in entries) {
87-
when (value) {
88-
is Boolean -> {
89-
preferenceEditor.putBoolean(key, value)
90-
}
91-
is Float -> {
92-
preferenceEditor.putFloat(key, value)
93-
}
94-
is Int -> {
95-
preferenceEditor.putInt(key, value)
110+
fun loadSerializedPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
111+
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS) {
112+
PreferencesObjectInputStream(it).use { input ->
113+
val editor = preferences.edit()
114+
editor.clear()
115+
@Suppress("UNCHECKED_CAST")
116+
val entries = input.readObject() as Map<String, *>
117+
for ((key, value) in entries) {
118+
when (value) {
119+
is Boolean -> editor.putBoolean(key, value)
120+
is Float -> editor.putFloat(key, value)
121+
is Int -> editor.putInt(key, value)
122+
is Long -> editor.putLong(key, value)
123+
is String -> editor.putString(key, value)
124+
is Set<*> -> {
125+
// There are currently only Sets with type String possible
126+
@Suppress("UNCHECKED_CAST")
127+
editor.putStringSet(key, value as Set<String>?)
128+
}
96129
}
97-
is Long -> {
98-
preferenceEditor.putLong(key, value)
99-
}
100-
is String -> {
101-
preferenceEditor.putString(key, value)
102-
}
103-
is Set<*> -> {
104-
// There are currently only Sets with type String possible
105-
@Suppress("UNCHECKED_CAST")
106-
preferenceEditor.putStringSet(key, value as Set<String>?)
130+
}
131+
132+
if (!editor.commit()) {
133+
Log.e(TAG, "Unable to loadSerializedPrefs")
134+
}
135+
}
136+
}
137+
}
138+
139+
/**
140+
* Remove all shared preferences from the app and load the preferences supplied to the manager.
141+
*/
142+
@Throws(JsonParserException::class)
143+
fun loadJsonPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
144+
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS) {
145+
val editor = preferences.edit()
146+
editor.clear()
147+
148+
val jsonObject = JsonParser.`object`().from(it)
149+
for ((key, value) in jsonObject) {
150+
when (value) {
151+
is Boolean -> editor.putBoolean(key, value)
152+
is Float -> editor.putFloat(key, value)
153+
is Int -> editor.putInt(key, value)
154+
is Long -> editor.putLong(key, value)
155+
is String -> editor.putString(key, value)
156+
is JsonArray -> {
157+
editor.putStringSet(key, value.mapNotNull { e -> e as? String }.toSet())
107158
}
108159
}
109160
}
110-
preferenceEditor.commit()
161+
162+
if (!editor.commit()) {
163+
Log.e(TAG, "Unable to loadJsonPrefs")
164+
}
111165
}
112166
}
113167
}

0 commit comments

Comments
 (0)