make allow unsigned permanent

This commit is contained in:
Fabian Freund
2026-03-26 18:32:56 +01:00
parent 0c6d669185
commit 05c52668df
6 changed files with 250 additions and 78 deletions
@@ -17,6 +17,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -25,11 +26,43 @@ import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
part 'browser_addon.g.dart';
const _signatureRequiredPref = 'xpinstall.signatures.required';
@Riverpod(keepAlive: true)
class AllowUnsignedExtensions extends _$AllowUnsignedExtensions {
Future<void> setAllowUnsigned({required bool allow}) async {
final fixator = ref.read(preferenceFixatorProvider.notifier);
if (allow) {
await fixator.register(_signatureRequiredPref, false);
} else {
await fixator.unregister(_signatureRequiredPref);
await GeckoPrefService().applyPrefs({_signatureRequiredPref: true});
}
state = AsyncData(allow);
}
@override
FutureOr<bool> build() async {
final prefs =
await GeckoPrefService().getPrefs([_signatureRequiredPref]);
final pref = prefs[_signatureRequiredPref];
final allowUnsigned = pref?.value == false;
// Re-register with fixator to prevent Gecko from resetting
if (allowUnsigned) {
await ref
.read(preferenceFixatorProvider.notifier)
.register(_signatureRequiredPref, false);
}
return allowUnsigned;
}
}
@Riverpod(keepAlive: true)
class BrowserAddonService extends _$BrowserAddonService {
Future<Uri> getAddonXpiUrl(String guid) async {
@@ -68,12 +101,7 @@ class BrowserAddonService extends _$BrowserAddonService {
}
}
Future<bool> installFromFile(
String filePath, {
bool allowUnsigned = false,
}) async {
final prefService = GeckoPrefService();
Future<bool> installFromFile(String filePath) async {
try {
// Validate file exists and has .xpi extension
final file = File(filePath);
@@ -86,25 +114,13 @@ class BrowserAddonService extends _$BrowserAddonService {
throw Exception('Invalid file type. Expected .xpi file');
}
// Temporarily disable signature requirement if user allows unsigned
if (allowUnsigned) {
await prefService.applyPrefs({_signatureRequiredPref: false});
}
// Create file:// URI and install
final fileUri = Uri.file(filePath);
if (!ref.mounted) return false;
try {
// Create file:// URI and install
final fileUri = Uri.file(filePath);
if (!ref.mounted) return false;
await ref.read(addonServiceProvider).installAddon(fileUri);
await ref.read(addonServiceProvider).installAddon(fileUri);
return true;
} finally {
// Always restore signature requirement
if (allowUnsigned) {
await prefService.applyPrefs({_signatureRequiredPref: true});
}
}
return true;
} catch (e, s) {
logger.e(
'Failed installing from file: $filePath',
@@ -9,6 +9,51 @@ part of 'browser_addon.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(AllowUnsignedExtensions)
final allowUnsignedExtensionsProvider = AllowUnsignedExtensionsProvider._();
final class AllowUnsignedExtensionsProvider
extends $AsyncNotifierProvider<AllowUnsignedExtensions, bool> {
AllowUnsignedExtensionsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'allowUnsignedExtensionsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$allowUnsignedExtensionsHash();
@$internal
@override
AllowUnsignedExtensions create() => AllowUnsignedExtensions();
}
String _$allowUnsignedExtensionsHash() =>
r'3b67f51cabf8e2f37d992320c5b93bb61c6dbe1f';
abstract class _$AllowUnsignedExtensions extends $AsyncNotifier<bool> {
FutureOr<bool> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<bool>, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<bool>, bool>,
AsyncValue<bool>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(BrowserAddonService)
final browserAddonServiceProvider = BrowserAddonServiceProvider._();
@@ -42,7 +87,7 @@ final class BrowserAddonServiceProvider
}
String _$browserAddonServiceHash() =>
r'1485fd056ce32e142e342e920affb82a5c77a370';
r'b90e69ce77b846d6056c800f232a8aa50b4b21d0';
abstract class _$BrowserAddonService extends $Notifier<void> {
void build();
@@ -23,6 +23,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_settings.dart';
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
@@ -286,6 +287,9 @@ class EngineSettingsReplicationService
.read(preferenceFixatorProvider.notifier)
.register('intl.accept_languages', settings.locales.join(','));
// Initialize unsigned extensions fixator from Gecko pref
await ref.read(allowUnsignedExtensionsProvider.future);
initialSettingsSent = true;
}
}
@@ -44,7 +44,7 @@ final class EngineSettingsReplicationServiceProvider
}
String _$engineSettingsReplicationServiceHash() =>
r'a2fe25ca6458dd22d705affee74036e1ba379972';
r'81443c37d18ad82f0694cd0e1115ccdf8a0f4c06';
abstract class _$EngineSettingsReplicationService extends $Notifier<void> {
void build();
@@ -40,7 +40,6 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedFile = useState<String?>(null);
final allowUnsigned = useState(false);
final isInstalling = useState(false);
final errorMessage = useState<String?>(null);
@@ -74,10 +73,7 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
try {
await ref
.read(browserAddonServiceProvider.notifier)
.installFromFile(
selectedFile.value!,
allowUnsigned: allowUnsigned.value,
);
.installFromFile(selectedFile.value!);
if (context.mounted) {
showInfoMessage(context, 'Extension installed successfully');
@@ -88,7 +84,7 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
if (errorString.contains('NotSigned') ||
errorString.contains('SIGNEDSTATE')) {
errorMessage.value =
'This extension is not signed by Mozilla. Enable "Allow unsigned extensions" to install it.';
'This extension is not signed by Mozilla. Enable "Allow unsigned extensions" in Extensions settings to install it.';
} else {
errorMessage.value = 'Installation failed: $errorString';
}
@@ -153,53 +149,6 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
],
),
),
const SizedBox(height: 16),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Allow unsigned extensions'),
subtitle: const Text(
'Unsigned extensions have not been verified by Mozilla',
),
value: allowUnsigned.value,
onChanged: isInstalling.value
? null
: (value) => allowUnsigned.value = value,
),
if (allowUnsigned.value) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.errorContainer.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.error,
),
),
child: Row(
children: [
Icon(
Icons.warning_amber,
color: Theme.of(context).colorScheme.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Only install unsigned extensions from sources you trust. '
'They may contain malicious code.',
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
fontSize: 12,
),
),
),
],
),
),
],
if (errorMessage.value != null) ...[
const SizedBox(height: 8),
Container(