Use JSON for settings imports/exports
This commit is contained in:
parent
6afdbd6fd3
commit
d8423499dc
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpInputStream;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 28.01.18.
|
||||
* Copyright 2018 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||
|
@ -34,73 +37,154 @@ import org.schabi.newpipe.streams.io.StoredFileHelper;
|
|||
*/
|
||||
|
||||
public final class ZipHelper {
|
||||
private ZipHelper() { }
|
||||
|
||||
private static final int BUFFER_SIZE = 2048;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface InputStreamConsumer {
|
||||
void acceptStream(InputStream inputStream) throws IOException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface OutputStreamConsumer {
|
||||
void acceptStream(OutputStream outputStream) throws IOException;
|
||||
}
|
||||
|
||||
|
||||
private ZipHelper() { }
|
||||
|
||||
|
||||
/**
|
||||
* This function helps to create zip files.
|
||||
* Caution this will override the original file.
|
||||
* This function helps to create zip files. Caution this will overwrite the original file.
|
||||
*
|
||||
* @param outZip The ZipOutputStream where the data should be stored in
|
||||
* @param file The path of the file that should be added to zip.
|
||||
* @param name The path of the file inside the zip.
|
||||
* @throws Exception
|
||||
* @param outZip the ZipOutputStream where the data should be stored in
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param fileOnDisk the path of the file on the disk that should be added to zip
|
||||
*/
|
||||
public static void addFileToZip(final ZipOutputStream outZip, final String file,
|
||||
final String name) throws Exception {
|
||||
public static void addFileToZip(final ZipOutputStream outZip,
|
||||
final String nameInZip,
|
||||
final String fileOnDisk) throws IOException {
|
||||
try (FileInputStream fi = new FileInputStream(fileOnDisk)) {
|
||||
addFileToZip(outZip, nameInZip, fi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function helps to create zip files. Caution this will overwrite the original file.
|
||||
*
|
||||
* @param outZip the ZipOutputStream where the data should be stored in
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param streamConsumer will be called with an output stream that will go to the output file
|
||||
*/
|
||||
public static void addFileToZip(final ZipOutputStream outZip,
|
||||
final String nameInZip,
|
||||
final OutputStreamConsumer streamConsumer) throws IOException {
|
||||
final byte[] bytes;
|
||||
try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream()) {
|
||||
streamConsumer.acceptStream(byteOutput);
|
||||
bytes = byteOutput.toByteArray();
|
||||
}
|
||||
|
||||
try (ByteArrayInputStream byteInput = new ByteArrayInputStream(bytes)) {
|
||||
ZipHelper.addFileToZip(outZip, nameInZip, byteInput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function helps to create zip files. Caution this will overwrite the original file.
|
||||
*
|
||||
* @param outZip the ZipOutputStream where the data should be stored in
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param inputStream the content to put inside the file
|
||||
*/
|
||||
public static void addFileToZip(final ZipOutputStream outZip,
|
||||
final String nameInZip,
|
||||
final InputStream inputStream) throws IOException {
|
||||
final byte[] data = new byte[BUFFER_SIZE];
|
||||
try (FileInputStream fi = new FileInputStream(file);
|
||||
BufferedInputStream inputStream = new BufferedInputStream(fi, BUFFER_SIZE)) {
|
||||
final ZipEntry entry = new ZipEntry(name);
|
||||
try (BufferedInputStream bufferedInputStream =
|
||||
new BufferedInputStream(inputStream, BUFFER_SIZE)) {
|
||||
final ZipEntry entry = new ZipEntry(nameInZip);
|
||||
outZip.putNextEntry(entry);
|
||||
int count;
|
||||
while ((count = inputStream.read(data, 0, BUFFER_SIZE)) != -1) {
|
||||
while ((count = bufferedInputStream.read(data, 0, BUFFER_SIZE)) != -1) {
|
||||
outZip.write(data, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will extract data from ZipInputStream.
|
||||
* Caution this will override the original file.
|
||||
* This will extract data from ZipInputStream. Caution this will overwrite the original file.
|
||||
*
|
||||
* @param zipFile The zip file
|
||||
* @param file The path of the file on the disk where the data should be extracted to.
|
||||
* @param name The path of the file inside the zip.
|
||||
* @param zipFile the zip file to extract from
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param fileOnDisk the path of the file on the disk where the data should be extracted to
|
||||
* @return will return true if the file was found within the zip file
|
||||
* @throws Exception
|
||||
*/
|
||||
public static boolean extractFileFromZip(final StoredFileHelper zipFile, final String file,
|
||||
final String name) throws Exception {
|
||||
public static boolean extractFileFromZip(final StoredFileHelper zipFile,
|
||||
final String nameInZip,
|
||||
final String fileOnDisk) throws IOException {
|
||||
return extractFileFromZip(zipFile, nameInZip, input -> {
|
||||
// delete old file first
|
||||
final File oldFile = new File(fileOnDisk);
|
||||
if (oldFile.exists()) {
|
||||
if (!oldFile.delete()) {
|
||||
throw new IOException("Could not delete " + fileOnDisk);
|
||||
}
|
||||
}
|
||||
|
||||
final byte[] data = new byte[BUFFER_SIZE];
|
||||
try (FileOutputStream outFile = new FileOutputStream(fileOnDisk)) {
|
||||
int count;
|
||||
while ((count = input.read(data)) != -1) {
|
||||
outFile.write(data, 0, count);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This will extract data from ZipInputStream.
|
||||
*
|
||||
* @param zipFile the zip file to extract from
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param streamConsumer will be called with the input stream from the file inside the zip
|
||||
* @return will return true if the file was found within the zip file
|
||||
*/
|
||||
public static boolean extractFileFromZip(final StoredFileHelper zipFile,
|
||||
final String nameInZip,
|
||||
final InputStreamConsumer streamConsumer)
|
||||
throws IOException {
|
||||
try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream(
|
||||
new SharpInputStream(zipFile.getStream())))) {
|
||||
ZipEntry ze;
|
||||
while ((ze = inZip.getNextEntry()) != null) {
|
||||
if (ze.getName().equals(nameInZip)) {
|
||||
streamConsumer.acceptStream(inZip);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param zipFile the zip file
|
||||
* @param fileInZip the filename to check
|
||||
* @return whether the provided filename is in the zip; only the first level is checked
|
||||
*/
|
||||
public static boolean zipContainsFile(final StoredFileHelper zipFile, final String fileInZip)
|
||||
throws Exception {
|
||||
try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream(
|
||||
new SharpInputStream(zipFile.getStream())))) {
|
||||
final byte[] data = new byte[BUFFER_SIZE];
|
||||
boolean found = false;
|
||||
ZipEntry ze;
|
||||
|
||||
while ((ze = inZip.getNextEntry()) != null) {
|
||||
if (ze.getName().equals(name)) {
|
||||
found = true;
|
||||
// delete old file first
|
||||
final File oldFile = new File(file);
|
||||
if (oldFile.exists()) {
|
||||
if (!oldFile.delete()) {
|
||||
throw new Exception("Could not delete " + file);
|
||||
}
|
||||
}
|
||||
|
||||
try (FileOutputStream outFile = new FileOutputStream(file)) {
|
||||
int count = 0;
|
||||
while ((count = inZip.read(data)) != -1) {
|
||||
outFile.write(data, 0, count);
|
||||
}
|
||||
}
|
||||
|
||||
inZip.closeEntry();
|
||||
if (ze.getName().equals(fileInZip)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -856,4 +856,5 @@
|
|||
</plurals>
|
||||
<string name="show_more">Show more</string>
|
||||
<string name="show_less">Show less</string>
|
||||
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.schabi.newpipe.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.grack.nanojson.JsonParser
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertThrows
|
||||
|
@ -18,6 +19,7 @@ import org.mockito.Mockito.verify
|
|||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.withSettings
|
||||
import org.mockito.junit.MockitoJUnitRunner
|
||||
import org.schabi.newpipe.settings.export.BackupFileLocator
|
||||
import org.schabi.newpipe.settings.export.ImportExportManager
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import us.shandian.giga.io.FileStream
|
||||
|
@ -33,21 +35,19 @@ class ImportExportManagerTest {
|
|||
private val classloader = ImportExportManager::class.java.classLoader!!
|
||||
}
|
||||
|
||||
private lateinit var fileLocator: NewPipeFileLocator
|
||||
private lateinit var fileLocator: BackupFileLocator
|
||||
private lateinit var storedFileHelper: StoredFileHelper
|
||||
|
||||
@Before
|
||||
fun setupFileLocator() {
|
||||
fileLocator = Mockito.mock(NewPipeFileLocator::class.java, withSettings().stubOnly())
|
||||
fileLocator = Mockito.mock(BackupFileLocator::class.java, withSettings().stubOnly())
|
||||
storedFileHelper = Mockito.mock(StoredFileHelper::class.java, withSettings().stubOnly())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `The settings must be exported successfully in the correct format`() {
|
||||
val db = File(classloader.getResource("settings/newpipe.db")!!.file)
|
||||
val newpipeSettings = File.createTempFile("newpipe_", "")
|
||||
`when`(fileLocator.db).thenReturn(db)
|
||||
`when`(fileLocator.settings).thenReturn(newpipeSettings)
|
||||
|
||||
val expectedPreferences = mapOf("such pref" to "much wow")
|
||||
val sharedPreferences =
|
||||
|
@ -60,7 +60,7 @@ class ImportExportManagerTest {
|
|||
|
||||
val zipFile = ZipFile(output)
|
||||
val entries = zipFile.entries().toList()
|
||||
assertEquals(2, entries.size)
|
||||
assertEquals(3, entries.size)
|
||||
|
||||
zipFile.getInputStream(entries.first { it.name == "newpipe.db" }).use { actual ->
|
||||
db.inputStream().use { expected ->
|
||||
|
@ -72,26 +72,11 @@ class ImportExportManagerTest {
|
|||
val actualPreferences = ObjectInputStream(actual).readObject()
|
||||
assertEquals(expectedPreferences, actualPreferences)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Settings file must be deleted`() {
|
||||
val settings = File.createTempFile("newpipe_", "")
|
||||
`when`(fileLocator.settings).thenReturn(settings)
|
||||
|
||||
ImportExportManager(fileLocator).deleteSettingsFile()
|
||||
|
||||
assertFalse(settings.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Deleting settings file must do nothing if none exist`() {
|
||||
val settings = File("non_existent")
|
||||
`when`(fileLocator.settings).thenReturn(settings)
|
||||
|
||||
ImportExportManager(fileLocator).deleteSettingsFile()
|
||||
|
||||
assertFalse(settings.exists())
|
||||
zipFile.getInputStream(entries.first { it.name == "preferences.json" }).use { actual ->
|
||||
val actualPreferences = JsonParser.`object`().from(actual)
|
||||
assertEquals(expectedPreferences, actualPreferences)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -156,38 +141,29 @@ class ImportExportManagerTest {
|
|||
|
||||
@Test
|
||||
fun `Contains setting must return true if a settings file exists in the zip`() {
|
||||
val settings = File.createTempFile("newpipe_", "")
|
||||
`when`(fileLocator.settings).thenReturn(settings)
|
||||
|
||||
val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!)
|
||||
`when`(storedFileHelper.stream).thenReturn(FileStream(zip))
|
||||
val contains = ImportExportManager(fileLocator).extractSettings(storedFileHelper)
|
||||
|
||||
assertTrue(contains)
|
||||
assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Contains setting must return false if a no settings file exists in the zip`() {
|
||||
val settings = File.createTempFile("newpipe_", "")
|
||||
`when`(fileLocator.settings).thenReturn(settings)
|
||||
|
||||
val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!)
|
||||
`when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
|
||||
val contains = ImportExportManager(fileLocator).extractSettings(storedFileHelper)
|
||||
|
||||
assertFalse(contains)
|
||||
assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Preferences must be set from the settings file`() {
|
||||
val settings = File(classloader.getResource("settings/newpipe.settings")!!.path)
|
||||
`when`(fileLocator.settings).thenReturn(settings)
|
||||
val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!)
|
||||
`when`(storedFileHelper.stream).thenReturn(FileStream(zip))
|
||||
|
||||
val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly())
|
||||
val editor = Mockito.mock(SharedPreferences.Editor::class.java)
|
||||
`when`(preferences.edit()).thenReturn(editor)
|
||||
`when`(editor.commit()).thenReturn(true)
|
||||
|
||||
ImportExportManager(fileLocator).loadSharedPreferences(preferences)
|
||||
ImportExportManager(fileLocator).loadSerializedPrefs(storedFileHelper, preferences)
|
||||
|
||||
verify(editor, atLeastOnce()).putBoolean(anyString(), anyBoolean())
|
||||
verify(editor, atLeastOnce()).putString(anyString(), anyString())
|
||||
|
@ -196,19 +172,15 @@ class ImportExportManagerTest {
|
|||
|
||||
@Test
|
||||
fun `Importing preferences with a serialization injected class should fail`() {
|
||||
val settings = File.createTempFile("newpipe_", "")
|
||||
`when`(fileLocator.settings).thenReturn(settings)
|
||||
|
||||
val emptyZip = File(classloader.getResource("settings/vulnerable_serialization.zip")?.file!!)
|
||||
`when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
|
||||
Assume.assumeTrue(ImportExportManager(fileLocator).extractSettings(storedFileHelper))
|
||||
|
||||
val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly())
|
||||
val editor = Mockito.mock(SharedPreferences.Editor::class.java)
|
||||
`when`(preferences.edit()).thenReturn(editor)
|
||||
|
||||
assertThrows(ClassNotFoundException::class.java) {
|
||||
ImportExportManager(fileLocator).loadSharedPreferences(preferences)
|
||||
ImportExportManager(fileLocator).loadSerializedPrefs(storedFileHelper, preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Loading…
Reference in a new issue