From 00d873a3c5709200451c74b46860a67c1c1a52c7 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 22 Aug 2026 00:56:59 +0000 Subject: Require explicit tap before ShareReceiverActivity saves anything This activity is exported (any installed app can target it directly with an ACTION_SEND intent, not just via the system share sheet). Starting extraction/Stash-save immediately on launch meant any other app could silently make this one fetch+JS-render an arbitrary URL and archive it to the user's own Stash server with zero user interaction. A tap is now the only thing standing between "another app sent an intent" and "real network work happens." Also switches to the shared saveArticleToStash helper while touching this same code path. --- .../terst/feedreader/ui/ShareReceiverActivity.kt | 66 +++++++++++++--------- 1 file changed, 39 insertions(+), 27 deletions(-) (limited to 'app/src/main') diff --git a/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt b/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt index 1a383ae..70d45f0 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt @@ -22,11 +22,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import org.terst.feedreader.data.AppDatabase -import org.terst.feedreader.data.Net -import org.terst.feedreader.data.Settings +import org.terst.feedreader.data.StashSaveOutcome import org.terst.feedreader.data.ensureStandaloneArticle -import org.terst.feedreader.stash.StashClient -import org.terst.feedreader.stash.StashSaveResult +import org.terst.feedreader.data.saveArticleToStash private val URL_REGEX = Regex("""https?://\S+""") @@ -54,29 +52,29 @@ class ShareReceiverActivity : ComponentActivity() { @Composable private fun ShareReceiverContent(url: String?, onDone: () -> Unit) { val context = LocalContext.current - var status by remember { mutableStateOf(if (url == null) "No link found in shared content" else "Saving…") } + var status by remember { mutableStateOf(if (url == null) "No link found in shared content" else null) } var done by remember { mutableStateOf(url == null) } + // Requires an explicit tap before touching the network -- this activity is exported + // (any installed app can target it directly with an ACTION_SEND intent, not just via + // the system share sheet), and starting extraction/Stash-save immediately on launch + // meant ANY other app could silently make this one fetch+JS-render an arbitrary URL + // and archive it to the user's own Stash server with zero user interaction. A tap + // here is the only thing standing between "another app sent an intent" and "this app + // did real network work," so it can't be skipped just because url is non-null. + var confirmed by remember { mutableStateOf(false) } - LaunchedEffect(url) { - if (url == null) return@LaunchedEffect + LaunchedEffect(url, confirmed) { + if (url == null || !confirmed) return@LaunchedEffect + status = "Saving…" val db = AppDatabase.get(context) val entity = ensureStandaloneArticle(context, db, url) if (!entity.isSaved) db.articleDao().setSaved(entity.id, true) - val settings = Settings(context) - val client = StashClient(Net.client, settings.stashBaseUrlNow(), settings.stashApiKeyNow()) - status = when (val result = client.saveArticle(url, entity.title)) { - is StashSaveResult.Saved -> { - db.articleDao().setStashArtifactId(entity.id, result.artifact.id) - "Saved for offline reading and archived to Stash" - } - is StashSaveResult.AlreadySaved -> { - db.articleDao().setStashArtifactId(entity.id, result.existingId) - "Saved for offline reading (already in Stash)" - } - is StashSaveResult.Failed -> - "Saved for offline reading. Stash archive failed: ${result.message}" + status = when (val outcome = saveArticleToStash(context, db, entity.id, url, entity.title)) { + is StashSaveOutcome.Saved -> "Saved for offline reading and archived to Stash" + is StashSaveOutcome.AlreadySaved -> "Saved for offline reading (already in Stash)" + is StashSaveOutcome.Failed -> "Saved for offline reading. Stash archive failed: ${outcome.reason}" } done = true } @@ -86,13 +84,27 @@ private fun ShareReceiverContent(url: String?, onDone: () -> Unit) { Column(modifier = Modifier.padding(20.dp)) { Text("Feeds", style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(8.dp)) - Text(status) - if (!done) { - Spacer(modifier = Modifier.height(8.dp)) - CircularProgressIndicator() - } else { - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = onDone) { Text("Done") } + when { + url == null -> { + Text(status ?: "No link found in shared content") + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = onDone) { Text("Done") } + } + !confirmed -> { + Text(url) + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = { confirmed = true }) { Text("Save") } + } + else -> { + Text(status ?: "Saving…") + if (!done) { + Spacer(modifier = Modifier.height(8.dp)) + CircularProgressIndicator() + } else { + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = onDone) { Text("Done") } + } + } } } } -- cgit v1.2.3