Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge changes from v2024.2/3/4 #6367

Merged
merged 7 commits into from
Aug 23, 2024
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.odk.collect.androidshared.utils

import timber.log.Timber
import java.io.File

object PathUtils {
@JvmStatic
fun getAbsoluteFilePath(dirPath: String, filePath: String): String {
val absoluteFilePath =
if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath

val canonicalAbsoluteFilePath = File(absoluteFilePath).canonicalPath
val canonicalDirPath = File(dirPath).canonicalPath
if (!canonicalAbsoluteFilePath.startsWith(canonicalDirPath)) {
Timber.e(
"Attempt to access file outside of Collect directory:\n" +
"dirPath: $dirPath\n" +
"filePath: $filePath\n" +
"absoluteFilePath: $absoluteFilePath\n" +
"canonicalAbsoluteFilePath: $canonicalAbsoluteFilePath\n" +
"canonicalDirPath: $canonicalDirPath"
)
}
return absoluteFilePath
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.odk.collect.androidshared.utils

import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.Test
import org.odk.collect.shared.TempFiles
import java.io.File

class PathUtilsTest {
@Test
fun `getAbsoluteFilePath() returns filePath prepended with dirPath`() {
val path = PathUtils.getAbsoluteFilePath("/anotherRoot/anotherDir", "root/dir/file")
assertThat(path, equalTo("/anotherRoot/anotherDir/root/dir/file"))
}

@Test
fun `getAbsoluteFilePath() returns valid path when filePath does not start with seperator`() {
val path = PathUtils.getAbsoluteFilePath("/root/dir", "file")
assertThat(path, equalTo("/root/dir/file"))
}

@Test
fun `getAbsoluteFilePath() returns filePath when it starts with dirPath`() {
val path = PathUtils.getAbsoluteFilePath("/root/dir", "/root/dir/file")
assertThat(path, equalTo("/root/dir/file"))
}

@Test
fun `getAbsoluteFilePath() works when dirPath is not canonical`() {
val tempDir = TempFiles.createTempDir()
val nonCanonicalPath =
tempDir.canonicalPath + File.separator + ".." + File.separator + tempDir.name
assertThat(File(nonCanonicalPath).canonicalPath, equalTo(tempDir.canonicalPath))

val path = PathUtils.getAbsoluteFilePath(nonCanonicalPath, "file")
assertThat(path, equalTo(nonCanonicalPath + File.separator + "file"))
}
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,68 @@
package org.odk.collect.android.feature.external

import android.content.Context
import android.content.Intent
import android.provider.BaseColumns._ID
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import org.odk.collect.android.external.InstancesContract
import org.odk.collect.android.instancemanagement.send.InstanceUploaderActivity
import org.odk.collect.android.support.TestDependencies
import org.odk.collect.android.support.pages.FormEntryPage
import org.odk.collect.android.support.pages.OkDialog
import org.odk.collect.android.support.rules.CollectTestRule
import org.odk.collect.android.support.rules.TestRuleChain
import org.odk.collect.android.utilities.ApplicationConstants

@RunWith(AndroidJUnit4::class)
class InstanceUploadActionTest {

val collectTestRule = CollectTestRule()
private val rule = CollectTestRule()
private val context = ApplicationProvider.getApplicationContext<Context>()
private val testDependencies = TestDependencies()

@get:Rule
val rule: RuleChain = TestRuleChain.chain()
.around(collectTestRule)
val chain: RuleChain = TestRuleChain.chain(testDependencies)
.around(rule)

@Test
fun whenInstanceDoesNotExist_showsError() {
val instanceIds = longArrayOf(11)
instanceUploadAction(instanceIds)
fun whenIntentIncludesURLExtra_instancesAreUploadedToThatURL() {
rule.startAtMainMenu()
.copyForm("one-question.xml")
.startBlankForm("One Question")
.fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("what is your age", "34"))

OkDialog()
.assertOnPage()
.assertText(org.odk.collect.strings.R.string.no_forms_uploaded)
val instanceId =
context.contentResolver.query(InstancesContract.getUri("DEMO"), null, null, null, null)
.use {
it!!.moveToFirst()
it.getLong(it.getColumnIndex(_ID))
}

val intent = Intent("org.odk.collect.android.INSTANCE_UPLOAD")
intent.type = InstancesContract.CONTENT_TYPE
intent.putExtra(ApplicationConstants.BundleKeys.URL, testDependencies.server.url)
intent.putExtra("instances", longArrayOf(instanceId))

rule.launch(intent, OkDialog())
.assertTextInDialog("One Question - Success")
assertThat(testDependencies.server.submissions.size, equalTo(1))
}

private fun instanceUploadAction(instanceIds: LongArray) {
@Test
fun whenInstanceDoesNotExist_showsError() {
rule.startAtMainMenu()

val intent = Intent("org.odk.collect.android.INSTANCE_UPLOAD")
intent.type = InstancesContract.CONTENT_TYPE
intent.putExtra("instances", instanceIds)
collectTestRule.launch<InstanceUploaderActivity>(intent)
intent.putExtra("instances", longArrayOf(11))

rule.launch(intent, OkDialog())
.assertText(org.odk.collect.strings.R.string.no_forms_uploaded)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ object AnalyticsEvents {

const val INSTANCE_PROVIDER_INSERT = "InstanceProviderInsert"

const val INSTANCE_PROVIDER_UPDATE = "InstanceProviderUpdate"

const val INSTANCE_PROVIDER_DELETE = "InstanceProviderDelete"

/**
Expand All @@ -101,4 +99,9 @@ object AnalyticsEvents {
const val DELETE_SAVED_FORM_FEW = "DeleteSavedFormFew" // < 10
const val DELETE_SAVED_FORM_TENS = "DeleteSavedFormTens" // >= 10
const val DELETE_SAVED_FORM_HUNDREDS = "DeleteSavedFormHundreds" // >= 100

/**
* Tracks how often the INSTANCE_UPLOAD action is used with a custom server URL
*/
const val INSTANCE_UPLOAD_CUSTOM_SERVER = "InstanceUploadCustomServer"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import android.database.Cursor
import android.provider.BaseColumns
import org.odk.collect.android.database.forms.DatabaseFormColumns
import org.odk.collect.android.database.instances.DatabaseInstanceColumns
import org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath
import org.odk.collect.forms.Form
import org.odk.collect.forms.instances.Instance
import org.odk.collect.shared.PathUtils.getAbsoluteFilePath
import org.odk.collect.shared.PathUtils.getRelativeFilePath
import java.lang.Boolean

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.odk.collect.android.database.DatabaseConstants.SAVEPOINTS_DATABASE_VE
import org.odk.collect.android.database.DatabaseConstants.SAVEPOINTS_TABLE_NAME
import org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.FORM_DB_ID
import org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.INSTANCE_DB_ID
import org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath
import org.odk.collect.db.sqlite.CursorExt.foldAndClose
import org.odk.collect.db.sqlite.DatabaseConnection
import org.odk.collect.db.sqlite.SQLiteDatabaseExt.delete
Expand Down Expand Up @@ -120,11 +121,11 @@ class DatabaseSavepointsRepository(
return Savepoint(
cursor.getLong(formDbIdColumnIndex),
if (cursor.isNull(instanceDbIdColumnIndex)) null else cursor.getLong(instanceDbIdColumnIndex),
PathUtils.getAbsoluteFilePath(
getAbsoluteFilePath(
cachePath,
cursor.getString(savepointFilePathColumnIndex)
),
PathUtils.getAbsoluteFilePath(
getAbsoluteFilePath(
instancesPath,
cursor.getString(instanceDirPathColumnIndex)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@
package org.odk.collect.android.external;

import static android.provider.BaseColumns._ID;
import static org.odk.collect.android.database.DatabaseObjectMapper.getFormFromCurrentCursorPosition;
import static org.odk.collect.android.database.DatabaseObjectMapper.getFormFromValues;
import static org.odk.collect.android.database.DatabaseObjectMapper.getValuesFromForm;
import static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_DELETE;
import static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_SEND;
import static org.odk.collect.android.database.forms.DatabaseFormColumns.BASE64_RSA_PUBLIC_KEY;
Expand Down Expand Up @@ -52,11 +49,9 @@
import org.odk.collect.android.injection.DaggerUtils;
import org.odk.collect.android.itemsets.FastExternalItemsetsRepository;
import org.odk.collect.android.storage.StoragePathProvider;
import org.odk.collect.android.storage.StorageSubdirectory;
import org.odk.collect.android.utilities.ContentUriHelper;
import org.odk.collect.android.utilities.FormsRepositoryProvider;
import org.odk.collect.android.utilities.InstancesRepositoryProvider;
import org.odk.collect.forms.Form;
import org.odk.collect.forms.FormsRepository;
import org.odk.collect.forms.instances.InstancesRepository;
import org.odk.collect.projects.ProjectsRepository;
Expand Down Expand Up @@ -189,20 +184,7 @@ public String getType(@NonNull Uri uri) {

@Override
public synchronized Uri insert(@NonNull Uri uri, ContentValues initialValues) {
deferDaggerInit();

// Validate the requested uri
if (URI_MATCHER.match(uri) != FORMS) {
throw new IllegalArgumentException("Unknown URI " + uri);
}

String projectId = getProjectId(uri);
logServerEvent(projectId, AnalyticsEvents.FORMS_PROVIDER_INSERT);

String formsPath = storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, projectId);
String cachePath = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, projectId);
Form form = getFormsRepository(projectId).save(getFormFromValues(initialValues, formsPath, cachePath));
return FormsContract.getUri(projectId, form.getDbId());
return null;
}

/**
Expand Down Expand Up @@ -248,53 +230,7 @@ public int delete(@NonNull Uri uri, String where, String[] whereArgs) {

@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
deferDaggerInit();

String projectId = getProjectId(uri);
logServerEvent(projectId, AnalyticsEvents.FORMS_PROVIDER_UPDATE);

FormsRepository formsRepository = getFormsRepository(projectId);
String formsPath = storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, projectId);
String cachePath = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, projectId);

int count;

switch (URI_MATCHER.match(uri)) {
case FORMS:
try (Cursor cursor = databaseQuery(projectId, null, where, whereArgs, null, null, null)) {
while (cursor.moveToNext()) {
Form form = getFormFromCurrentCursorPosition(cursor, formsPath, cachePath);
ContentValues existingValues = getValuesFromForm(form, formsPath);
existingValues.putAll(values);

formsRepository.save(getFormFromValues(existingValues, formsPath, cachePath));
}

count = cursor.getCount();
}
break;

case FORM_ID:
Form form = formsRepository.get(ContentUriHelper.getIdFromUri(uri));
if (form != null) {
ContentValues existingValues = getValuesFromForm(form, formsPath);
existingValues.putAll(values);

formsRepository.save(getFormFromValues(existingValues, formsPath, cachePath));
count = 1;
} else {
count = 0;
}

break;

default:
throw new IllegalArgumentException("Unknown URI " + uri);
}

getContext().getContentResolver().notifyChange(uri, null);

return count;
return 0;
}

@NotNull
Expand Down
Loading