diff --git a/lib/solid_auth_issuer.dart b/lib/solid_auth_issuer.dart index 4105772..0a7b087 100644 --- a/lib/solid_auth_issuer.dart +++ b/lib/solid_auth_issuer.dart @@ -32,26 +32,100 @@ import 'dart:async'; import 'package:http/http.dart' as http; -/// Get POD issuer URI +/// In-memory cache: maps a server URL / WebID to its resolved OIDC issuer URI. +/// The issuer URI for a given Solid server never changes at runtime, so a +/// simple process-lifetime cache is safe and avoids repeated HTTP round-trips. + +final Map _issuerCache = {}; + +/// Profile-body cache: maps the plain profile card URL (fragment stripped) to +/// the Turtle body fetched during [getIssuer]. +/// +/// When the user enters a WebID URL such as `https://example.org/profile/card#me`, +/// [getIssuer] must fetch the profile document to extract the OIDC issuer URI. +/// The same document is needed again after authentication to populate profile +/// data. Caching it here avoids a redundant (second) HTTP round-trip. + +final Map _profileBodyCache = {}; + +/// Strips any URI fragment (e.g. `#me`) from [profUrl] so that cache keys are +/// canonical regardless of whether the caller includes a fragment. + +String _normalizeProfileUrl(String profUrl) { + final int hashIndex = profUrl.indexOf('#'); + return hashIndex == -1 ? profUrl : profUrl.substring(0, hashIndex); +} + +/// Returns the profile card body that was fetched during [getIssuer], or `null` +/// if the profile has not been fetched yet (e.g. the server URL is a plain +/// issuer URI rather than a WebID URL). +/// +/// The [profUrl] fragment (if any) is stripped before the look-up so callers +/// may pass either the bare document URL or a full WebID URL. + +String? getCachedIssuerProfileBody(String profUrl) => + _profileBodyCache[_normalizeProfileUrl(profUrl)]; + +/// Clears both in-memory caches. +/// Useful in tests or whenever server configuration may have changed. + +void clearIssuerCaches() { + _issuerCache.clear(); + _profileBodyCache.clear(); +} + +/// Get POD issuer URI. +/// +/// Results are cached in memory so that repeated calls for the same [textUrl] +/// (e.g. when the user logs out and back in) skip the network look-up. + Future getIssuer(String textUrl) async { + // Trim leading/trailing whitespace defensively so that solid_auth behaves + // correctly regardless of whether the caller has already sanitised the input. + final String trimmed = textUrl.trim(); + + // Normalize the key so that trivially different representations of the same + // URL (e.g. percent-encoding variants) share a single cache entry. + // Uri.tryParse is used instead of Uri.parse to avoid a FormatException for + // malformed inputs; fall back to the trimmed string if parsing fails. + final String cacheKey = Uri.tryParse(trimmed)?.toString() ?? trimmed; + + // Return cached result immediately when available. + + if (_issuerCache.containsKey(cacheKey)) { + return _issuerCache[cacheKey]!; + } + String issuerUri = ''; - if (textUrl.contains('profile/card#me')) { - String pubProf = await fetchProfileData(textUrl); + if (trimmed.contains('profile/card#me')) { + String pubProf = await fetchProfileData(trimmed); + + // Cache the profile body under the plain profile document URL (fragment + // stripped). solidpod/authenticate.dart can reuse this to skip a second + // HTTP GET. + + _profileBodyCache[_normalizeProfileUrl(trimmed)] = pubProf; issuerUri = getIssuerUri(pubProf); } if (issuerUri == '') { - /// This reg expression works with localhost and other urls + /// This reg expression works with localhost and other urls. RegExp exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+(\.|\:)[\w\.]+'); - Iterable matches = exp.allMatches(textUrl); + Iterable matches = exp.allMatches(trimmed); for (var match in matches) { - issuerUri = textUrl.substring(match.start, match.end); + issuerUri = trimmed.substring(match.start, match.end); } } + + if (issuerUri.isNotEmpty) { + _issuerCache[cacheKey] = issuerUri; + } + return issuerUri; } -/// Get public profile information from webId +/// Get public profile information from webId. + Future fetchProfileData(String profUrl) async { final response = await http.get( Uri.parse(profUrl), @@ -71,7 +145,8 @@ Future fetchProfileData(String profUrl) async { } } -/// Read public profile RDF file and get the issuer URI +/// Read public profile RDF file and get the issuer URI. + String getIssuerUri(String profileRdfStr) { String issuerUri = ''; var profileDataList = profileRdfStr.split('\n');