diff --git a/lib/screens/cloud_screen.dart b/lib/screens/cloud_screen.dart index 69af4f0..5a3b285 100644 --- a/lib/screens/cloud_screen.dart +++ b/lib/screens/cloud_screen.dart @@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import '../utils/farb_theme.dart'; +import '../utils/sanitize.dart'; import '../services/cloud_service.dart'; import '../services/auth_service.dart'; import '../database/db_helper.dart'; @@ -216,7 +217,8 @@ class _CloudScreenState extends State { _updateSync('Download: $title...', 0.1 + (0.4 * processed / (totalNew > 0 ? totalNew : 1))); - final dest = '${dir.path}/$title'; + final safeTitle = sanitizeDateiname(title); + final dest = '${dir.path}/$safeTitle'; if (await widget.cloud.download(sid, dest)) { downloaded++; // In DB eintragen mit cloud_id diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 10c184a..5ab17de 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -12,6 +12,7 @@ import '../widgets/song_tile.dart'; import 'download_screen.dart'; import 'cloud_screen.dart'; import 'settings_screen.dart'; +import 'login_screen.dart'; import '../services/cloud_service.dart'; import '../services/auth_service.dart'; import '../config/app_config.dart'; @@ -397,7 +398,7 @@ class _MeloHomeState extends State { await AuthService().logout(); if (mounted) { Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const MeloHome()), + MaterialPageRoute(builder: (_) => const LoginScreen()), ); } break; diff --git a/lib/services/audio_handler.dart b/lib/services/audio_handler.dart index e7c4406..795461f 100644 --- a/lib/services/audio_handler.dart +++ b/lib/services/audio_handler.dart @@ -54,20 +54,20 @@ class MeloAudioHandler extends BaseAudioHandler { Future play() async { if (_player.aktuellerSong != null) { _playing = true; - await _player.playPause(); + await _player.play(); } } @override Future pause() async { _playing = false; - await _player.playPause(); + await _player.pause(); } @override Future stop() async { _playing = false; - await _player.playPause(); + await _player.pause(); playbackState.add(playbackState.value.copyWith( controls: [MediaControl.play], playing: false, @@ -77,7 +77,7 @@ class MeloAudioHandler extends BaseAudioHandler { @override Future seek(Duration position) async { - _player.spiele(_player.aktuellerSong!, position: position.inSeconds); + await _player.seek(position); } @override diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 4edc2e5..fba6d73 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../config/app_config.dart'; import '../services/melo_logger.dart'; @@ -11,6 +11,8 @@ class AuthService { factory AuthService() => _instance; AuthService._(); + final FlutterSecureStorage _secure = const FlutterSecureStorage(); + String? _token; String _user = ''; bool _initialisiert = false; @@ -34,9 +36,8 @@ class AuthService { Future initialisieren() async { if (_initialisiert) return istEingeloggt; try { - final prefs = await SharedPreferences.getInstance(); - _token = prefs.getString('baka_token'); - _user = prefs.getString('baka_user') ?? ''; + _token = await _secure.read(key: 'baka_token'); + _user = await _secure.read(key: 'baka_user') ?? ''; if (_token != null && _token!.isNotEmpty) { // Token beim Server validieren (nicht blind vertrauen) @@ -46,8 +47,8 @@ class AuthService { MeloLogger().zustand('auth_token_expired', {'user': _user}); _token = null; _user = ''; - await prefs.remove('baka_token'); - await prefs.remove('baka_user'); + await _secure.delete(key: 'baka_token'); + await _secure.delete(key: 'baka_user'); } else { MeloLogger().zustand('auth_restored', {'user': _user}); } @@ -100,9 +101,8 @@ class AuthService { _user = user; if (_token != null && _token!.isNotEmpty) { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('baka_token', _token!); - await prefs.setString('baka_user', _user); + await _secure.write(key: 'baka_token', value: _token!); + await _secure.write(key: 'baka_user', value: _user); MeloLogger().aktion('auth_login_ok', {'user': _user}); return AuthResult.ok; } @@ -144,9 +144,8 @@ class AuthService { _user = user; if (_token != null && _token!.isNotEmpty) { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('baka_token', _token!); - await prefs.setString('baka_user', _user); + await _secure.write(key: 'baka_token', value: _token!); + await _secure.write(key: 'baka_user', value: _user); MeloLogger().aktion('auth_register_ok', {'user': _user}); return AuthResult.ok; } @@ -172,9 +171,8 @@ class AuthService { Future logout() async { _token = null; _user = ''; - final prefs = await SharedPreferences.getInstance(); - await prefs.remove('baka_token'); - await prefs.remove('baka_user'); + await _secure.delete(key: 'baka_token'); + await _secure.delete(key: 'baka_user'); MeloLogger().aktion('auth_logout', {}); } } diff --git a/lib/services/cloud_service.dart b/lib/services/cloud_service.dart index d62baf9..5b055d0 100644 --- a/lib/services/cloud_service.dart +++ b/lib/services/cloud_service.dart @@ -11,11 +11,9 @@ class CloudService { static final http.Client _client = http.Client(); static String get _base => AppConfig.cloudUrl; - String _user = ''; /// Login mit Baka-Auth – Token wird aus AuthService bezogen Future login(String user) async { - _user = user; try { final r = await _client .get(Uri.parse('$_base/api/cloud/status'), @@ -32,9 +30,6 @@ class CloudService { final headers = { 'X-API-Key': AppConfig.ytProxyApiKey, }; - if (_user.isNotEmpty) { - headers['X-User'] = _user; - } if (token != null && token.isNotEmpty) { headers['Authorization'] = 'Bearer $token'; } diff --git a/lib/services/download_service.dart b/lib/services/download_service.dart index 3a54c05..13251f6 100644 --- a/lib/services/download_service.dart +++ b/lib/services/download_service.dart @@ -12,6 +12,7 @@ import '../utils/audio_validator.dart'; import 'melo_logger.dart'; import '../config/app_config.dart'; import '../main.dart'; // für notificationsPlugin +import '../utils/sanitize.dart'; /// Download-Service: Lädt YouTube-Audio über den yt-proxy herunter. /// Nutzt ChangeNotifier für UI-Updates via ListenableBuilder. @@ -292,9 +293,9 @@ class DownloadService extends ChangeNotifier { : Directory('${(await getApplicationDocumentsDirectory()).path}/music'); if (!await dir.exists()) await dir.create(recursive: true); - // Sicheren Dateinamen erstellen - final safeName = titel.replaceAll(RegExp(r'[^\w\s-]'), '').trim(); - final lokalerName = '${safeName.isEmpty ? "song" : safeName}.mp3'; + // Sicheren Dateinamen erstellen (zentrale Sanitize-Funktion) + final safeName = sanitizeDateiname(titel); + final lokalerName = '$safeName.mp3'; dateiPfad = '${dir.path}/$lokalerName'; final stopwatch2 = Stopwatch()..start(); diff --git a/lib/services/navidrome_service.dart b/lib/services/navidrome_service.dart index 5018ffe..0826412 100644 --- a/lib/services/navidrome_service.dart +++ b/lib/services/navidrome_service.dart @@ -5,8 +5,10 @@ import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../models/song.dart'; import '../database/db_helper.dart'; +import '../utils/sanitize.dart'; /// Ein Song aus der Subsonic-API class SubsonicSong { @@ -63,6 +65,7 @@ class SubsonicAlbum { class NavidromeService { final DbHelper _db = DbHelper(); + final FlutterSecureStorage _secure = const FlutterSecureStorage(); String _serverUrl = ''; String _user = ''; @@ -86,7 +89,7 @@ class NavidromeService { final prefs = await SharedPreferences.getInstance(); final url = prefs.getString('navidrome_url'); final user = prefs.getString('navidrome_user'); - final pass = prefs.getString('navidrome_pass'); + final pass = await _secure.read(key: 'navidrome_pass'); if (url != null && user != null && pass != null && url.isNotEmpty) { setCredentials(url, user, pass); } @@ -101,7 +104,7 @@ class NavidromeService { final prefs = await SharedPreferences.getInstance(); await prefs.setString('navidrome_url', url); await prefs.setString('navidrome_user', user); - await prefs.setString('navidrome_pass', password); + await _secure.write(key: 'navidrome_pass', value: password); } catch (e) { debugPrint('Fehler beim Speichern der Navidrome-Zugangsdaten: $e'); } @@ -178,9 +181,9 @@ class NavidromeService { final musikDir = Directory('${dir.path}/music'); if (!await musikDir.exists()) await musikDir.create(recursive: true); - final safeName = s.titel.replaceAll(RegExp(r'[^\w\s-]'), '').trim(); + final safeName = sanitizeDateiname(s.titel); final kurzId = s.id.length > 8 ? s.id.substring(0, 8) : s.id; - final dateiName = '${safeName.isEmpty ? "song" : safeName}_$kurzId.mp3'; + final dateiName = '${safeName}_$kurzId.mp3'; final dateiPfad = '${musikDir.path}/$dateiName'; final file = File(dateiPfad); diff --git a/lib/services/player_service.dart b/lib/services/player_service.dart index 8f10432..c5f0408 100644 --- a/lib/services/player_service.dart +++ b/lib/services/player_service.dart @@ -86,6 +86,24 @@ class PlayerService { } } + /// Explizit abspielen (kein Toggle) — für System-Controls (Bluetooth, etc.) + Future play() async { + if (_player == null || aktuellerSong == null) return; + await _p.play(); + } + + /// Explizit pausieren (kein Toggle) — für System-Controls (Bluetooth, etc.) + Future pause() async { + if (_player == null) return; + await _p.pause(); + } + + /// Nur seeken ohne Reload der Quelle + Future seek(Duration position) async { + if (_player == null) return; + await _p.seek(position); + } + Future vorheriges() async { if (_player == null || aktuellerSong == null) return; final pos = _p.position; diff --git a/lib/utils/sanitize.dart b/lib/utils/sanitize.dart new file mode 100644 index 0000000..96df035 --- /dev/null +++ b/lib/utils/sanitize.dart @@ -0,0 +1,24 @@ +/// Zentrale Sanitize-Funktion für Dateinamen. +/// Verhindert Path-Traversal (../), Null-Bytes und andere gefährliche Zeichen. +String sanitizeDateiname(String name) { + // 1. Extrahiere nur den Dateinamen (keine Pfad-Komponenten) + var safe = name.split('/').last.split('\\').last; + + // 2. Entferne NULL-Bytes und andere gefährliche Steuerzeichen + safe = safe.replaceAll(RegExp(r'[\x00-\x1F\x7F]'), ''); + + // 3. Entferne alles außer Wort-Zeichen, Leerzeichen und Bindestrichen + safe = safe.replaceAll(RegExp(r'[^\w\s.-]'), ''); + + // 4. Entferne führende Punkte (versteckte Dateien) + safe = safe.replaceAll(RegExp(r'^\.+'), ''); + + // 5. Trimmen und Default-Name falls leer + safe = safe.trim(); + if (safe.isEmpty) safe = 'unknown'; + + // 6. Auf sinnvolle Länge kürzen (255 Zeichen ist typisches Dateisystem-Limit) + if (safe.length > 200) safe = safe.substring(0, 200); + + return safe; +} diff --git a/lib/viewmodels/melo_home_viewmodel.dart b/lib/viewmodels/melo_home_viewmodel.dart index f32453b..2cad2ba 100644 --- a/lib/viewmodels/melo_home_viewmodel.dart +++ b/lib/viewmodels/melo_home_viewmodel.dart @@ -269,7 +269,7 @@ class MeloHomeViewModel extends ChangeNotifier { @override void dispose() { _positionsSub?.cancel(); - player.dispose(); + // PlayerService ist Singleton — nicht hier disposesen, lebt für die App-Lebensdauer super.dispose(); } }