fix auth flows
This commit is contained in:
@@ -110,6 +110,7 @@ AppLinkPolicySnapshot? appLinkPolicySnapshot(Ref ref) {
|
|||||||
key: _toNativeRule(value),
|
key: _toNativeRule(value),
|
||||||
},
|
},
|
||||||
marketplaceFallbackEnabled: settings.appLinkMarketplaceFallback,
|
marketplaceFallbackEnabled: settings.appLinkMarketplaceFallback,
|
||||||
|
authExceptionsEnabled: settings.appLinkAuthExceptionsEnabled,
|
||||||
protectGeneralContext: protection.protectGeneralContext,
|
protectGeneralContext: protection.protectGeneralContext,
|
||||||
protectedContextIds: protection.protectedContextIds.toList(),
|
protectedContextIds: protection.protectedContextIds.toList(),
|
||||||
strictContextIds: protection.strictContextIds.toList(),
|
strictContextIds: protection.strictContextIds.toList(),
|
||||||
|
|||||||
+1
-1
@@ -120,7 +120,7 @@ final class AppLinkPolicySnapshotProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$appLinkPolicySnapshotHash() =>
|
String _$appLinkPolicySnapshotHash() =>
|
||||||
r'6fe2dca118d7162561fc7f6280d1a0411d50972a';
|
r'4d456a3cbf091e95ac35d913e8fa20d5f25978ab';
|
||||||
|
|
||||||
/// Single serialised writer that mirrors the Dart-owned app-link policy to the
|
/// Single serialised writer that mirrors the Dart-owned app-link policy to the
|
||||||
/// native profile-scoped store (§2.8), the sole policy source consulted by the
|
/// native profile-scoped store (§2.8), the sole policy source consulted by the
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'797520166026d1f0272cf713aa1c25ecf129c236';
|
String _$tabRepositoryHash() => r'5824be32664bac24623cf8863f68b7afa254d025';
|
||||||
|
|
||||||
abstract class _$TabRepository extends $Notifier<void> {
|
abstract class _$TabRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -1397,8 +1397,8 @@ final class VisibleTabListItemsFamily extends $Family
|
|||||||
/// switcher lays them out — the unassigned bucket first, then containers by
|
/// switcher lays them out — the unassigned bucket first, then containers by
|
||||||
/// pinned/`order_key`. Stepping off the end of one container therefore
|
/// pinned/`order_key`. Stepping off the end of one container therefore
|
||||||
/// continues into the next, and selecting that tab moves the selected container
|
/// continues into the next, and selecting that tab moves the selected container
|
||||||
/// along with it. Containers without tabs are skipped so their tree query never
|
/// along with it. Named containers holding no tabs are skipped so their tree
|
||||||
/// runs.
|
/// query never runs.
|
||||||
///
|
///
|
||||||
/// "Previous" is a step towards the top of that order and "next" a step
|
/// "Previous" is a step towards the top of that order and "next" a step
|
||||||
/// towards its end, so direction follows `tabListDirection` (baked into the
|
/// towards its end, so direction follows `tabListDirection` (baked into the
|
||||||
@@ -1443,8 +1443,8 @@ final sequentialTabNavigationOrderProvider =
|
|||||||
/// switcher lays them out — the unassigned bucket first, then containers by
|
/// switcher lays them out — the unassigned bucket first, then containers by
|
||||||
/// pinned/`order_key`. Stepping off the end of one container therefore
|
/// pinned/`order_key`. Stepping off the end of one container therefore
|
||||||
/// continues into the next, and selecting that tab moves the selected container
|
/// continues into the next, and selecting that tab moves the selected container
|
||||||
/// along with it. Containers without tabs are skipped so their tree query never
|
/// along with it. Named containers holding no tabs are skipped so their tree
|
||||||
/// runs.
|
/// query never runs.
|
||||||
///
|
///
|
||||||
/// "Previous" is a step towards the top of that order and "next" a step
|
/// "Previous" is a step towards the top of that order and "next" a step
|
||||||
/// towards its end, so direction follows `tabListDirection` (baked into the
|
/// towards its end, so direction follows `tabListDirection` (baked into the
|
||||||
@@ -1493,8 +1493,8 @@ final class SequentialTabNavigationOrderProvider
|
|||||||
/// switcher lays them out — the unassigned bucket first, then containers by
|
/// switcher lays them out — the unassigned bucket first, then containers by
|
||||||
/// pinned/`order_key`. Stepping off the end of one container therefore
|
/// pinned/`order_key`. Stepping off the end of one container therefore
|
||||||
/// continues into the next, and selecting that tab moves the selected container
|
/// continues into the next, and selecting that tab moves the selected container
|
||||||
/// along with it. Containers without tabs are skipped so their tree query never
|
/// along with it. Named containers holding no tabs are skipped so their tree
|
||||||
/// runs.
|
/// query never runs.
|
||||||
///
|
///
|
||||||
/// "Previous" is a step towards the top of that order and "next" a step
|
/// "Previous" is a step towards the top of that order and "next" a step
|
||||||
/// towards its end, so direction follows `tabListDirection` (baked into the
|
/// towards its end, so direction follows `tabListDirection` (baked into the
|
||||||
|
|||||||
@@ -817,6 +817,11 @@ class _AppLinksModeSection extends HookConsumerWidget {
|
|||||||
(s) => s.appLinkMarketplaceFallback,
|
(s) => s.appLinkMarketplaceFallback,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final authExceptionsEnabled = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select(
|
||||||
|
(s) => s.appLinkAuthExceptionsEnabled,
|
||||||
|
),
|
||||||
|
);
|
||||||
final rules = ref.watch(
|
final rules = ref.watch(
|
||||||
generalSettingsWithDefaultsProvider.select((s) => s.appLinkRules),
|
generalSettingsWithDefaultsProvider.select((s) => s.appLinkRules),
|
||||||
);
|
);
|
||||||
@@ -887,6 +892,23 @@ class _AppLinksModeSection extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Allow login app callbacks'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Let apps that opened a Custom Tab receive their login callback, '
|
||||||
|
'even when links are set to never open in apps',
|
||||||
|
),
|
||||||
|
value: authExceptionsEnabled,
|
||||||
|
onChanged: (value) async {
|
||||||
|
await ref
|
||||||
|
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||||
|
.save(
|
||||||
|
(current) =>
|
||||||
|
current.copyWith.appLinkAuthExceptionsEnabled(value),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
_AppLinkRulesSubsection(rules: rules),
|
_AppLinkRulesSubsection(rules: rules),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -252,6 +252,12 @@ class GeneralSettings with FastEquatable {
|
|||||||
/// Defaults to false — the wrong default for a de-Googled browser.
|
/// Defaults to false — the wrong default for a de-Googled browser.
|
||||||
final bool appLinkMarketplaceFallback;
|
final bool appLinkMarketplaceFallback;
|
||||||
|
|
||||||
|
/// Whether app-link "never" rules allow a same-caller Custom Tab / ActionView
|
||||||
|
/// login callback to return to the app that opened the browser. Defaults to
|
||||||
|
/// true to keep OAuth-style sign-in flows working while normal app links still
|
||||||
|
/// obey [appLinksMode].
|
||||||
|
final bool appLinkAuthExceptionsEnabled;
|
||||||
|
|
||||||
/// Whether the local search index (`history` table populated via tab→
|
/// Whether the local search index (`history` table populated via tab→
|
||||||
/// history triggers) is active. When false, the SQL trigger guard returns
|
/// history triggers) is active. When false, the SQL trigger guard returns
|
||||||
/// without writing; existing rows stay until the user clears them.
|
/// without writing; existing rows stay until the user clears them.
|
||||||
@@ -359,6 +365,7 @@ class GeneralSettings with FastEquatable {
|
|||||||
required this.appLinkRules,
|
required this.appLinkRules,
|
||||||
required this.appLinkContextOverrides,
|
required this.appLinkContextOverrides,
|
||||||
required this.appLinkMarketplaceFallback,
|
required this.appLinkMarketplaceFallback,
|
||||||
|
required this.appLinkAuthExceptionsEnabled,
|
||||||
required this.enableLocalSearchIndex,
|
required this.enableLocalSearchIndex,
|
||||||
required this.indexPrivateTabs,
|
required this.indexPrivateTabs,
|
||||||
required this.acceptSuggestionOnSubmit,
|
required this.acceptSuggestionOnSubmit,
|
||||||
@@ -436,6 +443,7 @@ class GeneralSettings with FastEquatable {
|
|||||||
Map<String, PersistedAppLinkRule>? appLinkRules,
|
Map<String, PersistedAppLinkRule>? appLinkRules,
|
||||||
Map<String, ContextAppLinkPolicy>? appLinkContextOverrides,
|
Map<String, ContextAppLinkPolicy>? appLinkContextOverrides,
|
||||||
bool? appLinkMarketplaceFallback,
|
bool? appLinkMarketplaceFallback,
|
||||||
|
bool? appLinkAuthExceptionsEnabled,
|
||||||
bool? enableLocalSearchIndex,
|
bool? enableLocalSearchIndex,
|
||||||
bool? indexPrivateTabs,
|
bool? indexPrivateTabs,
|
||||||
bool? acceptSuggestionOnSubmit,
|
bool? acceptSuggestionOnSubmit,
|
||||||
@@ -524,6 +532,7 @@ class GeneralSettings with FastEquatable {
|
|||||||
appLinkRules = appLinkRules ?? const {},
|
appLinkRules = appLinkRules ?? const {},
|
||||||
appLinkContextOverrides = appLinkContextOverrides ?? const {},
|
appLinkContextOverrides = appLinkContextOverrides ?? const {},
|
||||||
appLinkMarketplaceFallback = appLinkMarketplaceFallback ?? false,
|
appLinkMarketplaceFallback = appLinkMarketplaceFallback ?? false,
|
||||||
|
appLinkAuthExceptionsEnabled = appLinkAuthExceptionsEnabled ?? true,
|
||||||
enableLocalSearchIndex = enableLocalSearchIndex ?? true,
|
enableLocalSearchIndex = enableLocalSearchIndex ?? true,
|
||||||
indexPrivateTabs = indexPrivateTabs ?? false,
|
indexPrivateTabs = indexPrivateTabs ?? false,
|
||||||
acceptSuggestionOnSubmit = acceptSuggestionOnSubmit ?? true,
|
acceptSuggestionOnSubmit = acceptSuggestionOnSubmit ?? true,
|
||||||
@@ -693,6 +702,7 @@ class GeneralSettings with FastEquatable {
|
|||||||
appLinkRules,
|
appLinkRules,
|
||||||
appLinkContextOverrides,
|
appLinkContextOverrides,
|
||||||
appLinkMarketplaceFallback,
|
appLinkMarketplaceFallback,
|
||||||
|
appLinkAuthExceptionsEnabled,
|
||||||
enableLocalSearchIndex,
|
enableLocalSearchIndex,
|
||||||
indexPrivateTabs,
|
indexPrivateTabs,
|
||||||
acceptSuggestionOnSubmit,
|
acceptSuggestionOnSubmit,
|
||||||
|
|||||||
@@ -161,6 +161,10 @@ abstract class _$GeneralSettingsCWProxy {
|
|||||||
|
|
||||||
GeneralSettings appLinkMarketplaceFallback(bool appLinkMarketplaceFallback);
|
GeneralSettings appLinkMarketplaceFallback(bool appLinkMarketplaceFallback);
|
||||||
|
|
||||||
|
GeneralSettings appLinkAuthExceptionsEnabled(
|
||||||
|
bool appLinkAuthExceptionsEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
GeneralSettings enableLocalSearchIndex(bool enableLocalSearchIndex);
|
GeneralSettings enableLocalSearchIndex(bool enableLocalSearchIndex);
|
||||||
|
|
||||||
GeneralSettings indexPrivateTabs(bool indexPrivateTabs);
|
GeneralSettings indexPrivateTabs(bool indexPrivateTabs);
|
||||||
@@ -253,6 +257,7 @@ abstract class _$GeneralSettingsCWProxy {
|
|||||||
Map<String, PersistedAppLinkRule> appLinkRules,
|
Map<String, PersistedAppLinkRule> appLinkRules,
|
||||||
Map<String, ContextAppLinkPolicy> appLinkContextOverrides,
|
Map<String, ContextAppLinkPolicy> appLinkContextOverrides,
|
||||||
bool appLinkMarketplaceFallback,
|
bool appLinkMarketplaceFallback,
|
||||||
|
bool appLinkAuthExceptionsEnabled,
|
||||||
bool enableLocalSearchIndex,
|
bool enableLocalSearchIndex,
|
||||||
bool indexPrivateTabs,
|
bool indexPrivateTabs,
|
||||||
bool acceptSuggestionOnSubmit,
|
bool acceptSuggestionOnSubmit,
|
||||||
@@ -556,6 +561,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
GeneralSettings appLinkMarketplaceFallback(bool appLinkMarketplaceFallback) =>
|
GeneralSettings appLinkMarketplaceFallback(bool appLinkMarketplaceFallback) =>
|
||||||
call(appLinkMarketplaceFallback: appLinkMarketplaceFallback);
|
call(appLinkMarketplaceFallback: appLinkMarketplaceFallback);
|
||||||
|
|
||||||
|
@override
|
||||||
|
GeneralSettings appLinkAuthExceptionsEnabled(
|
||||||
|
bool appLinkAuthExceptionsEnabled,
|
||||||
|
) => call(appLinkAuthExceptionsEnabled: appLinkAuthExceptionsEnabled);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GeneralSettings enableLocalSearchIndex(bool enableLocalSearchIndex) =>
|
GeneralSettings enableLocalSearchIndex(bool enableLocalSearchIndex) =>
|
||||||
call(enableLocalSearchIndex: enableLocalSearchIndex);
|
call(enableLocalSearchIndex: enableLocalSearchIndex);
|
||||||
@@ -665,6 +675,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
Object? appLinkRules = const $CopyWithPlaceholder(),
|
Object? appLinkRules = const $CopyWithPlaceholder(),
|
||||||
Object? appLinkContextOverrides = const $CopyWithPlaceholder(),
|
Object? appLinkContextOverrides = const $CopyWithPlaceholder(),
|
||||||
Object? appLinkMarketplaceFallback = const $CopyWithPlaceholder(),
|
Object? appLinkMarketplaceFallback = const $CopyWithPlaceholder(),
|
||||||
|
Object? appLinkAuthExceptionsEnabled = const $CopyWithPlaceholder(),
|
||||||
Object? enableLocalSearchIndex = const $CopyWithPlaceholder(),
|
Object? enableLocalSearchIndex = const $CopyWithPlaceholder(),
|
||||||
Object? indexPrivateTabs = const $CopyWithPlaceholder(),
|
Object? indexPrivateTabs = const $CopyWithPlaceholder(),
|
||||||
Object? acceptSuggestionOnSubmit = const $CopyWithPlaceholder(),
|
Object? acceptSuggestionOnSubmit = const $CopyWithPlaceholder(),
|
||||||
@@ -1061,6 +1072,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
? _value.appLinkMarketplaceFallback
|
? _value.appLinkMarketplaceFallback
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: appLinkMarketplaceFallback as bool,
|
: appLinkMarketplaceFallback as bool,
|
||||||
|
appLinkAuthExceptionsEnabled:
|
||||||
|
appLinkAuthExceptionsEnabled == const $CopyWithPlaceholder() ||
|
||||||
|
appLinkAuthExceptionsEnabled == null
|
||||||
|
? _value.appLinkAuthExceptionsEnabled
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: appLinkAuthExceptionsEnabled as bool,
|
||||||
enableLocalSearchIndex:
|
enableLocalSearchIndex:
|
||||||
enableLocalSearchIndex == const $CopyWithPlaceholder() ||
|
enableLocalSearchIndex == const $CopyWithPlaceholder() ||
|
||||||
enableLocalSearchIndex == null
|
enableLocalSearchIndex == null
|
||||||
@@ -1257,6 +1274,7 @@ GeneralSettings _$GeneralSettingsFromJson(
|
|||||||
json['appLinkContextOverrides'] as Map<String, dynamic>?,
|
json['appLinkContextOverrides'] as Map<String, dynamic>?,
|
||||||
),
|
),
|
||||||
appLinkMarketplaceFallback: json['appLinkMarketplaceFallback'] as bool?,
|
appLinkMarketplaceFallback: json['appLinkMarketplaceFallback'] as bool?,
|
||||||
|
appLinkAuthExceptionsEnabled: json['appLinkAuthExceptionsEnabled'] as bool?,
|
||||||
enableLocalSearchIndex: json['enableLocalSearchIndex'] as bool?,
|
enableLocalSearchIndex: json['enableLocalSearchIndex'] as bool?,
|
||||||
indexPrivateTabs: json['indexPrivateTabs'] as bool?,
|
indexPrivateTabs: json['indexPrivateTabs'] as bool?,
|
||||||
acceptSuggestionOnSubmit: json['acceptSuggestionOnSubmit'] as bool?,
|
acceptSuggestionOnSubmit: json['acceptSuggestionOnSubmit'] as bool?,
|
||||||
@@ -1356,6 +1374,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
|
|||||||
(k, e) => MapEntry(k, e.toJson()),
|
(k, e) => MapEntry(k, e.toJson()),
|
||||||
),
|
),
|
||||||
'appLinkMarketplaceFallback': instance.appLinkMarketplaceFallback,
|
'appLinkMarketplaceFallback': instance.appLinkMarketplaceFallback,
|
||||||
|
'appLinkAuthExceptionsEnabled': instance.appLinkAuthExceptionsEnabled,
|
||||||
'enableLocalSearchIndex': instance.enableLocalSearchIndex,
|
'enableLocalSearchIndex': instance.enableLocalSearchIndex,
|
||||||
'indexPrivateTabs': instance.indexPrivateTabs,
|
'indexPrivateTabs': instance.indexPrivateTabs,
|
||||||
'acceptSuggestionOnSubmit': instance.acceptSuggestionOnSubmit,
|
'acceptSuggestionOnSubmit': instance.acceptSuggestionOnSubmit,
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ const generalSettingColumnTypes = <String, DriftSqlType>{
|
|||||||
'customTabsEnabled': DriftSqlType.bool,
|
'customTabsEnabled': DriftSqlType.bool,
|
||||||
'appLinksMode': DriftSqlType.string,
|
'appLinksMode': DriftSqlType.string,
|
||||||
'appLinkMarketplaceFallback': DriftSqlType.bool,
|
'appLinkMarketplaceFallback': DriftSqlType.bool,
|
||||||
|
'appLinkAuthExceptionsEnabled': DriftSqlType.bool,
|
||||||
'enableLocalSearchIndex': DriftSqlType.bool,
|
'enableLocalSearchIndex': DriftSqlType.bool,
|
||||||
'indexPrivateTabs': DriftSqlType.bool,
|
'indexPrivateTabs': DriftSqlType.bool,
|
||||||
'acceptSuggestionOnSubmit': DriftSqlType.bool,
|
'acceptSuggestionOnSubmit': DriftSqlType.bool,
|
||||||
|
|||||||
+4
@@ -19,6 +19,10 @@ class AuthIntentReceiverActivity : Activity() {
|
|||||||
|
|
||||||
val sourceIntent = intent?.let { Intent(it) } ?: Intent()
|
val sourceIntent = intent?.let { Intent(it) } ?: Intent()
|
||||||
|
|
||||||
|
// Stamp the caller before CustomTabIntentProcessor builds the session source, so the
|
||||||
|
// app-links authentication carve-out can recognise a sign-in callback for this tab.
|
||||||
|
addExternalCallerInformation(sourceIntent)
|
||||||
|
|
||||||
if (GlobalComponents.components == null && !GlobalComponents.ensureExternalComponents(applicationContext)) {
|
if (GlobalComponents.components == null && !GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.weblibre.flutter_mozilla_components.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY
|
||||||
|
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE
|
||||||
|
import mozilla.components.support.utils.ext.packageManagerCompatHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records which app sent [intent] so the session created from it carries a `caller`.
|
||||||
|
*
|
||||||
|
* AC's `CustomTabIntentProcessor` reads the caller through `SafeIntent.externalPackage()`, which
|
||||||
|
* only looks at the [EXTRA_ACTIVITY_REFERRER_PACKAGE] extra — nothing populates it for us, so a
|
||||||
|
* receiver has to stamp it before handing the intent to the processors or every custom tab ends up
|
||||||
|
* with `Source.External.CustomTab(null)`. Mirrors Fenix's `IntentReceiverActivity`
|
||||||
|
* `addReferrerInformation`.
|
||||||
|
*
|
||||||
|
* The app-links authentication carve-out
|
||||||
|
* ([eu.weblibre.flutter_mozilla_components.applinks.WebLibreAppLinksInterceptor]) is the consumer:
|
||||||
|
* it lets a sign-in callback return to the app that opened the tab.
|
||||||
|
*/
|
||||||
|
fun Activity.addExternalCallerInformation(intent: Intent) {
|
||||||
|
val caller = resolveExternalCallerPackage(intent) ?: return
|
||||||
|
intent.putExtra(EXTRA_ACTIVITY_REFERRER_PACKAGE, caller)
|
||||||
|
|
||||||
|
// ApplicationInfo.category is API 26+; this module builds against minSdk 24.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
try {
|
||||||
|
val category = packageManagerCompatHelper.getApplicationInfoCompat(caller, 0).category
|
||||||
|
intent.putExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, category)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
// The caller is not resolvable — the package id alone is enough for our purposes.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort identity of the app that sent [intent].
|
||||||
|
*
|
||||||
|
* [Activity.getCallingPackage] is supplied by the system and cannot be forged, so it wins when
|
||||||
|
* present (only set for `startActivityForResult` callers). The referrer chain below is
|
||||||
|
* caller-controlled and therefore spoofable — an app can claim to be another package. Consumers
|
||||||
|
* must not grant anything on it that the caller could not already do itself.
|
||||||
|
*/
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
fun Activity.resolveExternalCallerPackage(intent: Intent): String? {
|
||||||
|
callingPackage?.let { return it }
|
||||||
|
|
||||||
|
// Android can throw when the referrer carries data it cannot deserialise.
|
||||||
|
val activityReferrer = try {
|
||||||
|
referrer
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
activityReferrer?.let { uri ->
|
||||||
|
if (uri.scheme == ANDROID_APP_SCHEME) {
|
||||||
|
uri.host?.let { return it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val referrerUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_REFERRER)
|
||||||
|
if (referrerUri?.scheme == ANDROID_APP_SCHEME) {
|
||||||
|
referrerUri.host?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
intent.getStringExtra(Intent.EXTRA_REFERRER_NAME)?.let { name ->
|
||||||
|
Uri.parse(name).takeIf { it.scheme == ANDROID_APP_SCHEME }?.host?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val ANDROID_APP_SCHEME = "android-app"
|
||||||
+6
-21
@@ -93,7 +93,7 @@ class IntentReceiverActivity : Activity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val caller = resolveCallerPackage(intent) ?: return false
|
val caller = resolveExternalCallerPackage(intent) ?: return false
|
||||||
if (caller == packageName) return false
|
if (caller == packageName) return false
|
||||||
if (!IntentGatekeeperPreferences.isBlocked(applicationContext, caller)) return false
|
if (!IntentGatekeeperPreferences.isBlocked(applicationContext, caller)) return false
|
||||||
|
|
||||||
@@ -102,32 +102,17 @@ class IntentReceiverActivity : Activity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveCallerPackage(intent: Intent): String? {
|
|
||||||
referrer?.let { uri ->
|
|
||||||
if (uri.scheme == "android-app") {
|
|
||||||
uri.host?.let { return it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val referrerUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_REFERRER)
|
|
||||||
if (referrerUri?.scheme == "android-app") {
|
|
||||||
referrerUri.host?.let { return it }
|
|
||||||
}
|
|
||||||
|
|
||||||
intent.getStringExtra(Intent.EXTRA_REFERRER_NAME)?.let { name ->
|
|
||||||
Uri.parse(name).takeIf { it.scheme == "android-app" }?.host?.let { return it }
|
|
||||||
}
|
|
||||||
|
|
||||||
return callingPackage
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processIntent(intent: Intent) {
|
private fun processIntent(intent: Intent) {
|
||||||
|
// Must run before any intent processor: CustomTabIntentProcessor reads the caller off the
|
||||||
|
// intent when it builds the session source, and the app-links authentication carve-out
|
||||||
|
// needs that caller to recognise a sign-in callback.
|
||||||
|
addExternalCallerInformation(intent)
|
||||||
|
|
||||||
if (GlobalComponents.components == null) {
|
if (GlobalComponents.components == null) {
|
||||||
if (GlobalComponents.ensureExternalComponents(applicationContext)) {
|
if (GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||||
routeIntent(intent)
|
routeIntent(intent)
|
||||||
|
|||||||
+2
@@ -57,6 +57,7 @@ data class AppLinkPolicy(
|
|||||||
val protectedContextIds: Set<String>,
|
val protectedContextIds: Set<String>,
|
||||||
val strictContextIds: Set<String>,
|
val strictContextIds: Set<String>,
|
||||||
val protectedTargetPatterns: List<ProtectedTargetPattern>,
|
val protectedTargetPatterns: List<ProtectedTargetPattern>,
|
||||||
|
val authExceptionsEnabled: Boolean,
|
||||||
/**
|
/**
|
||||||
* Per-container overrides keyed by contextId; only isolated containers appear. A navigation whose
|
* Per-container overrides keyed by contextId; only isolated containers appear. A navigation whose
|
||||||
* source contextId is a key uses the entry's mode + rules instead of the global ones (replace).
|
* source contextId is a key uses the entry's mode + rules instead of the global ones (replace).
|
||||||
@@ -72,6 +73,7 @@ data class AppLinkPolicy(
|
|||||||
protectedContextIds = emptySet(),
|
protectedContextIds = emptySet(),
|
||||||
strictContextIds = emptySet(),
|
strictContextIds = emptySet(),
|
||||||
protectedTargetPatterns = emptyList(),
|
protectedTargetPatterns = emptyList(),
|
||||||
|
authExceptionsEnabled = true,
|
||||||
contextOverrides = emptyMap(),
|
contextOverrides = emptyMap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -16,11 +16,15 @@ import mozilla.components.support.base.log.logger.Logger
|
|||||||
* behaviour so the app opens in its own recents entry.
|
* behaviour so the app opens in its own recents entry.
|
||||||
* - [AUTOMATIC]: global-`always` or a remembered `alwaysOpen` rule — `NEW_TASK`, subject to the
|
* - [AUTOMATIC]: global-`always` or a remembered `alwaysOpen` rule — `NEW_TASK`, subject to the
|
||||||
* 2 s same-package cooldown loop-breaker (§2.4).
|
* 2 s same-package cooldown loop-breaker (§2.4).
|
||||||
|
* - [AUTHENTICATION]: same-caller Custom Tab / ActionView callback — `NEW_TASK | CLEAR_TOP` so the
|
||||||
|
* originating app can resume its existing task. Not a user gesture, so it takes the same 2 s
|
||||||
|
* cooldown as [AUTOMATIC] (AC applies its loop-breaker to authentication flows too).
|
||||||
* - [MARKETPLACE]: install-app fallback — `NEW_TASK | CLEAR_TASK`.
|
* - [MARKETPLACE]: install-app fallback — `NEW_TASK | CLEAR_TASK`.
|
||||||
*/
|
*/
|
||||||
enum class AppLinkLaunchMode {
|
enum class AppLinkLaunchMode {
|
||||||
MANUAL,
|
MANUAL,
|
||||||
AUTOMATIC,
|
AUTOMATIC,
|
||||||
|
AUTHENTICATION,
|
||||||
MARKETPLACE,
|
MARKETPLACE,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,9 +38,9 @@ enum class AppLinkLaunchResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Launches external apps. Every launch re-resolves immediately first (no cache) and verifies the
|
* Launches external apps. Every launch re-resolves immediately first (no cache) and verifies the
|
||||||
* expected package before `startActivity` (§2.7). Automatic launches honour a 2 s same-package
|
* expected package before `startActivity` (§2.7). Automatic and authentication launches honour a 2 s
|
||||||
* cooldown to break app→browser→app ping-pong loops (§2.4); manual and prompt-resolved opens are
|
* same-package cooldown to break app→browser→app ping-pong loops (§2.4); manual and prompt-resolved
|
||||||
* user gestures that bypass the check but still record it.
|
* opens are user gestures that bypass the check but still record it.
|
||||||
*/
|
*/
|
||||||
class AppLinkLauncher(
|
class AppLinkLauncher(
|
||||||
private val resolver: ExternalAppResolver,
|
private val resolver: ExternalAppResolver,
|
||||||
@@ -81,7 +85,7 @@ class AppLinkLauncher(
|
|||||||
else -> resolved.packageName
|
else -> resolved.packageName
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode == AppLinkLaunchMode.AUTOMATIC) {
|
if (mode == AppLinkLaunchMode.AUTOMATIC || mode == AppLinkLaunchMode.AUTHENTICATION) {
|
||||||
val (lastPackage, lastTs) = lastLaunch
|
val (lastPackage, lastTs) = lastLaunch
|
||||||
if (lastPackage != null && lastPackage == targetPackage &&
|
if (lastPackage != null && lastPackage == targetPackage &&
|
||||||
clock.elapsedRealtime() < lastTs + cooldownMs
|
clock.elapsedRealtime() < lastTs + cooldownMs
|
||||||
@@ -117,6 +121,8 @@ class AppLinkLauncher(
|
|||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
AppLinkLaunchMode.AUTOMATIC ->
|
AppLinkLaunchMode.AUTOMATIC ->
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
AppLinkLaunchMode.AUTHENTICATION ->
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
AppLinkLaunchMode.MARKETPLACE ->
|
AppLinkLaunchMode.MARKETPLACE ->
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -28,6 +28,7 @@ fun AppLinkPolicySnapshot.toAppLinkPolicy(): AppLinkPolicy {
|
|||||||
port = pattern.port?.toInt(),
|
port = pattern.port?.toInt(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
authExceptionsEnabled = authExceptionsEnabled,
|
||||||
contextOverrides = contextOverrides.mapValues { (_, override) ->
|
contextOverrides = contextOverrides.mapValues { (_, override) ->
|
||||||
ContextAppLinkPolicy(
|
ContextAppLinkPolicy(
|
||||||
globalMode = override.mode.toAppLinkMode(),
|
globalMode = override.mode.toAppLinkMode(),
|
||||||
|
|||||||
+3
@@ -93,6 +93,7 @@ class AppLinkPolicyStore internal constructor(
|
|||||||
root.put(FIELD_MIGRATED, migrated)
|
root.put(FIELD_MIGRATED, migrated)
|
||||||
root.put(FIELD_GLOBAL_MODE, policy.globalMode.name)
|
root.put(FIELD_GLOBAL_MODE, policy.globalMode.name)
|
||||||
root.put(FIELD_MARKETPLACE, policy.marketplaceFallbackEnabled)
|
root.put(FIELD_MARKETPLACE, policy.marketplaceFallbackEnabled)
|
||||||
|
root.put(FIELD_AUTH_EXCEPTIONS, policy.authExceptionsEnabled)
|
||||||
root.put(FIELD_PROTECT_GENERAL, policy.protectGeneralContext)
|
root.put(FIELD_PROTECT_GENERAL, policy.protectGeneralContext)
|
||||||
root.put(FIELD_PROTECTED_CONTEXTS, JSONArray(policy.protectedContextIds.toList()))
|
root.put(FIELD_PROTECTED_CONTEXTS, JSONArray(policy.protectedContextIds.toList()))
|
||||||
root.put(FIELD_STRICT_CONTEXTS, JSONArray(policy.strictContextIds.toList()))
|
root.put(FIELD_STRICT_CONTEXTS, JSONArray(policy.strictContextIds.toList()))
|
||||||
@@ -177,6 +178,7 @@ class AppLinkPolicyStore internal constructor(
|
|||||||
globalMode = AppLinkMode.valueOf(root.getString(FIELD_GLOBAL_MODE)),
|
globalMode = AppLinkMode.valueOf(root.getString(FIELD_GLOBAL_MODE)),
|
||||||
rules = rules,
|
rules = rules,
|
||||||
marketplaceFallbackEnabled = root.optBoolean(FIELD_MARKETPLACE, false),
|
marketplaceFallbackEnabled = root.optBoolean(FIELD_MARKETPLACE, false),
|
||||||
|
authExceptionsEnabled = root.optBoolean(FIELD_AUTH_EXCEPTIONS, true),
|
||||||
protectGeneralContext = root.optBoolean(FIELD_PROTECT_GENERAL, false),
|
protectGeneralContext = root.optBoolean(FIELD_PROTECT_GENERAL, false),
|
||||||
protectedContextIds = root.optJSONArray(FIELD_PROTECTED_CONTEXTS).toStringSet(),
|
protectedContextIds = root.optJSONArray(FIELD_PROTECTED_CONTEXTS).toStringSet(),
|
||||||
strictContextIds = root.optJSONArray(FIELD_STRICT_CONTEXTS).toStringSet(),
|
strictContextIds = root.optJSONArray(FIELD_STRICT_CONTEXTS).toStringSet(),
|
||||||
@@ -218,6 +220,7 @@ class AppLinkPolicyStore internal constructor(
|
|||||||
private const val FIELD_MIGRATED = "migrated"
|
private const val FIELD_MIGRATED = "migrated"
|
||||||
private const val FIELD_GLOBAL_MODE = "globalMode"
|
private const val FIELD_GLOBAL_MODE = "globalMode"
|
||||||
private const val FIELD_MARKETPLACE = "marketplaceFallbackEnabled"
|
private const val FIELD_MARKETPLACE = "marketplaceFallbackEnabled"
|
||||||
|
private const val FIELD_AUTH_EXCEPTIONS = "authExceptionsEnabled"
|
||||||
private const val FIELD_PROTECT_GENERAL = "protectGeneralContext"
|
private const val FIELD_PROTECT_GENERAL = "protectGeneralContext"
|
||||||
private const val FIELD_PROTECTED_CONTEXTS = "protectedContextIds"
|
private const val FIELD_PROTECTED_CONTEXTS = "protectedContextIds"
|
||||||
private const val FIELD_STRICT_CONTEXTS = "strictContextIds"
|
private const val FIELD_STRICT_CONTEXTS = "strictContextIds"
|
||||||
|
|||||||
+95
-11
@@ -58,9 +58,27 @@ class WebLibreAppLinksInterceptor(
|
|||||||
|
|
||||||
val uriScheme = runCatching { uri.toUri().scheme }.getOrNull()
|
val uriScheme = runCatching { uri.toUri().scheme }.getOrNull()
|
||||||
val engineSupportsScheme = AppLinkSchemes.isEngineSupported(uriScheme)
|
val engineSupportsScheme = AppLinkSchemes.isEngineSupported(uriScheme)
|
||||||
|
val session = components.core.store.state.findTabOrCustomTab(engineSession)
|
||||||
|
val policy = AppLinkPolicyStores.forProfile(components.profileApplicationContext).policy
|
||||||
|
|
||||||
|
// A tab an external app opened for us (Custom Tab / ActionView) may be hosting a sign-in
|
||||||
|
// round trip. Gated on the policy so turning the carve-out off restores the plain §2.4
|
||||||
|
// eligibility rules rather than only skipping the launch below.
|
||||||
|
val authExceptionsAllowed = policy.authExceptionsEnabled && isPossibleAuthentication(session)
|
||||||
|
|
||||||
// Step 2 — navigation eligibility. Any hit lets the engine proceed normally.
|
// Step 2 — navigation eligibility. Any hit lets the engine proceed normally.
|
||||||
if (!isEligible(uri, lastUri, uriScheme, engineSupportsScheme, hasUserGesture, isRedirect, isDirectNavigation, isSubframeRequest)) {
|
if (!isEligible(
|
||||||
|
uri,
|
||||||
|
lastUri,
|
||||||
|
uriScheme,
|
||||||
|
engineSupportsScheme,
|
||||||
|
hasUserGesture,
|
||||||
|
isRedirect,
|
||||||
|
isDirectNavigation,
|
||||||
|
isSubframeRequest,
|
||||||
|
authExceptionsAllowed,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,10 +92,6 @@ class WebLibreAppLinksInterceptor(
|
|||||||
|
|
||||||
val resolved = runtime.resolver.resolve(uri, includeHttpAppLinks = true, useCache = true)
|
val resolved = runtime.resolver.resolve(uri, includeHttpAppLinks = true, useCache = true)
|
||||||
|
|
||||||
val policy = AppLinkPolicyStores.forProfile(components.profileApplicationContext).policy
|
|
||||||
|
|
||||||
val session = components.core.store.state.findTabOrCustomTab(engineSession)
|
|
||||||
|
|
||||||
// Container isolation (replace semantics): a container with "isolated app link settings"
|
// Container isolation (replace semantics): a container with "isolated app link settings"
|
||||||
// enabled contributes an entry keyed by its contextId. When the source tab's contextId has
|
// enabled contributes an entry keyed by its contextId. When the source tab's contextId has
|
||||||
// one, its mode + rules fully replace the global ones for this navigation.
|
// one, its mode + rules fully replace the global ones for this navigation.
|
||||||
@@ -85,12 +99,47 @@ class WebLibreAppLinksInterceptor(
|
|||||||
val effectiveMode = override?.globalMode ?: policy.globalMode
|
val effectiveMode = override?.globalMode ?: policy.globalMode
|
||||||
val effectiveRules = override?.rules ?: policy.rules
|
val effectiveRules = override?.rules ?: policy.rules
|
||||||
|
|
||||||
|
val isProtectedNavigation = isProtected(policy, session, uri)
|
||||||
|
val isPrivateNavigation = session?.content?.private ?: false
|
||||||
|
val isWalletNavigation = AppLinkSchemes.isWallet(resolved.originalScheme) ||
|
||||||
|
AppLinkSchemes.isWallet(resolved.intentDataScheme)
|
||||||
|
|
||||||
|
// An ambiguous resolution is never treated as a callback: with several handlers we cannot
|
||||||
|
// show the caller *is* the target, and the launch would raise a chooser rather than return
|
||||||
|
// to the app. AC declines here too — its package comes from the bound component, which is
|
||||||
|
// only set for an unambiguous handler.
|
||||||
|
val authTargetPackage = if (resolved.isAmbiguous) null else resolved.packageName
|
||||||
|
|
||||||
|
// §2.4 authentication carve-out (AC parity): a tab opened *by* the app the navigation
|
||||||
|
// targets is a sign-in round trip rather than a general app link, so it returns to its
|
||||||
|
// caller even under `never`. The forced-prompt contexts still win — a protected container,
|
||||||
|
// a private tab or a wallet scheme must not leak out silently, so those fall through to the
|
||||||
|
// classifier, which prompts for them regardless of mode (§2.4 step 4).
|
||||||
|
if (authExceptionsAllowed &&
|
||||||
|
isAuthenticationCallback(session, authTargetPackage) &&
|
||||||
|
!isProtectedNavigation && !isPrivateNavigation && !isWalletNavigation
|
||||||
|
) {
|
||||||
|
val result = runtime.launcher.launch(
|
||||||
|
uri,
|
||||||
|
AppLinkLaunchMode.AUTHENTICATION,
|
||||||
|
expectedPackage = authTargetPackage,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"auth app-link callback uri=$uri tab=${session?.id} " +
|
||||||
|
"caller=${callerPackage(session)} package=$authTargetPackage -> $result",
|
||||||
|
)
|
||||||
|
return if (result == AppLinkLaunchResult.LAUNCHED) {
|
||||||
|
RequestInterceptor.InterceptionResponse.Deny
|
||||||
|
} else {
|
||||||
|
safeNonLaunchResponse(pendingStore, resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val input = ClassifierInput(
|
val input = ClassifierInput(
|
||||||
resolved = resolved,
|
resolved = resolved,
|
||||||
isProtected = isProtected(policy, session, uri),
|
isProtected = isProtectedNavigation,
|
||||||
isPrivate = session?.content?.private ?: false,
|
isPrivate = isPrivateNavigation,
|
||||||
isWallet = AppLinkSchemes.isWallet(resolved.originalScheme) ||
|
isWallet = isWalletNavigation,
|
||||||
AppLinkSchemes.isWallet(resolved.intentDataScheme),
|
|
||||||
missingSession = session == null,
|
missingSession = session == null,
|
||||||
suppressionHit = session != null &&
|
suppressionHit = session != null &&
|
||||||
pendingStore.isSuppressed(session.id, targetFingerprint(uri, resolved)),
|
pendingStore.isSuppressed(session.id, targetFingerprint(uri, resolved)),
|
||||||
@@ -262,6 +311,38 @@ class WebLibreAppLinksInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when [targetPackage] is the very app that opened this tab — the shape of a sign-in
|
||||||
|
* callback. [targetPackage] must be an unambiguous resolution; pass `null` otherwise.
|
||||||
|
*/
|
||||||
|
private fun isAuthenticationCallback(session: SessionState?, targetPackage: String?): Boolean {
|
||||||
|
if (targetPackage.isNullOrEmpty()) return false
|
||||||
|
return callerPackage(session) == targetPackage
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The package that launched this session, as recorded by
|
||||||
|
* [eu.weblibre.flutter_mozilla_components.activities.addExternalCallerInformation]. Note the
|
||||||
|
* underlying referrer is caller-supplied and can be spoofed, so this may only gate actions the
|
||||||
|
* caller could already perform itself (here: launching its own intent).
|
||||||
|
*/
|
||||||
|
private fun callerPackage(session: SessionState?): String? {
|
||||||
|
return when (val source = session?.source) {
|
||||||
|
is SessionState.Source.External.CustomTab -> source.caller?.packageId
|
||||||
|
is SessionState.Source.External.ActionView -> source.caller?.packageId
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isPossibleAuthentication(session: SessionState?): Boolean {
|
||||||
|
return when (session?.source) {
|
||||||
|
is SessionState.Source.External.CustomTab,
|
||||||
|
is SessionState.Source.External.ActionView,
|
||||||
|
-> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Eligibility (§2.4 step 2) ----
|
// ---- Eligibility (§2.4 step 2) ----
|
||||||
|
|
||||||
private fun isEligible(
|
private fun isEligible(
|
||||||
@@ -273,6 +354,7 @@ class WebLibreAppLinksInterceptor(
|
|||||||
isRedirect: Boolean,
|
isRedirect: Boolean,
|
||||||
isDirectNavigation: Boolean,
|
isDirectNavigation: Boolean,
|
||||||
isSubframeRequest: Boolean,
|
isSubframeRequest: Boolean,
|
||||||
|
authExceptionsAllowed: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (uriScheme == null) return false
|
if (uriScheme == null) return false
|
||||||
// A subframe request not triggered by the user and outside the allowlist stays in-page.
|
// A subframe request not triggered by the user and outside the allowlist stays in-page.
|
||||||
@@ -282,8 +364,10 @@ class WebLibreAppLinksInterceptor(
|
|||||||
val isIntentionalNavigation = hasUserGesture || isAllowedRedirect || isDirectNavigation
|
val isIntentionalNavigation = hasUserGesture || isAllowedRedirect || isDirectNavigation
|
||||||
// Unintentional engine-supported navigation continues in the browser.
|
// Unintentional engine-supported navigation continues in the browser.
|
||||||
if (engineSupportsScheme && !isIntentionalNavigation) return false
|
if (engineSupportsScheme && !isIntentionalNavigation) return false
|
||||||
// Same-domain engine-supported navigation continues in the browser (AC subdomain stripping).
|
// Same-domain engine-supported navigation continues in the browser (AC subdomain stripping),
|
||||||
if (engineSupportsScheme && isSameDomain(lastUri, uri)) return false
|
// unless this tab could be hosting an authentication round trip whose callback is an http
|
||||||
|
// app link on the same site.
|
||||||
|
if (engineSupportsScheme && isSameDomain(lastUri, uri) && !authExceptionsAllowed) return false
|
||||||
// Always-denied schemes never resolve or launch externally.
|
// Always-denied schemes never resolve or launch externally.
|
||||||
if (AppLinkSchemes.isAlwaysDenied(uriScheme)) return false
|
if (AppLinkSchemes.isAlwaysDenied(uriScheme)) return false
|
||||||
return true
|
return true
|
||||||
|
|||||||
+16
-8
@@ -5830,6 +5830,11 @@ data class AppLinkPolicySnapshot (
|
|||||||
/** Remembered rules keyed by canonical scope. */
|
/** Remembered rules keyed by canonical scope. */
|
||||||
val rules: Map<String, NativeAppLinkRule>,
|
val rules: Map<String, NativeAppLinkRule>,
|
||||||
val marketplaceFallbackEnabled: Boolean,
|
val marketplaceFallbackEnabled: Boolean,
|
||||||
|
/**
|
||||||
|
* Allows same-caller Custom Tab / ActionView authentication callbacks to
|
||||||
|
* return to their app even when the general app-link mode is `never`.
|
||||||
|
*/
|
||||||
|
val authExceptionsEnabled: Boolean,
|
||||||
/** Regular / no-contextId tabs are proxied via the `general` scope. */
|
/** Regular / no-contextId tabs are proxied via the `general` scope. */
|
||||||
val protectGeneralContext: Boolean,
|
val protectGeneralContext: Boolean,
|
||||||
/** contextIds that resolve to a proxy after inherit/bypass/alias. */
|
/** contextIds that resolve to a proxy after inherit/bypass/alias. */
|
||||||
@@ -5850,12 +5855,13 @@ data class AppLinkPolicySnapshot (
|
|||||||
val globalMode = pigeonVar_list[0] as AppLinksMode
|
val globalMode = pigeonVar_list[0] as AppLinksMode
|
||||||
val rules = pigeonVar_list[1] as Map<String, NativeAppLinkRule>
|
val rules = pigeonVar_list[1] as Map<String, NativeAppLinkRule>
|
||||||
val marketplaceFallbackEnabled = pigeonVar_list[2] as Boolean
|
val marketplaceFallbackEnabled = pigeonVar_list[2] as Boolean
|
||||||
val protectGeneralContext = pigeonVar_list[3] as Boolean
|
val authExceptionsEnabled = pigeonVar_list[3] as Boolean
|
||||||
val protectedContextIds = pigeonVar_list[4] as List<String>
|
val protectGeneralContext = pigeonVar_list[4] as Boolean
|
||||||
val strictContextIds = pigeonVar_list[5] as List<String>
|
val protectedContextIds = pigeonVar_list[5] as List<String>
|
||||||
val protectedTargetPatterns = pigeonVar_list[6] as List<ProtectedTargetPattern>
|
val strictContextIds = pigeonVar_list[6] as List<String>
|
||||||
val contextOverrides = pigeonVar_list[7] as Map<String, NativeContextAppLinkPolicy>
|
val protectedTargetPatterns = pigeonVar_list[7] as List<ProtectedTargetPattern>
|
||||||
return AppLinkPolicySnapshot(globalMode, rules, marketplaceFallbackEnabled, protectGeneralContext, protectedContextIds, strictContextIds, protectedTargetPatterns, contextOverrides)
|
val contextOverrides = pigeonVar_list[8] as Map<String, NativeContextAppLinkPolicy>
|
||||||
|
return AppLinkPolicySnapshot(globalMode, rules, marketplaceFallbackEnabled, authExceptionsEnabled, protectGeneralContext, protectedContextIds, strictContextIds, protectedTargetPatterns, contextOverrides)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@@ -5863,6 +5869,7 @@ data class AppLinkPolicySnapshot (
|
|||||||
globalMode,
|
globalMode,
|
||||||
rules,
|
rules,
|
||||||
marketplaceFallbackEnabled,
|
marketplaceFallbackEnabled,
|
||||||
|
authExceptionsEnabled,
|
||||||
protectGeneralContext,
|
protectGeneralContext,
|
||||||
protectedContextIds,
|
protectedContextIds,
|
||||||
strictContextIds,
|
strictContextIds,
|
||||||
@@ -5878,7 +5885,7 @@ data class AppLinkPolicySnapshot (
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
val other = other as AppLinkPolicySnapshot
|
val other = other as AppLinkPolicySnapshot
|
||||||
return GeckoPigeonUtils.deepEquals(this.globalMode, other.globalMode) && GeckoPigeonUtils.deepEquals(this.rules, other.rules) && GeckoPigeonUtils.deepEquals(this.marketplaceFallbackEnabled, other.marketplaceFallbackEnabled) && GeckoPigeonUtils.deepEquals(this.protectGeneralContext, other.protectGeneralContext) && GeckoPigeonUtils.deepEquals(this.protectedContextIds, other.protectedContextIds) && GeckoPigeonUtils.deepEquals(this.strictContextIds, other.strictContextIds) && GeckoPigeonUtils.deepEquals(this.protectedTargetPatterns, other.protectedTargetPatterns) && GeckoPigeonUtils.deepEquals(this.contextOverrides, other.contextOverrides)
|
return GeckoPigeonUtils.deepEquals(this.globalMode, other.globalMode) && GeckoPigeonUtils.deepEquals(this.rules, other.rules) && GeckoPigeonUtils.deepEquals(this.marketplaceFallbackEnabled, other.marketplaceFallbackEnabled) && GeckoPigeonUtils.deepEquals(this.authExceptionsEnabled, other.authExceptionsEnabled) && GeckoPigeonUtils.deepEquals(this.protectGeneralContext, other.protectGeneralContext) && GeckoPigeonUtils.deepEquals(this.protectedContextIds, other.protectedContextIds) && GeckoPigeonUtils.deepEquals(this.strictContextIds, other.strictContextIds) && GeckoPigeonUtils.deepEquals(this.protectedTargetPatterns, other.protectedTargetPatterns) && GeckoPigeonUtils.deepEquals(this.contextOverrides, other.contextOverrides)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -5886,6 +5893,7 @@ data class AppLinkPolicySnapshot (
|
|||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.globalMode)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.globalMode)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.rules)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.rules)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.marketplaceFallbackEnabled)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.marketplaceFallbackEnabled)
|
||||||
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.authExceptionsEnabled)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.protectGeneralContext)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.protectGeneralContext)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.protectedContextIds)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.protectedContextIds)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.strictContextIds)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.strictContextIds)
|
||||||
@@ -5894,7 +5902,7 @@ data class AppLinkPolicySnapshot (
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "AppLinkPolicySnapshot(globalMode=$globalMode, rules=$rules, marketplaceFallbackEnabled=$marketplaceFallbackEnabled, protectGeneralContext=$protectGeneralContext, protectedContextIds=$protectedContextIds, strictContextIds=$strictContextIds, protectedTargetPatterns=$protectedTargetPatterns, contextOverrides=$contextOverrides)"
|
return "AppLinkPolicySnapshot(globalMode=$globalMode, rules=$rules, marketplaceFallbackEnabled=$marketplaceFallbackEnabled, authExceptionsEnabled=$authExceptionsEnabled, protectGeneralContext=$protectGeneralContext, protectedContextIds=$protectedContextIds, strictContextIds=$strictContextIds, protectedTargetPatterns=$protectedTargetPatterns, contextOverrides=$contextOverrides)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
@@ -58,6 +58,11 @@ class AppLinkClassifierTest {
|
|||||||
|
|
||||||
// ---- §2.2 table: engine-supported (http) scheme, app resolves ----
|
// ---- §2.2 table: engine-supported (http) scheme, app resolves ----
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun safeDefaultAllowsAuthExceptions() {
|
||||||
|
assertEquals(true, AppLinkPolicy.SAFE_DEFAULT.authExceptionsEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun engineSupportedAlwaysAutoLaunches() {
|
fun engineSupportedAlwaysAutoLaunches() {
|
||||||
val d = AppLinkClassifier.classify(
|
val d = AppLinkClassifier.classify(
|
||||||
|
|||||||
+44
@@ -13,6 +13,7 @@ import kotlin.test.assertEquals
|
|||||||
import org.mockito.ArgumentMatchers.anyBoolean
|
import org.mockito.ArgumentMatchers.anyBoolean
|
||||||
import org.mockito.ArgumentMatchers.anyString
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
import org.mockito.Mockito.mock
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.Mockito.`when`
|
import org.mockito.Mockito.`when`
|
||||||
|
|
||||||
class AppLinkLauncherTest {
|
class AppLinkLauncherTest {
|
||||||
@@ -69,6 +70,33 @@ class AppLinkLauncherTest {
|
|||||||
assertEquals(1, started)
|
assertEquals(1, started)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun authenticationLaunchUsesClearTopFlags() {
|
||||||
|
val intent = mock(Intent::class.java)
|
||||||
|
val resolved = ResolvedAppLink(
|
||||||
|
hasExternalApp = true,
|
||||||
|
appIntent = intent,
|
||||||
|
packageName = "com.example.app",
|
||||||
|
appName = "App",
|
||||||
|
fallbackUrl = null,
|
||||||
|
marketplaceIntent = null,
|
||||||
|
isAmbiguous = false,
|
||||||
|
engineSupportsScheme = false,
|
||||||
|
scopeKey = "pkg:com.example.app",
|
||||||
|
originalScheme = "example",
|
||||||
|
intentDataScheme = "example",
|
||||||
|
)
|
||||||
|
var startedIntent: Intent? = null
|
||||||
|
val l = launcher(resolved, FakeClock()) { startedIntent = it }
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
AppLinkLaunchResult.LAUNCHED,
|
||||||
|
l.launch("example://callback", AppLinkLaunchMode.AUTHENTICATION),
|
||||||
|
)
|
||||||
|
verify(intent).flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
assertEquals(intent, startedIntent)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun automaticLaunchWithinCooldownIsRefused() {
|
fun automaticLaunchWithinCooldownIsRefused() {
|
||||||
val clock = FakeClock(1000L)
|
val clock = FakeClock(1000L)
|
||||||
@@ -78,6 +106,22 @@ class AppLinkLauncherTest {
|
|||||||
assertEquals(AppLinkLaunchResult.COOLDOWN, l.launch("zoommtg://x", AppLinkLaunchMode.AUTOMATIC))
|
assertEquals(AppLinkLaunchResult.COOLDOWN, l.launch("zoommtg://x", AppLinkLaunchMode.AUTOMATIC))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun authenticationLaunchWithinCooldownIsRefused() {
|
||||||
|
val clock = FakeClock(1000L)
|
||||||
|
val l = launcher(resolvedFor("com.example.app"), clock)
|
||||||
|
assertEquals(
|
||||||
|
AppLinkLaunchResult.LAUNCHED,
|
||||||
|
l.launch("zoommtg://x", AppLinkLaunchMode.AUTHENTICATION),
|
||||||
|
)
|
||||||
|
// An app that re-opens its Custom Tab on receiving the callback would otherwise ping-pong.
|
||||||
|
clock.now = 1500L // < 2000 ms later
|
||||||
|
assertEquals(
|
||||||
|
AppLinkLaunchResult.COOLDOWN,
|
||||||
|
l.launch("zoommtg://x", AppLinkLaunchMode.AUTHENTICATION),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun automaticLaunchAfterCooldownSucceeds() {
|
fun automaticLaunchAfterCooldownSucceeds() {
|
||||||
val clock = FakeClock(1000L)
|
val clock = FakeClock(1000L)
|
||||||
|
|||||||
@@ -6486,6 +6486,7 @@ class AppLinkPolicySnapshot {
|
|||||||
required this.globalMode,
|
required this.globalMode,
|
||||||
required this.rules,
|
required this.rules,
|
||||||
required this.marketplaceFallbackEnabled,
|
required this.marketplaceFallbackEnabled,
|
||||||
|
required this.authExceptionsEnabled,
|
||||||
required this.protectGeneralContext,
|
required this.protectGeneralContext,
|
||||||
required this.protectedContextIds,
|
required this.protectedContextIds,
|
||||||
required this.strictContextIds,
|
required this.strictContextIds,
|
||||||
@@ -6500,6 +6501,10 @@ class AppLinkPolicySnapshot {
|
|||||||
|
|
||||||
bool marketplaceFallbackEnabled;
|
bool marketplaceFallbackEnabled;
|
||||||
|
|
||||||
|
/// Allows same-caller Custom Tab / ActionView authentication callbacks to
|
||||||
|
/// return to their app even when the general app-link mode is `never`.
|
||||||
|
bool authExceptionsEnabled;
|
||||||
|
|
||||||
/// Regular / no-contextId tabs are proxied via the `general` scope.
|
/// Regular / no-contextId tabs are proxied via the `general` scope.
|
||||||
bool protectGeneralContext;
|
bool protectGeneralContext;
|
||||||
|
|
||||||
@@ -6521,6 +6526,7 @@ class AppLinkPolicySnapshot {
|
|||||||
globalMode,
|
globalMode,
|
||||||
rules,
|
rules,
|
||||||
marketplaceFallbackEnabled,
|
marketplaceFallbackEnabled,
|
||||||
|
authExceptionsEnabled,
|
||||||
protectGeneralContext,
|
protectGeneralContext,
|
||||||
protectedContextIds,
|
protectedContextIds,
|
||||||
strictContextIds,
|
strictContextIds,
|
||||||
@@ -6540,12 +6546,13 @@ class AppLinkPolicySnapshot {
|
|||||||
rules: (result[1]! as Map<Object?, Object?>)
|
rules: (result[1]! as Map<Object?, Object?>)
|
||||||
.cast<String, NativeAppLinkRule>(),
|
.cast<String, NativeAppLinkRule>(),
|
||||||
marketplaceFallbackEnabled: result[2]! as bool,
|
marketplaceFallbackEnabled: result[2]! as bool,
|
||||||
protectGeneralContext: result[3]! as bool,
|
authExceptionsEnabled: result[3]! as bool,
|
||||||
protectedContextIds: (result[4]! as List<Object?>).cast<String>(),
|
protectGeneralContext: result[4]! as bool,
|
||||||
strictContextIds: (result[5]! as List<Object?>).cast<String>(),
|
protectedContextIds: (result[5]! as List<Object?>).cast<String>(),
|
||||||
protectedTargetPatterns: (result[6]! as List<Object?>)
|
strictContextIds: (result[6]! as List<Object?>).cast<String>(),
|
||||||
|
protectedTargetPatterns: (result[7]! as List<Object?>)
|
||||||
.cast<ProtectedTargetPattern>(),
|
.cast<ProtectedTargetPattern>(),
|
||||||
contextOverrides: (result[7]! as Map<Object?, Object?>)
|
contextOverrides: (result[8]! as Map<Object?, Object?>)
|
||||||
.cast<String, NativeContextAppLinkPolicy>(),
|
.cast<String, NativeContextAppLinkPolicy>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6565,6 +6572,7 @@ class AppLinkPolicySnapshot {
|
|||||||
marketplaceFallbackEnabled,
|
marketplaceFallbackEnabled,
|
||||||
other.marketplaceFallbackEnabled,
|
other.marketplaceFallbackEnabled,
|
||||||
) &&
|
) &&
|
||||||
|
_deepEquals(authExceptionsEnabled, other.authExceptionsEnabled) &&
|
||||||
_deepEquals(protectGeneralContext, other.protectGeneralContext) &&
|
_deepEquals(protectGeneralContext, other.protectGeneralContext) &&
|
||||||
_deepEquals(protectedContextIds, other.protectedContextIds) &&
|
_deepEquals(protectedContextIds, other.protectedContextIds) &&
|
||||||
_deepEquals(strictContextIds, other.strictContextIds) &&
|
_deepEquals(strictContextIds, other.strictContextIds) &&
|
||||||
@@ -6578,7 +6586,7 @@ class AppLinkPolicySnapshot {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AppLinkPolicySnapshot(globalMode: $globalMode, rules: $rules, marketplaceFallbackEnabled: $marketplaceFallbackEnabled, protectGeneralContext: $protectGeneralContext, protectedContextIds: $protectedContextIds, strictContextIds: $strictContextIds, protectedTargetPatterns: $protectedTargetPatterns, contextOverrides: $contextOverrides)';
|
return 'AppLinkPolicySnapshot(globalMode: $globalMode, rules: $rules, marketplaceFallbackEnabled: $marketplaceFallbackEnabled, authExceptionsEnabled: $authExceptionsEnabled, protectGeneralContext: $protectGeneralContext, protectedContextIds: $protectedContextIds, strictContextIds: $strictContextIds, protectedTargetPatterns: $protectedTargetPatterns, contextOverrides: $contextOverrides)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2994,6 +2994,10 @@ class AppLinkPolicySnapshot {
|
|||||||
|
|
||||||
final bool marketplaceFallbackEnabled;
|
final bool marketplaceFallbackEnabled;
|
||||||
|
|
||||||
|
/// Allows same-caller Custom Tab / ActionView authentication callbacks to
|
||||||
|
/// return to their app even when the general app-link mode is `never`.
|
||||||
|
final bool authExceptionsEnabled;
|
||||||
|
|
||||||
/// Regular / no-contextId tabs are proxied via the `general` scope.
|
/// Regular / no-contextId tabs are proxied via the `general` scope.
|
||||||
final bool protectGeneralContext;
|
final bool protectGeneralContext;
|
||||||
|
|
||||||
@@ -3014,6 +3018,7 @@ class AppLinkPolicySnapshot {
|
|||||||
required this.globalMode,
|
required this.globalMode,
|
||||||
required this.rules,
|
required this.rules,
|
||||||
required this.marketplaceFallbackEnabled,
|
required this.marketplaceFallbackEnabled,
|
||||||
|
required this.authExceptionsEnabled,
|
||||||
required this.protectGeneralContext,
|
required this.protectGeneralContext,
|
||||||
required this.protectedContextIds,
|
required this.protectedContextIds,
|
||||||
required this.strictContextIds,
|
required this.strictContextIds,
|
||||||
|
|||||||
Reference in New Issue
Block a user