diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml index 25a5f24ac..3fffd013c 100644 --- a/F-Droid/AndroidManifest.xml +++ b/F-Droid/AndroidManifest.xml @@ -83,6 +83,11 @@ android:name="org.fdroid.fdroid.data.ApkProvider" android:exported="false"/> + + Does not do any checks to see if the apk already exists or not. */ private ContentProviderOperation updateExistingApk(final Apk apk) { - Uri uri = ApkProvider.getContentUri(apk); + Uri uri = TempApkProvider.getApkUri(apk); ContentValues values = apk.toContentValues(); return ContentProviderOperation.newUpdate(uri).withValues(values).build(); } @@ -336,7 +337,7 @@ public class RepoUpdater { */ private ContentProviderOperation insertNewApk(final Apk apk) { ContentValues values = apk.toContentValues(); - Uri uri = ApkProvider.getContentUri(); + Uri uri = TempApkProvider.getContentUri(); return ContentProviderOperation.newInsert(uri).withValues(values).build(); } @@ -376,7 +377,7 @@ public class RepoUpdater { // TODO: Deal with more than MAX_QUERY_PARAMS... if (toDelete.size() > 0) { - Uri uri = ApkProvider.getContentUriForApks(repo, toDelete); + Uri uri = TempApkProvider.getApksUri(repo, toDelete); return ContentProviderOperation.newDelete(uri).build(); } else { return null; @@ -409,6 +410,13 @@ public class RepoUpdater { if (downloadedFile == null || !downloadedFile.exists()) throw new UpdateException(repo, downloadedFile + " does not exist!"); + // This is where we will store all of the metadata before commiting at the + // end of the process. This is due to the fact that we can't verify the cert + // the index was signed with until we've finished reading it - and we don't + // want to put stuff in the real database until we are sure it is from a + // trusted source. + TempApkProvider.Helper.init(context); + // Due to a bug in Android 5.0 Lollipop, the inclusion of spongycastle causes // breakage when verifying the signature of the downloaded .jar. For more // details, check out https://gitlab.com/fdroid/fdroidclient/issues/111. @@ -419,9 +427,6 @@ public class RepoUpdater { indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), progressListener, repo, (int) indexEntry.getSize()); - /* JarEntry can only read certificates after the file represented by that JarEntry - * has been read completely, so verification cannot run until now... */ - // Process the index... final SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); final XMLReader reader = parser.getXMLReader(); @@ -430,7 +435,11 @@ public class RepoUpdater { reader.parse(new InputSource(indexInputStream)); signingCertFromJar = getSigningCertFromJar(indexEntry); + // JarEntry can only read certificates after the file represented by that JarEntry + // has been read completely, so verification cannot run until now... assertSigningCertFromXmlCorrect(); + + TempApkProvider.Helper.commit(context); RepoProvider.Helper.update(context, repo, repoDetailsToSave); } catch (SAXException | ParserConfigurationException | IOException e) { diff --git a/F-Droid/src/org/fdroid/fdroid/data/ApkProvider.java b/F-Droid/src/org/fdroid/fdroid/data/ApkProvider.java index 495689c17..2a2b4bed1 100644 --- a/F-Droid/src/org/fdroid/fdroid/data/ApkProvider.java +++ b/F-Droid/src/org/fdroid/fdroid/data/ApkProvider.java @@ -227,15 +227,15 @@ public class ApkProvider extends FDroidProvider { private static final int CODE_REPO = CODE_APP + 1; private static final int CODE_APKS = CODE_REPO + 1; private static final int CODE_REPO_APPS = CODE_APKS + 1; - private static final int CODE_REPO_APK = CODE_REPO_APPS + 1; + protected static final int CODE_REPO_APK = CODE_REPO_APPS + 1; private static final String PROVIDER_NAME = "ApkProvider"; - private static final String PATH_APK = "apk"; + protected static final String PATH_APK = "apk"; private static final String PATH_APKS = "apks"; private static final String PATH_APP = "app"; private static final String PATH_REPO = "repo"; private static final String PATH_REPO_APPS = "repo-apps"; - private static final String PATH_REPO_APK = "repo-apk"; + protected static final String PATH_REPO_APK = "repo-apk"; private static final UriMatcher matcher = new UriMatcher(-1); @@ -322,7 +322,7 @@ public class ApkProvider extends FDroidProvider { .build(); } - private static String buildApkString(List apks) { + protected static String buildApkString(List apks) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < apks.size(); i++) { if (i != 0) { diff --git a/F-Droid/src/org/fdroid/fdroid/data/TempApkProvider.java b/F-Droid/src/org/fdroid/fdroid/data/TempApkProvider.java new file mode 100644 index 000000000..a8bc1dbbc --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/data/TempApkProvider.java @@ -0,0 +1,139 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.net.Uri; +import android.provider.BaseColumns; +import android.util.Log; + +import org.fdroid.fdroid.Utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class does all of its operations in a temporary sqlite table. + */ +public class TempApkProvider extends ApkProvider { + + private static final String TAG = "TempApkProvider"; + + private static final String PROVIDER_NAME = "TempApkProvider"; + + private static final String PATH_INIT = "init"; + private static final String PATH_COMMIT = "commit"; + private static final String PATH_ROLLBACK = "rollback"; + + private static final int CODE_INIT = 10000; + private static final int CODE_COMMIT = CODE_INIT + 1; + private static final int CODE_ROLLBACK = CODE_INIT + 2; + + private static final UriMatcher matcher = new UriMatcher(-1); + + static { + matcher.addURI(getAuthority(), PATH_INIT, CODE_INIT); + matcher.addURI(getAuthority(), PATH_COMMIT, CODE_COMMIT); + matcher.addURI(getAuthority(), PATH_ROLLBACK, CODE_ROLLBACK); + matcher.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE); + matcher.addURI(getAuthority(), PATH_REPO_APK + "/#/*", CODE_REPO_APK); + } + + @Override + protected String getTableName() { + return "temp_" + super.getTableName(); + } + + public static String getAuthority() { + return AUTHORITY + "." + PROVIDER_NAME; + } + + public static Uri getContentUri() { + return Uri.parse("content://" + getAuthority()); + } + + public static Uri getApkUri(Apk apk) { + return getContentUri() + .buildUpon() + .appendPath(PATH_APK) + .appendPath(Integer.toString(apk.vercode)) + .appendPath(apk.id) + .build(); + } + + public static Uri getApksUri(Repo repo, List apks) { + return getContentUri() + .buildUpon() + .appendPath(PATH_REPO_APK) + .appendPath(Long.toString(repo.id)) + .appendPath(buildApkString(apks)) + .build(); + } + + public static class Helper { + + /** + * Deletes the old temporary table (if it exists). Then creates a new temporary apk provider + * table and populates it with all the data from the real apk provider table. + */ + public static void init(Context context) { + Uri uri = Uri.withAppendedPath(getContentUri(), PATH_INIT); + context.getContentResolver().insert(uri, new ContentValues()); + } + + /** + * Saves data from the temp table to the apk table, by removing _EVERYTHING_ from the real + * apk table and inserting all of the records from here. The temporary table is then removed. + */ + public static void commit(Context context) { + Uri uri = Uri.withAppendedPath(getContentUri(), PATH_COMMIT); + context.getContentResolver().insert(uri, new ContentValues()); + } + + /** + * Not sure that this is strictly necessary, but this will remove the temp table. + */ + public static void rollback(Context context) { + Uri uri = Uri.withAppendedPath(getContentUri(), PATH_ROLLBACK); + context.getContentResolver().insert(uri, new ContentValues()); + } + + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + int code = matcher.match(uri); + + if (code == CODE_INIT) { + initTable(); + return null; + } else if (code == CODE_COMMIT) { + commitTable(); + return null; + } else if (code == CODE_ROLLBACK) { + removeTable(); + return null; + } else { + return super.insert(uri, values); + } + } + + private void initTable() { + removeTable(); + write().execSQL("CREATE TEMPORARY TABLE " + getTableName() + " AS SELECT * FROM " + DBHelper.TABLE_APK); + } + + private void commitTable() { + Log.d(TAG, "Deleting all apks from " + DBHelper.TABLE_APK + " so they can be copied from " + getTableName()); + write().execSQL("DELETE FROM " + DBHelper.TABLE_APK); + write().execSQL("INSERT INTO " + DBHelper.TABLE_APK + " SELECT * FROM " + getTableName()); + } + + private void removeTable() { + write().execSQL("DROP TABLE IF EXISTS " + getTableName()); + } +}