From e936d025252043e8a77461fbdbd5079e095f782f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:39:51 +0000 Subject: [PATCH 1/7] Initial plan From f387cb8aaf9295c867664ade6b38a9ec12758742 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:42:36 +0000 Subject: [PATCH 2/7] Add NavigaTUM room links Co-authored-by: kordianbruck <298860+kordianbruck@users.noreply.github.com> --- assets/translations/de.json | 2 ++ assets/translations/en.json | 2 ++ ...atum_navigation_additional_properties.dart | 4 ++- ...um_navigation_additional_properties.g.dart | 6 +++- .../model/navigatum_navigation_details.dart | 6 ++++ .../views/navigatum_room_view.dart | 29 +++++++++++++++++-- test/navigaTumApi/navigatum_test.dart | 9 +++++- 7 files changed, 53 insertions(+), 5 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index 6b8c9d67..0ce18b15 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -103,6 +103,8 @@ "occupiedUntil": "Belegt bis {}", "rooms": "Räume", "roomDetails": "Raumdetails", + "openInNavigaTum": "In NavigaTUM öffnen", + "openRoomCalendar": "Raumkalender öffnen", "building": "Gebäude", "nFreeRooms": { "zero": "Keine freien Räume", diff --git a/assets/translations/en.json b/assets/translations/en.json index c1c49833..6ba9296c 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -103,6 +103,8 @@ "occupiedUntil": "Occupied until {}", "rooms": "Rooms", "roomDetails": "Room Details", + "openInNavigaTum": "Open in NavigaTUM", + "openRoomCalendar": "Open room calendar", "building": "Building", "nFreeRooms": { "zero": "No free rooms", diff --git a/lib/navigaTumComponent/model/details/navigatum_navigation_additional_properties.dart b/lib/navigaTumComponent/model/details/navigatum_navigation_additional_properties.dart index bfb5c1c5..e4aa031f 100644 --- a/lib/navigaTumComponent/model/details/navigatum_navigation_additional_properties.dart +++ b/lib/navigaTumComponent/model/details/navigatum_navigation_additional_properties.dart @@ -7,8 +7,10 @@ part 'navigatum_navigation_additional_properties.g.dart'; class NavigaTumNavigationAdditionalProperties { @JsonKey(name: "computed") final List properties; + @JsonKey(name: "calendar_url") + final String? calendarUrl; - NavigaTumNavigationAdditionalProperties(this.properties); + NavigaTumNavigationAdditionalProperties(this.properties, this.calendarUrl); factory NavigaTumNavigationAdditionalProperties.fromJson( Map json, diff --git a/lib/navigaTumComponent/model/details/navigatum_navigation_additional_properties.g.dart b/lib/navigaTumComponent/model/details/navigatum_navigation_additional_properties.g.dart index aee2fc0e..c3695b74 100644 --- a/lib/navigaTumComponent/model/details/navigatum_navigation_additional_properties.g.dart +++ b/lib/navigaTumComponent/model/details/navigatum_navigation_additional_properties.g.dart @@ -15,8 +15,12 @@ _$NavigaTumNavigationAdditionalPropertiesFromJson(Map json) => NavigaTumNavigationProperty.fromJson(e as Map), ) .toList(), + json['calendar_url'] as String?, ); Map _$NavigaTumNavigationAdditionalPropertiesToJson( NavigaTumNavigationAdditionalProperties instance, -) => {'computed': instance.properties}; +) => { + 'computed': instance.properties, + 'calendar_url': instance.calendarUrl, +}; diff --git a/lib/navigaTumComponent/model/navigatum_navigation_details.dart b/lib/navigaTumComponent/model/navigatum_navigation_details.dart index 5cb76f3c..9166340e 100644 --- a/lib/navigaTumComponent/model/navigatum_navigation_details.dart +++ b/lib/navigaTumComponent/model/navigatum_navigation_details.dart @@ -23,6 +23,12 @@ class NavigaTumNavigationDetails { bool get hasCoordinates => coordinates.latitude != null && coordinates.longitude != null; + Uri get navigaTumUri => Uri.https("nav.tum.de", "/$type/$id"); + + Uri? get calendarUri => additionalProperties.calendarUrl != null + ? Uri.tryParse(additionalProperties.calendarUrl!) + : null; + NavigaTumNavigationDetails( this.id, this.name, diff --git a/lib/navigaTumComponent/views/navigatum_room_view.dart b/lib/navigaTumComponent/views/navigatum_room_view.dart index 47d95f31..45a14fdd 100644 --- a/lib/navigaTumComponent/views/navigatum_room_view.dart +++ b/lib/navigaTumComponent/views/navigatum_room_view.dart @@ -3,6 +3,7 @@ import 'package:campus_flutter/base/util/custom_back_button.dart'; import 'package:campus_flutter/base/util/delayed_loading_indicator.dart'; import 'package:campus_flutter/base/errorHandling/error_handling_router.dart'; import 'package:campus_flutter/base/util/map_launcher.dart'; +import 'package:campus_flutter/base/util/url_launcher.dart'; import 'package:campus_flutter/navigaTumComponent/model/navigatum_navigation_details.dart'; import 'package:campus_flutter/navigaTumComponent/viewModels/navigatum_details_viewmodel.dart'; import 'package:campus_flutter/navigaTumComponent/views/navigatum_room_details_view.dart'; @@ -29,8 +30,8 @@ class NavigaTumRoomScaffold extends ConsumerWidget { StreamBuilder( stream: ref.watch(navigaTumDetailsViewModel(id)).details, builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.hasCoordinates) { - return _directionsButton(snapshot.data!, context); + if (snapshot.hasData) { + return _actionButtons(snapshot.data!, context, ref); } else { return const SizedBox.shrink(); } @@ -42,6 +43,30 @@ class NavigaTumRoomScaffold extends ConsumerWidget { ); } + Widget _actionButtons( + NavigaTumNavigationDetails details, + BuildContext context, + WidgetRef ref, + ) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => UrlLauncher.url(details.navigaTumUri, ref), + tooltip: context.tr("openInNavigaTum"), + icon: Icon(Icons.open_in_new, color: context.theme.primaryColor), + ), + if (details.calendarUri != null) + IconButton( + onPressed: () => UrlLauncher.url(details.calendarUri!, ref), + tooltip: context.tr("openRoomCalendar"), + icon: Icon(Icons.calendar_month, color: context.theme.primaryColor), + ), + if (details.hasCoordinates) _directionsButton(details, context), + ], + ); + } + Widget _directionsButton( NavigaTumNavigationDetails details, BuildContext context, diff --git a/test/navigaTumApi/navigatum_test.dart b/test/navigaTumApi/navigatum_test.dart index 0bfd319f..c8bba3b4 100644 --- a/test/navigaTumApi/navigatum_test.dart +++ b/test/navigaTumApi/navigatum_test.dart @@ -115,7 +115,8 @@ void main() { "computed": [ {"name": "Seats", "text": "200"}, {"name": "Building", "text": "MI"} - ] + ], + "calendar_url": "https://campus.tum.de/tumonline/tvKalender.wSicht?cOrg=19691&cRes=12543&cReadonly=J" }, "coords": { "lat": 48.26244, @@ -163,6 +164,12 @@ void main() { expect(details.additionalProperties.properties.length, 2); expect(details.additionalProperties.properties[0].name, 'Seats'); expect(details.additionalProperties.properties[0].text, '200'); + expect( + details.additionalProperties.calendarUrl, + 'https://campus.tum.de/tumonline/tvKalender.wSicht?cOrg=19691&cRes=12543&cReadonly=J', + ); + expect(details.calendarUri?.host, 'campus.tum.de'); + expect(details.navigaTumUri.toString(), 'https://nav.tum.de/room/5606.EG.001'); // Maps expect(details.maps.defaultMapId, 'rf142'); From c56eea321dee2f6dd8a63b867167da4663509caf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:43:59 +0000 Subject: [PATCH 3/7] Scope NavigaTUM room link route Co-authored-by: kordianbruck <298860+kordianbruck@users.noreply.github.com> --- lib/navigaTumComponent/model/navigatum_navigation_details.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/navigaTumComponent/model/navigatum_navigation_details.dart b/lib/navigaTumComponent/model/navigatum_navigation_details.dart index 9166340e..ac43c9c1 100644 --- a/lib/navigaTumComponent/model/navigatum_navigation_details.dart +++ b/lib/navigaTumComponent/model/navigatum_navigation_details.dart @@ -23,7 +23,7 @@ class NavigaTumNavigationDetails { bool get hasCoordinates => coordinates.latitude != null && coordinates.longitude != null; - Uri get navigaTumUri => Uri.https("nav.tum.de", "/$type/$id"); + Uri get navigaTumUri => Uri.https("nav.tum.de", "/room/$id"); Uri? get calendarUri => additionalProperties.calendarUrl != null ? Uri.tryParse(additionalProperties.calendarUrl!) From 567fd7281d0b15b956dd0ba51c896b4577876a65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:44:18 +0000 Subject: [PATCH 4/7] Encode NavigaTUM room URL safely Co-authored-by: kordianbruck <298860+kordianbruck@users.noreply.github.com> --- lib/navigaTumComponent/model/navigatum_navigation_details.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/navigaTumComponent/model/navigatum_navigation_details.dart b/lib/navigaTumComponent/model/navigatum_navigation_details.dart index ac43c9c1..5ad0f97b 100644 --- a/lib/navigaTumComponent/model/navigatum_navigation_details.dart +++ b/lib/navigaTumComponent/model/navigatum_navigation_details.dart @@ -23,7 +23,8 @@ class NavigaTumNavigationDetails { bool get hasCoordinates => coordinates.latitude != null && coordinates.longitude != null; - Uri get navigaTumUri => Uri.https("nav.tum.de", "/room/$id"); + Uri get navigaTumUri => + Uri(scheme: "https", host: "nav.tum.de", pathSegments: ["room", id]); Uri? get calendarUri => additionalProperties.calendarUrl != null ? Uri.tryParse(additionalProperties.calendarUrl!) From b2b4fe43545c402862ee686bf0319f7a472f06e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:44:48 +0000 Subject: [PATCH 5/7] Preserve NavigaTUM room path segments Co-authored-by: kordianbruck <298860+kordianbruck@users.noreply.github.com> --- .../model/navigatum_navigation_details.dart | 7 +++++-- test/navigaTumApi/navigatum_test.dart | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/navigaTumComponent/model/navigatum_navigation_details.dart b/lib/navigaTumComponent/model/navigatum_navigation_details.dart index 5ad0f97b..ffb6d809 100644 --- a/lib/navigaTumComponent/model/navigatum_navigation_details.dart +++ b/lib/navigaTumComponent/model/navigatum_navigation_details.dart @@ -23,8 +23,11 @@ class NavigaTumNavigationDetails { bool get hasCoordinates => coordinates.latitude != null && coordinates.longitude != null; - Uri get navigaTumUri => - Uri(scheme: "https", host: "nav.tum.de", pathSegments: ["room", id]); + Uri get navigaTumUri => Uri( + scheme: "https", + host: "nav.tum.de", + pathSegments: ["room", ...id.split("/")], + ); Uri? get calendarUri => additionalProperties.calendarUrl != null ? Uri.tryParse(additionalProperties.calendarUrl!) diff --git a/test/navigaTumApi/navigatum_test.dart b/test/navigaTumApi/navigatum_test.dart index c8bba3b4..64951496 100644 --- a/test/navigaTumApi/navigatum_test.dart +++ b/test/navigaTumApi/navigatum_test.dart @@ -197,5 +197,23 @@ void main() { expect(details.hasCoordinates, isFalse); }); + + test('navigaTumUri preserves slash-separated room path segments', () { + final json = j(''' +{ + "id": "5500/01?A", + "name": "Test Room", + "parent_names": ["Garching"], + "type": "room", + "type_common_name": "Room", + "props": {"computed": []}, + "coords": {}, + "maps": {"default": "rf000"} +} +'''); + final details = NavigaTumNavigationDetails.fromJson(json); + + expect(details.navigaTumUri.toString(), 'https://nav.tum.de/room/5500/01%3FA'); + }); }); } From d571256d59bf5899b9cbd686f5956a6b2541c40f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:45:11 +0000 Subject: [PATCH 6/7] Treat NavigaTUM room ids as opaque Co-authored-by: kordianbruck <298860+kordianbruck@users.noreply.github.com> --- .../model/navigatum_navigation_details.dart | 7 ++----- test/navigaTumApi/navigatum_test.dart | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/navigaTumComponent/model/navigatum_navigation_details.dart b/lib/navigaTumComponent/model/navigatum_navigation_details.dart index ffb6d809..5ad0f97b 100644 --- a/lib/navigaTumComponent/model/navigatum_navigation_details.dart +++ b/lib/navigaTumComponent/model/navigatum_navigation_details.dart @@ -23,11 +23,8 @@ class NavigaTumNavigationDetails { bool get hasCoordinates => coordinates.latitude != null && coordinates.longitude != null; - Uri get navigaTumUri => Uri( - scheme: "https", - host: "nav.tum.de", - pathSegments: ["room", ...id.split("/")], - ); + Uri get navigaTumUri => + Uri(scheme: "https", host: "nav.tum.de", pathSegments: ["room", id]); Uri? get calendarUri => additionalProperties.calendarUrl != null ? Uri.tryParse(additionalProperties.calendarUrl!) diff --git a/test/navigaTumApi/navigatum_test.dart b/test/navigaTumApi/navigatum_test.dart index 64951496..4f362412 100644 --- a/test/navigaTumApi/navigatum_test.dart +++ b/test/navigaTumApi/navigatum_test.dart @@ -198,7 +198,7 @@ void main() { expect(details.hasCoordinates, isFalse); }); - test('navigaTumUri preserves slash-separated room path segments', () { + test('navigaTumUri encodes the room id as a single path segment', () { final json = j(''' { "id": "5500/01?A", @@ -213,7 +213,7 @@ void main() { '''); final details = NavigaTumNavigationDetails.fromJson(json); - expect(details.navigaTumUri.toString(), 'https://nav.tum.de/room/5500/01%3FA'); + expect(details.navigaTumUri.toString(), 'https://nav.tum.de/room/5500%2F01%3FA'); }); }); } From 464de929c30f92a0c70bd667a40c7ef120ca2c2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:45:35 +0000 Subject: [PATCH 7/7] Restrict room calendar URLs Co-authored-by: kordianbruck <298860+kordianbruck@users.noreply.github.com> --- .../model/navigatum_navigation_details.dart | 12 ++++++++--- test/navigaTumApi/navigatum_test.dart | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/navigaTumComponent/model/navigatum_navigation_details.dart b/lib/navigaTumComponent/model/navigatum_navigation_details.dart index 5ad0f97b..e7757119 100644 --- a/lib/navigaTumComponent/model/navigatum_navigation_details.dart +++ b/lib/navigaTumComponent/model/navigatum_navigation_details.dart @@ -26,9 +26,15 @@ class NavigaTumNavigationDetails { Uri get navigaTumUri => Uri(scheme: "https", host: "nav.tum.de", pathSegments: ["room", id]); - Uri? get calendarUri => additionalProperties.calendarUrl != null - ? Uri.tryParse(additionalProperties.calendarUrl!) - : null; + Uri? get calendarUri { + final uri = additionalProperties.calendarUrl != null + ? Uri.tryParse(additionalProperties.calendarUrl!) + : null; + if (uri == null || uri.scheme != "https" || uri.host != "campus.tum.de") { + return null; + } + return uri; + } NavigaTumNavigationDetails( this.id, diff --git a/test/navigaTumApi/navigatum_test.dart b/test/navigaTumApi/navigatum_test.dart index 4f362412..8430f90e 100644 --- a/test/navigaTumApi/navigatum_test.dart +++ b/test/navigaTumApi/navigatum_test.dart @@ -215,5 +215,26 @@ void main() { expect(details.navigaTumUri.toString(), 'https://nav.tum.de/room/5500%2F01%3FA'); }); + + test('calendarUri rejects untrusted schemes', () { + final json = j(''' +{ + "id": "5606.EG.001", + "name": "Test Room", + "parent_names": ["Garching"], + "type": "room", + "type_common_name": "Room", + "props": { + "computed": [], + "calendar_url": "javascript:alert(1)" + }, + "coords": {}, + "maps": {"default": "rf000"} +} +'''); + final details = NavigaTumNavigationDetails.fromJson(json); + + expect(details.calendarUri, isNull); + }); }); }