diff --git a/CHANGELOG.md b/CHANGELOG.md index c22c76e350..56d453a61c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ AppEngine version, listed here to ease deployment and troubleshooting. ## Next Release (replace with git tag when deployed) * Bump runtimeVersion to `2025.09.08`. * Upgraded stable Flutter analysis SDK to `3.35.3`. + * Updated SDK constraint (and language version) to `3.9` ## `20250904t074800-all` * Bump runtimeVersion to `2025.08.29`. diff --git a/app/bin/tools/check_domain_access.dart b/app/bin/tools/check_domain_access.dart index 9e857865fe..42a83bf9ff 100644 --- a/app/bin/tools/check_domain_access.dart +++ b/app/bin/tools/check_domain_access.dart @@ -43,8 +43,10 @@ Future _checkHosts() async { for (final type in [InternetAddressType.IPv4, InternetAddressType.IPv6]) { final typeStr = type == InternetAddressType.IPv4 ? 'IPv4' : 'IPv6'; try { - final addresses = - await InternetAddress.lookup(host, type: type).timeout(timeLimit); + final addresses = await InternetAddress.lookup( + host, + type: type, + ).timeout(timeLimit); final failed = []; for (final address in addresses) { try { @@ -56,7 +58,8 @@ Future _checkHosts() async { } final successCount = addresses.length - failed.length; print( - '- $host $typeStr resolve and connect succeeded: $successCount / ${addresses.length}'); + '- $host $typeStr resolve and connect succeeded: $successCount / ${addresses.length}', + ); } catch (e) { print('- $host $typeStr lookup failed: $e'); } diff --git a/app/bin/tools/check_drift.dart b/app/bin/tools/check_drift.dart index 2bb425cb53..3156dba279 100644 --- a/app/bin/tools/check_drift.dart +++ b/app/bin/tools/check_drift.dart @@ -74,12 +74,9 @@ Future<_Sample> _sample({ Future<_Item> _singleItem(SearchForm form) async { final params = form.toServiceQuery().toUriQueryParameters(); final rs = await _client.get( - Uri.parse('https://search-dot-dartlang-pub.appspot.com/search').replace( - queryParameters: { - ...params, - 'debug-drift': '1', - }, - ), + Uri.parse( + 'https://search-dot-dartlang-pub.appspot.com/search', + ).replace(queryParameters: {...params, 'debug-drift': '1'}), ); if (rs.statusCode == 200) { final body = json.decode(rs.body) as Map; @@ -90,8 +87,8 @@ Future<_Item> _singleItem(SearchForm form) async { final updatedPackages = (index['updatedPackages'] as List).cast(); final lastUpdated = DateTime.parse(index['lastUpdated'] as String); - final packagesList = - (body['packages'] as List).cast>(); + final packagesList = (body['packages'] as List) + .cast>(); final packages = []; final scores = {}; @@ -119,9 +116,7 @@ class _Sample { final List<_Item>? items; _Diff? _diff; - _Sample({ - this.items, - }); + _Sample({this.items}); int get length => items!.length; _Item get first => items!.first; @@ -189,17 +184,18 @@ class _Diff { for (var j = i + 1; j < items.length; j++) { final item = items[i]; final other = items[j]; - final updatedShared = item.updatedPackages! - .toSet() - .intersection(other.updatedPackages!.toSet()); + final updatedShared = item.updatedPackages!.toSet().intersection( + other.updatedPackages!.toSet(), + ); final allUpdated = { ...item.updatedPackages!, - ...other.updatedPackages! + ...other.updatedPackages!, }; updatedCounts.add(allUpdated.length - updatedShared.length); - final pkgShared = - item.packages!.toSet().intersection(other.packages!.toSet()); + final pkgShared = item.packages!.toSet().intersection( + other.packages!.toSet(), + ); final allPackages = {...item.packages!, ...other.packages!}; packagesCounts.add(allPackages.length - pkgShared.length); @@ -219,7 +215,9 @@ class _Diff { updatedCount: updatedCounts.fold(0, (sum, v) => sum + v) / items.length, updatedDuration: updatedDiffs.fold( - Duration.zero, (mv, d) => mv > d ? mv : d), + Duration.zero, + (mv, d) => mv > d ? mv : d, + ), ); } @@ -227,12 +225,12 @@ class _Diff { hasObservableDifference! || packagesCount! > 0.0 || scoreDiffPct! > 0.0; Map toJson() => { - 'observable': hasObservableDifference, - 'pkg': packagesCount, - 'maxScore': scoreDiffPct, - 'updated': updatedCount, - 'maxDelta': updatedDuration, - }; + 'observable': hasObservableDifference, + 'pkg': packagesCount, + 'maxScore': scoreDiffPct, + 'updated': updatedCount, + 'maxDelta': updatedDuration, + }; } class _FormWithSummary { diff --git a/app/bin/tools/isolate_search_benchmark.dart b/app/bin/tools/isolate_search_benchmark.dart index f71f0c6309..405a8deadb 100644 --- a/app/bin/tools/isolate_search_benchmark.dart +++ b/app/bin/tools/isolate_search_benchmark.dart @@ -24,11 +24,15 @@ final queries = [ ]; Future main(List args) async { - print('Started. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' - 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB'); + print( + 'Started. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' + 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB', + ); final runner = await startSearchIsolate(snapshot: args.first); - print('Loaded. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' - 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB'); + print( + 'Loaded. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' + 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB', + ); for (var i = 0; i < 5; i++) { await _benchmark(runner); @@ -36,8 +40,10 @@ Future main(List args) async { } await runner.close(); - print('Done. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' - 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB'); + print( + 'Done. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' + 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB', + ); } Future _benchmark(IsolateRunner primary) async { @@ -46,25 +52,31 @@ Future _benchmark(IsolateRunner primary) async { for (var i = 0; i < 100; i++) { final random = Random(i); final items = queries - .map((q) => ServiceSearchQuery.parse( - query: q, - tagsPredicate: TagsPredicate.regularSearch(), - )) + .map( + (q) => ServiceSearchQuery.parse( + query: q, + tagsPredicate: TagsPredicate.regularSearch(), + ), + ) .toList(); items.shuffle(random); - await Future.wait(items.map((q) async { - final sw = Stopwatch()..start(); - await index.search(q); - final d = sw.elapsed.inMicroseconds; - durations.putIfAbsent('all', () => []).add(d); - final key = q.parsedQuery.hasFreeText ? 'primary' : 'reduced'; - durations.putIfAbsent(key, () => []).add(d); - })); + await Future.wait( + items.map((q) async { + final sw = Stopwatch()..start(); + await index.search(q); + final d = sw.elapsed.inMicroseconds; + durations.putIfAbsent('all', () => []).add(d); + final key = q.parsedQuery.hasFreeText ? 'primary' : 'reduced'; + durations.putIfAbsent(key, () => []).add(d); + }), + ); } for (final e in durations.entries) { e.value.sort(); - print('${e.key.padLeft(10)}: ' - '${e.value.average.round().toString().padLeft(10)} avg ' - '${e.value[e.value.length * 90 ~/ 100].toString().padLeft(10)} p90'); + print( + '${e.key.padLeft(10)}: ' + '${e.value.average.round().toString().padLeft(10)} avg ' + '${e.value[e.value.length * 90 ~/ 100].toString().padLeft(10)} p90', + ); } } diff --git a/app/bin/tools/public_package_page_checker.dart b/app/bin/tools/public_package_page_checker.dart index 9c95e17f66..67e6c59c97 100644 --- a/app/bin/tools/public_package_page_checker.dart +++ b/app/bin/tools/public_package_page_checker.dart @@ -13,8 +13,12 @@ import 'package:pool/pool.dart'; /// checks for rendering user-generated content (esp. markdown). Future main(List args) async { final argParser = ArgParser() - ..addOption('concurrency', - abbr: 'c', defaultsTo: '1', help: 'Number of concurrent processing.') + ..addOption( + 'concurrency', + abbr: 'c', + defaultsTo: '1', + help: 'Number of concurrent processing.', + ) ..addOption('pub-hosted-url', abbr: 'p', help: 'The PUB_HOSTED_URL to use.') ..addOption('limit', help: 'Stop after N successful checks.') ..addFlag('help', abbr: 'h', defaultsTo: false, help: 'Show help.'); @@ -22,13 +26,15 @@ Future main(List args) async { final argv = argParser.parse(args); if (argv['help'] as bool) { print( - 'Usage: dart public_package_page_checker.dart --pub-hosted-url https://staging-site/ -c 8'); + 'Usage: dart public_package_page_checker.dart --pub-hosted-url https://staging-site/ -c 8', + ); print('Crawls and checks public package pages on pub.dev site'); print(argParser.usage); return; } - final pubHostedUrl = (argv['pub-hosted-url'] as String?) ?? + final pubHostedUrl = + (argv['pub-hosted-url'] as String?) ?? Platform.environment['PUB_HOSTED_URL']; if (pubHostedUrl == null) { print('Missing PUB_HOSTED_URL.'); @@ -40,10 +46,11 @@ Future main(List args) async { final client = httpRetryClient(); try { - final nameRs = - await client.get(Uri.parse('$pubHostedUrl/api/package-names')); - final packages = - ((json.decode(nameRs.body) as Map)['packages'] as List).cast(); + final nameRs = await client.get( + Uri.parse('$pubHostedUrl/api/package-names'), + ); + final packages = ((json.decode(nameRs.body) as Map)['packages'] as List) + .cast(); packages.shuffle(); var count = 0; @@ -51,37 +58,35 @@ Future main(List args) async { final pool = Pool(concurrency); await Future.wait( packages.map( - (p) => pool.withResource( - () async { - if (!running) { - return; - } + (p) => pool.withResource(() async { + if (!running) { + return; + } - Future checkContent(String subPath) async { - final uri = Uri.parse('$pubHostedUrl/packages/$p$subPath'); - print('GET $uri'); - final rs = await client.get(uri); - if (rs.statusCode != 200) { - print('Failed to GET: $uri'); - throw StateError('Failed to GET "$uri".'); - } - return rs.body; + Future checkContent(String subPath) async { + final uri = Uri.parse('$pubHostedUrl/packages/$p$subPath'); + print('GET $uri'); + final rs = await client.get(uri); + if (rs.statusCode != 200) { + print('Failed to GET: $uri'); + throw StateError('Failed to GET "$uri".'); } + return rs.body; + } - final mainBody = await checkContent(''); - if (mainBody.contains('$p/changelog')) { - await checkContent('/changelog'); - } - if (mainBody.contains('$p/example')) { - await checkContent('/example'); - } - count++; - if (running && limit > 0 && count >= limit) { - print('Limit reached, stopping...'); - running = false; - } - }, - ), + final mainBody = await checkContent(''); + if (mainBody.contains('$p/changelog')) { + await checkContent('/changelog'); + } + if (mainBody.contains('$p/example')) { + await checkContent('/example'); + } + count++; + if (running && limit > 0 && count >= limit) { + print('Limit reached, stopping...'); + running = false; + } + }), ), ); } finally { diff --git a/app/bin/tools/sdk_search_benchmark.dart b/app/bin/tools/sdk_search_benchmark.dart index 37b72b8734..7af1897c00 100644 --- a/app/bin/tools/sdk_search_benchmark.dart +++ b/app/bin/tools/sdk_search_benchmark.dart @@ -8,20 +8,18 @@ import 'package:pub_dev/search/sdk_mem_index.dart'; /// Loads a Dart SDK search snapshot and executes queries on it, benchmarking their total time to complete. Future main() async { - print('Started. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' - 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB'); + print( + 'Started. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' + 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB', + ); final index = await createSdkMemIndex(); - print('Loaded. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' - 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB'); + print( + 'Loaded. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' + 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB', + ); // NOTE: please add more queries to this list, especially if there is a performance bottleneck. - final queries = [ - 'chart', - 'json', - 'camera', - 'android camera', - 'sql database', - ]; + final queries = ['chart', 'json', 'camera', 'android camera', 'sql database']; final sw = Stopwatch()..start(); var count = 0; @@ -31,6 +29,8 @@ Future main() async { } sw.stop(); print('${(sw.elapsedMilliseconds / count).toStringAsFixed(2)} ms/request'); - print('Done. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' - 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB'); + print( + 'Done. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' + 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB', + ); } diff --git a/app/bin/tools/search_benchmark.dart b/app/bin/tools/search_benchmark.dart index 1b46ecd3ba..f10b66362d 100644 --- a/app/bin/tools/search_benchmark.dart +++ b/app/bin/tools/search_benchmark.dart @@ -11,12 +11,16 @@ import 'package:pub_dev/search/updater.dart'; /// Loads a search snapshot and executes queries on it, benchmarking their total time to complete. Future main(List args) async { - print('Started. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' - 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB'); + print( + 'Started. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' + 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB', + ); // Assumes that the first argument is a search snapshot file. final index = await loadInMemoryPackageIndexFromFile(args.first); - print('Loaded. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' - 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB'); + print( + 'Loaded. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' + 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB', + ); // NOTE: please add more queries to this list, especially if there is a performance bottleneck. final queries = [ @@ -33,14 +37,18 @@ Future main(List args) async { final sw = Stopwatch()..start(); var count = 0; for (var i = 0; i < 100; i++) { - index.search(ServiceSearchQuery.parse( - query: queries[i % queries.length], - tagsPredicate: TagsPredicate.regularSearch(), - )); + index.search( + ServiceSearchQuery.parse( + query: queries[i % queries.length], + tagsPredicate: TagsPredicate.regularSearch(), + ), + ); count++; } sw.stop(); print('${(sw.elapsedMilliseconds / count).toStringAsFixed(2)} ms/request'); - print('Done. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' - 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB'); + print( + 'Done. Current memory: ${ProcessInfo.currentRss ~/ 1024} KiB, ' + 'max memory: ${ProcessInfo.maxRss ~/ 1024} KiB', + ); } diff --git a/app/lib/account/agent.dart b/app/lib/account/agent.dart index cbbe4790f1..c093f30c2c 100644 --- a/app/lib/account/agent.dart +++ b/app/lib/account/agent.dart @@ -9,8 +9,9 @@ import '../shared/exceptions.dart'; import 'models.dart'; -final _uuidRegExp = - RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'); +final _uuidRegExp = RegExp( + r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', +); /// Agents identify authenticated users or automated system accounts. /// @@ -55,9 +56,7 @@ abstract class KnownAgents { } /// Returns an agentId in the format of `service:gcp-service-account:` - static String gcpServiceAccountAgentId({ - required String oauthUserId, - }) { + static String gcpServiceAccountAgentId({required String oauthUserId}) { return [_gcpServiceAccountPrefix, oauthUserId].join(); } @@ -95,7 +94,9 @@ void checkUserIdParam(String value) { void checkAgentParam(String value) { InvalidInputException.check( - looksLikeUserIdOrServiceAgent(value), 'Invalid "agent".'); + looksLikeUserIdOrServiceAgent(value), + 'Invalid "agent".', + ); } /// An [AuthenticatedAgent] represents an _agent_ (a user or automated service) @@ -155,10 +156,7 @@ class AuthenticatedGitHubAction implements AuthenticatedAgent { /// The parsed, GitHub-specific JWT payload. final GitHubJwtPayload payload; - AuthenticatedGitHubAction({ - required this.idToken, - required this.payload, - }) { + AuthenticatedGitHubAction({required this.idToken, required this.payload}) { _assertRepository(payload.repository); } @@ -169,7 +167,8 @@ class AuthenticatedGitHubAction implements AuthenticatedAgent { void _assertRepository(String repository) { if (repository.trim().isEmpty) { throw AssertionError( - 'The JWT from GitHub must have a non-empty `repository`.'); + 'The JWT from GitHub must have a non-empty `repository`.', + ); } final parts = repository.split('/'); if (parts.length != 2) { @@ -185,8 +184,9 @@ void _assertRepository(String repository) { /// The [agentId] has the following format: `service:gcp-service-account:` class AuthenticatedGcpServiceAccount implements AuthenticatedAgent { @override - late final agentId = - KnownAgents.gcpServiceAccountAgentId(oauthUserId: oauthUserId); + late final agentId = KnownAgents.gcpServiceAccountAgentId( + oauthUserId: oauthUserId, + ); @override String get displayId => email; @@ -220,10 +220,7 @@ class AuthenticatedUser implements AuthenticatedAgent { final User user; final String audience; - AuthenticatedUser( - this.user, { - required this.audience, - }); + AuthenticatedUser(this.user, {required this.audience}); @override String get agentId => user.userId; diff --git a/app/lib/account/auth_provider.dart b/app/lib/account/auth_provider.dart index 2d30350846..3f8f166c6e 100644 --- a/app/lib/account/auth_provider.dart +++ b/app/lib/account/auth_provider.dart @@ -12,9 +12,7 @@ import '../shared/exceptions.dart'; final webmasterScope = wmx.SearchConsoleApi.webmastersReadonlyScope; /// The list of scopes that are allowed in the public API request. -final _allowedScopes = { - webmasterScope, -}; +final _allowedScopes = {webmasterScope}; class AuthResult { final String oauthUserId; @@ -33,9 +31,7 @@ class AuthResult { this.accessToken, }); - AuthResult withToken({ - required String accessToken, - }) { + AuthResult withToken({required String accessToken}) { return AuthResult( oauthUserId: oauthUserId, email: email, @@ -51,10 +47,7 @@ class AccountProfile { final String? name; final String? imageUrl; - AccountProfile({ - required this.name, - required this.imageUrl, - }); + AccountProfile({required this.name, required this.imageUrl}); } /// Authenticates bearer tokens from the `'authentication: bearer'` header. @@ -91,8 +84,9 @@ abstract class AuthProvider { }); /// Calls the Google tokeninfo POST endpoint with [accessToken]. - Future callTokenInfoWithAccessToken( - {required String accessToken}); + Future callTokenInfoWithAccessToken({ + required String accessToken, + }); /// Close resources. Future close(); @@ -105,6 +99,8 @@ void verifyIncludeScopes(List? includeScopes) { } for (final scope in includeScopes) { InvalidInputException.check( - _allowedScopes.contains(scope), 'Invalid scope: "$scope".'); + _allowedScopes.contains(scope), + 'Invalid scope: "$scope".', + ); } } diff --git a/app/lib/account/backend.dart b/app/lib/account/backend.dart index 9c2f9c2cea..526e7d6d37 100644 --- a/app/lib/account/backend.dart +++ b/app/lib/account/backend.dart @@ -85,15 +85,18 @@ Future requireAuthenticatedWebUser() async { /// /// Throws [AuthorizationException] if it doesn't have the permission. Future requireAuthenticatedAdmin( - AdminPermission permission) async { + AdminPermission permission, +) async { final agent = await _requireAuthenticatedAgent(); if (agent is AuthenticatedGcpServiceAccount) { final admin = activeConfiguration.admins!.firstWhereOrNull( - (a) => a.oauthUserId == agent.oauthUserId && a.email == agent.email); + (a) => a.oauthUserId == agent.oauthUserId && a.email == agent.email, + ); final isAdmin = admin != null && admin.permissions.contains(permission); if (!isAdmin) { _logger.warning( - 'Authenticated user (${agent.displayId}) is trying to access unauthorized admin APIs.'); + 'Authenticated user (${agent.displayId}) is trying to access unauthorized admin APIs.', + ); throw AuthorizationException.userIsNotAdminForPubSite(); } return SupportAgent(); @@ -111,7 +114,8 @@ Future requireAuthenticatedClient() async { if (agent is AuthenticatedUser && agent.audience != activeConfiguration.pubClientAudience) { throw AuthenticationException.tokenInvalid( - 'token audience "${agent.audience}" does not match expected value'); + 'token audience "${agent.audience}" does not match expected value', + ); } return agent; } @@ -162,7 +166,8 @@ Future _tryAuthenticateServiceAgent(String token) async { idToken.payload.aud.single != activeConfiguration.externalServiceAudience) { throw AssertionError( - 'authProvider.tryAuthenticateAsServiceToken should not return a parsed token with audience mismatch.'); + 'authProvider.tryAuthenticateAsServiceToken should not return a parsed token with audience mismatch.', + ); } if (idToken.payload.iss == GitHubJwtPayload.issuerUrl) { @@ -170,10 +175,7 @@ Future _tryAuthenticateServiceAgent(String token) async { if (payload == null) { throw AuthenticationException.tokenInvalid('unable to parse payload'); } - return AuthenticatedGitHubAction( - idToken: idToken, - payload: payload, - ); + return AuthenticatedGitHubAction(idToken: idToken, payload: payload); } if (idToken.payload.iss == GcpServiceAccountJwtPayload.issuerUrl) { @@ -181,10 +183,7 @@ Future _tryAuthenticateServiceAgent(String token) async { if (payload == null) { throw AuthenticationException.tokenInvalid('unable to parse payload'); } - return AuthenticatedGcpServiceAccount( - idToken: idToken, - payload: payload, - ); + return AuthenticatedGcpServiceAccount(idToken: idToken, payload: payload); } return null; @@ -193,9 +192,9 @@ Future _tryAuthenticateServiceAgent(String token) async { /// Represents the backend for the account handling and authentication. class AccountBackend { final DatastoreDB _db; - final _emailCache = Cache(Cache.inMemoryCacheProvider(1000)) - .withTTL(Duration(minutes: 10)) - .withCodec(utf8); + final _emailCache = Cache( + Cache.inMemoryCacheProvider(1000), + ).withTTL(Duration(minutes: 10)).withCodec(utf8); AccountBackend(this._db); @@ -212,8 +211,9 @@ class AccountBackend { for (final userId in userIds) { checkUserIdParam(userId); } - final keys = - userIds.map((id) => _db.emptyKey.append(User, id: id)).toList(); + final keys = userIds + .map((id) => _db.emptyKey.append(User, id: id)) + .toList(); return await _db.lookup(keys); } @@ -270,16 +270,18 @@ class AccountBackend { return await fn(); } else { return await ss.fork(() async { - _registerBearerToken(token); - return await fn(); - }) as R; + _registerBearerToken(token); + return await fn(); + }) + as R; } } Future _lookupUserByOauthUserId(String oauthUserId) async { // TODO: This should be cached. final oauthUserMapping = await _db.lookupOrNull( - _db.emptyKey.append(OAuthUserID, id: oauthUserId)); + _db.emptyKey.append(OAuthUserID, id: oauthUserId), + ); if (oauthUserMapping == null) { return null; } @@ -307,12 +309,15 @@ class AccountBackend { /// Returns an [User] entity for the authenticated service account. /// This method should be used only by admin agents. Future userForServiceAccount( - AuthenticatedGcpServiceAccount authenticatedAgent) async { - final user = await _lookupOrCreateUserByOauthUserId(AuthResult( - oauthUserId: authenticatedAgent.oauthUserId, - email: authenticatedAgent.email, - audience: authenticatedAgent.audience, - )); + AuthenticatedGcpServiceAccount authenticatedAgent, + ) async { + final user = await _lookupOrCreateUserByOauthUserId( + AuthResult( + oauthUserId: authenticatedAgent.oauthUserId, + email: authenticatedAgent.email, + audience: authenticatedAgent.audience, + ), + ); if (user == null) { throw AuthenticationException.failed(); } @@ -341,14 +346,12 @@ class AccountBackend { } return await withRetryTransaction(_db, (tx) async { - final oauthUserIdKey = emptyKey.append( - OAuthUserID, - id: auth.oauthUserId, - ); + final oauthUserIdKey = emptyKey.append(OAuthUserID, id: auth.oauthUserId); // Check that the user doesn't exist in this transaction. - final oauthUserMapping = - await tx.lookupOrNull(oauthUserIdKey); + final oauthUserMapping = await tx.lookupOrNull( + oauthUserIdKey, + ); if (oauthUserMapping != null) { // If the user does exist we can just return it. return await tx.lookupValue( @@ -365,10 +368,10 @@ class AccountBackend { // Notice, that we're doing this outside the transaction, but these are // legacy users, we should avoid creation of new users with only emails // as this lookup is eventually consistent. - final usersWithEmail = await (_db.query() - ..filter('email =', auth.email)) - .run() - .toList(); + final usersWithEmail = + await (_db.query()..filter('email =', auth.email)) + .run() + .toList(); if (usersWithEmail.length == 1 && usersWithEmail.single.oauthUserId == null && !usersWithEmail.single.isDeleted) { @@ -419,17 +422,17 @@ class AccountBackend { /// Updates an existing or creates a new client session for pre-authorization /// secrets and post-authorization user information. - Future createOrUpdateClientSession({ - String? sessionId, - }) async { + Future createOrUpdateClientSession({String? sessionId}) async { final now = clock.now().toUtc(); - final oldSession = - sessionId == null ? null : await lookupValidUserSession(sessionId); + final oldSession = sessionId == null + ? null + : await lookupValidUserSession(sessionId); // try to update old session first if (oldSession != null) { final rs = await withRetryTransaction(_db, (tx) async { - final session = - await tx.userSessions.lookupOrNull(oldSession.sessionId); + final session = await tx.userSessions.lookupOrNull( + oldSession.sessionId, + ); if (session == null) { return null; } @@ -457,7 +460,8 @@ class AccountBackend { }) async { final now = clock.now().toUtc(); final info = await authProvider.callTokenInfoWithAccessToken( - accessToken: profile.accessToken ?? ''); + accessToken: profile.accessToken ?? '', + ); final user = await _lookupOrCreateUserByOauthUserId(profile); if (user == null || user.isModerated || user.isDeleted) { throw AuthenticationException.failed(); @@ -538,8 +542,10 @@ class AccountBackend { if (user == null || user.isModerated || user.isDeleted) { return null; } - return AuthenticatedUser(user, - audience: activeConfiguration.pubServerAudience!); + return AuthenticatedUser( + user, + audience: activeConfiguration.pubServerAudience!, + ); } /// Returns the user session associated with the [sessionId] or null if it @@ -611,8 +617,9 @@ class AccountBackend { required String? note, }) async { await withRetryTransaction(_db, (tx) async { - final user = - await tx.lookupOrNull(_db.emptyKey.append(User, id: userId)); + final user = await tx.lookupOrNull( + _db.emptyKey.append(User, id: userId), + ); if (user == null) throw NotFoundException.resource('User:$userId'); user.updateIsModerated( @@ -651,9 +658,7 @@ class AccountBackend { } /// Purge [cache] entries for given [userId]. -Future purgeAccountCache({ - required String userId, -}) async { +Future purgeAccountCache({required String userId}) async { await Future.wait([ cache.userPackageLikes(userId).purgeAndRepeat(), cache.publisherPage(userId).purgeAndRepeat(), diff --git a/app/lib/account/consent_backend.dart b/app/lib/account/consent_backend.dart index 09e41fde54..abab7149f5 100644 --- a/app/lib/account/consent_backend.dart +++ b/app/lib/account/consent_backend.dart @@ -85,7 +85,9 @@ class ConsentBackend { /// Resolves the consent. Future resolveConsent( - String consentId, api.ConsentResult result) async { + String consentId, + api.ConsentResult result, + ) async { InvalidInputException.checkUlid(consentId, 'consentId'); final authenticatedUser = await requireAuthenticatedWebUser(); final user = authenticatedUser.user; @@ -143,7 +145,9 @@ class ConsentBackend { return await _sendNotification(activeAgent.displayId, existing); } else { return api.InviteStatus( - emailSent: false, nextNotification: existing.nextNotification); + emailSent: false, + nextNotification: existing.nextNotification, + ); } } // Create a new entry. @@ -153,10 +157,7 @@ class ConsentBackend { kind: kind, args: args, ); - await _db.commit(inserts: [ - consent, - auditLogRecord, - ]); + await _db.commit(inserts: [consent, auditLogRecord]); await dedupCacheEntry.set(consent.consentId); return await _sendNotification(activeAgent.displayId, consent); }); @@ -194,7 +195,10 @@ class ConsentBackend { kind: ConsentKind.publisherContact, args: [publisherId, contactEmail], auditLogRecord: await AuditLogRecord.publisherContactInvited( - user: user, publisherId: publisherId, contactEmail: contactEmail), + user: user, + publisherId: publisherId, + contactEmail: contactEmail, + ), ); } @@ -218,15 +222,19 @@ class ConsentBackend { } Future _sendNotification( - String activeUserEmail, Consent consent) async { + String activeUserEmail, + Consent consent, + ) async { final invitedEmail = consent.email!; final action = _actions[consent.kind]!; - final email = emailBackend.prepareEntity(createInviteEmail( - invitedEmail: invitedEmail, - subject: action.renderEmailSubject(consent.args!), - inviteText: action.renderInviteText(activeUserEmail, consent.args!), - consentUrl: consentUrl(consent.consentId), - )); + final email = emailBackend.prepareEntity( + createInviteEmail( + invitedEmail: invitedEmail, + subject: action.renderEmailSubject(consent.args!), + inviteText: action.renderInviteText(activeUserEmail, consent.args!), + consentUrl: consentUrl(consent.consentId), + ), + ); final status = await withRetryTransaction(_db, (tx) async { final c = await tx.lookupValue(consent.key); c.notificationCount++; @@ -234,7 +242,9 @@ class ConsentBackend { tx.insert(c); tx.insert(email); return api.InviteStatus( - emailSent: true, nextNotification: c.nextNotification); + emailSent: true, + nextNotification: c.nextNotification, + ); }); await emailBackend.trySendOutgoingEmail(email); return status; @@ -249,7 +259,9 @@ class ConsentBackend { await _delete(entry, (a) => a.onExpire(entry)); } catch (e) { _logger.shout( - 'Delete failed: ${entry.consentId} ${entry.kind} ${entry.args}', e); + 'Delete failed: ${entry.consentId} ${entry.kind} ${entry.args}', + e, + ); } } } @@ -264,8 +276,9 @@ class ConsentBackend { final action = _actions[c.kind]!; if (!action.permitConfirmationWithOtherEmail && c.email != null) { InvalidInputException.check( - c.email?.toLowerCase() == user.email?.toLowerCase(), - 'This invitation is not for the user account currently logged in.'); + c.email?.toLowerCase() == user.email?.toLowerCase(), + 'This invitation is not for the user account currently logged in.', + ); } return c; } @@ -275,16 +288,13 @@ class ConsentBackend { Future Function(ConsentAction action) fn, ) async { final action = _actions[consent.kind]; - await retry( - () async { - if (action != null) await fn(action); - await withRetryTransaction(_db, (tx) async { - final c = await tx.lookupOrNull(consent.key); - if (c != null) tx.delete(c.key); - }); - }, - maxAttempts: 3, - ); + await retry(() async { + if (action != null) await fn(action); + await withRetryTransaction(_db, (tx) async { + final c = await tx.lookupOrNull(consent.key); + if (c != null) tx.delete(c.key); + }); + }, maxAttempts: 3); } } @@ -339,7 +349,8 @@ class _PackageUploaderAction extends ConsentAction { final currentUser = await requireAuthenticatedWebUser(); if (currentUser.email?.toLowerCase() != consent.email?.toLowerCase()) { throw NotAcceptableException( - 'Current user and consent user does not match.'); + 'Current user and consent user does not match.', + ); } await packageBackend.confirmUploader( @@ -353,12 +364,14 @@ class _PackageUploaderAction extends ConsentAction { Future onReject(Consent consent, User? user) async { final packageName = consent.args![0]; await withRetryTransaction(_db, (tx) async { - tx.insert(await AuditLogRecord.uploaderInviteRejected( - fromAgent: consent.fromAgent, - package: packageName, - uploaderEmail: user?.email ?? consent.email!, - userId: user?.userId, - )); + tx.insert( + await AuditLogRecord.uploaderInviteRejected( + fromAgent: consent.fromAgent, + package: packageName, + uploaderEmail: user?.email ?? consent.email!, + userId: user?.userId, + ), + ); }); } @@ -366,11 +379,13 @@ class _PackageUploaderAction extends ConsentAction { Future onExpire(Consent consent) async { final packageName = consent.args![0]; await withRetryTransaction(_db, (tx) async { - tx.insert(await AuditLogRecord.uploaderInviteExpired( - fromAgent: consent.fromAgent, - package: packageName, - uploaderEmail: consent.email!, - )); + tx.insert( + await AuditLogRecord.uploaderInviteExpired( + fromAgent: consent.fromAgent, + package: packageName, + uploaderEmail: consent.email!, + ), + ); }); } @@ -421,13 +436,15 @@ class _PublisherContactAction extends ConsentAction { Future onReject(Consent consent, User? user) async { final publisherId = consent.args![0]; await withRetryTransaction(_db, (tx) async { - tx.insert(await AuditLogRecord.publisherContactInviteRejected( - fromAgent: consent.fromAgent, - publisherId: publisherId, - contactEmail: consent.email!, - userEmail: user?.email, - userId: user?.userId, - )); + tx.insert( + await AuditLogRecord.publisherContactInviteRejected( + fromAgent: consent.fromAgent, + publisherId: publisherId, + contactEmail: consent.email!, + userEmail: user?.email, + userId: user?.userId, + ), + ); }); } @@ -435,11 +452,13 @@ class _PublisherContactAction extends ConsentAction { Future onExpire(Consent consent) async { final publisherId = consent.args![0]; await withRetryTransaction(_db, (tx) async { - tx.insert(await AuditLogRecord.publisherContactInviteExpired( - fromAgent: consent.fromAgent, - publisherId: publisherId, - contactEmail: consent.email!, - )); + tx.insert( + await AuditLogRecord.publisherContactInviteExpired( + fromAgent: consent.fromAgent, + publisherId: publisherId, + contactEmail: consent.email!, + ), + ); }); } @@ -503,12 +522,14 @@ class _PublisherMemberAction extends ConsentAction { Future onReject(Consent consent, User? user) async { final publisherId = consent.args![0]; await withRetryTransaction(_db, (tx) async { - tx.insert(await AuditLogRecord.publisherMemberInviteRejected( - fromAgent: consent.fromAgent, - publisherId: publisherId, - memberEmail: user?.email ?? consent.email!, - userId: user?.userId, - )); + tx.insert( + await AuditLogRecord.publisherMemberInviteRejected( + fromAgent: consent.fromAgent, + publisherId: publisherId, + memberEmail: user?.email ?? consent.email!, + userId: user?.userId, + ), + ); }); } @@ -516,11 +537,13 @@ class _PublisherMemberAction extends ConsentAction { Future onExpire(Consent consent) async { final publisherId = consent.args![0]; await withRetryTransaction(_db, (tx) async { - tx.insert(await AuditLogRecord.publisherMemberInviteExpired( - fromAgent: consent.fromAgent, - publisherId: publisherId, - memberEmail: consent.email!, - )); + tx.insert( + await AuditLogRecord.publisherMemberInviteExpired( + fromAgent: consent.fromAgent, + publisherId: publisherId, + memberEmail: consent.email!, + ), + ); }); } diff --git a/app/lib/account/default_auth_provider.dart b/app/lib/account/default_auth_provider.dart index 3d1c7f21f7..7d3007fd08 100644 --- a/app/lib/account/default_auth_provider.dart +++ b/app/lib/account/default_auth_provider.dart @@ -52,31 +52,35 @@ class DefaultAuthProvider extends BaseAuthProvider { } @override - Future callTokenInfoWithIdToken( - {required String idToken}) async { + Future callTokenInfoWithIdToken({ + required String idToken, + }) async { // Hit the token-info end-point documented at: // https://developers.google.com/identity/sign-in/web/backend-auth // Note: ideally, we would verify these JWTs locally, but unfortunately // we don't have a solid RSA implementation available in Dart. - final u = - _tokenInfoEndPoint.replace(queryParameters: {'id_token': idToken}); + final u = _tokenInfoEndPoint.replace( + queryParameters: {'id_token': idToken}, + ); return await _httpClient.get(u, headers: {'accept': 'application/json'}); } @override - Future callGetUserinfo( - {required String accessToken}) async { + Future callGetUserinfo({ + required String accessToken, + }) async { final authClient = auth.authenticatedClient( - _httpClient, - auth.AccessCredentials( - auth.AccessToken( - 'Bearer', - accessToken, - clock.now().toUtc().add(Duration(minutes: 20)), // avoid refresh - ), - null, - [], - )); + _httpClient, + auth.AccessCredentials( + auth.AccessToken( + 'Bearer', + accessToken, + clock.now().toUtc().add(Duration(minutes: 20)), // avoid refresh + ), + null, + [], + ), + ); final oauth2 = oauth2_v2.Oauth2Api(authClient); return await oauth2.userinfo.get(); @@ -228,16 +232,19 @@ abstract class BaseAuthProvider extends AuthProvider { JsonWebToken idToken, { required Future Function() openIdDataFetch, }) async { - final isValidTimestamp = - idToken.payload.isTimely(threshold: Duration(minutes: 2)); + final isValidTimestamp = idToken.payload.isTimely( + threshold: Duration(minutes: 2), + ); if (!isValidTimestamp) { throw AuthenticationException.tokenInvalid('invalid timestamps'); } - final aud = - idToken.payload.aud.length == 1 ? idToken.payload.aud.single : null; + final aud = idToken.payload.aud.length == 1 + ? idToken.payload.aud.single + : null; if (aud != activeConfiguration.externalServiceAudience) { throw AuthenticationException.tokenInvalid( - 'audience "${idToken.payload.aud}" does not match "${activeConfiguration.externalServiceAudience}"'); + 'audience "${idToken.payload.aud}" does not match "${activeConfiguration.externalServiceAudience}"', + ); } final signatureMatches = await verifyTokenSignature( token: idToken, @@ -268,12 +275,14 @@ abstract class BaseAuthProvider extends AuthProvider { if (_isLikelyAccessToken(token)) { // If this is most likely an access_token, we try access_token first, // and if not a valid access_token we try it as a JWT: - result = await _tryAuthenticateAccessToken(token) ?? + result = + await _tryAuthenticateAccessToken(token) ?? await _tryAuthenticateJwt(token); } else { // If this is not likely to be an access_token, we try JWT first, // and if not valid JWT we try it as access_token: - result = await _tryAuthenticateJwt(token) ?? + result = + await _tryAuthenticateJwt(token) ?? await _tryAuthenticateAccessToken(token); } @@ -304,8 +313,10 @@ abstract class BaseAuthProvider extends AuthProvider { final audience = info.audience; if (audience == null) { - _logger.warning('OAuth2 access attempted with invalid audience, ' - 'for email: "${info.email}", audience: "${info.audience}"'); + _logger.warning( + 'OAuth2 access attempted with invalid audience, ' + 'for email: "${info.email}", audience: "${info.audience}"', + ); return null; } diff --git a/app/lib/account/like_backend.dart b/app/lib/account/like_backend.dart index 902c0eeacc..f6b5dc22f0 100644 --- a/app/lib/account/like_backend.dart +++ b/app/lib/account/like_backend.dart @@ -57,8 +57,9 @@ class LikeBackend { throw NotFoundException.resource(package); } - final key = - _db.emptyKey.append(User, id: user.id).append(Like, id: package); + final key = _db.emptyKey + .append(User, id: user.id) + .append(Like, id: package); final oldLike = await tx.lookupOrNull(key); if (oldLike != null) { @@ -90,8 +91,9 @@ class LikeBackend { throw NotFoundException.resource(package); } - final likeKey = - _db.emptyKey.append(User, id: user.id).append(Like, id: package); + final likeKey = _db.emptyKey + .append(User, id: user.id) + .append(Like, id: package); final like = await tx.lookupOrNull(likeKey); if (like == null) { diff --git a/app/lib/account/models.dart b/app/lib/account/models.dart index f9fd6708c2..b910e7085e 100644 --- a/app/lib/account/models.dart +++ b/app/lib/account/models.dart @@ -406,11 +406,12 @@ String consentDedupId({ required String? email, required String? kind, required List args, -}) => - [fromAgentId, email, kind, ...args] - .nonNulls - .map(Uri.encodeComponent) - .join('/'); +}) => [ + fromAgentId, + email, + kind, + ...args, +].nonNulls.map(Uri.encodeComponent).join('/'); abstract class UserModeratedReason { static const bot = 'bot'; diff --git a/app/lib/account/models.g.dart b/app/lib/account/models.g.dart index f71f982924..48d8086167 100644 --- a/app/lib/account/models.g.dart +++ b/app/lib/account/models.g.dart @@ -7,35 +7,35 @@ part of 'models.dart'; // ************************************************************************** LikeData _$LikeDataFromJson(Map json) => LikeData( - userId: json['userId'] as String?, - package: json['package'] as String?, - created: json['created'] == null - ? null - : DateTime.parse(json['created'] as String), - ); + userId: json['userId'] as String?, + package: json['package'] as String?, + created: json['created'] == null + ? null + : DateTime.parse(json['created'] as String), +); Map _$LikeDataToJson(LikeData instance) => { - 'userId': instance.userId, - 'package': instance.package, - 'created': instance.created?.toIso8601String(), - }; + 'userId': instance.userId, + 'package': instance.package, + 'created': instance.created?.toIso8601String(), +}; SessionData _$SessionDataFromJson(Map json) => SessionData( - sessionId: json['sessionId'] as String, - userId: json['userId'] as String?, - email: json['email'] as String?, - name: json['name'] as String?, - imageUrl: json['imageUrl'] as String?, - created: DateTime.parse(json['created'] as String), - expires: DateTime.parse(json['expires'] as String), - authenticatedAt: json['authenticatedAt'] == null - ? null - : DateTime.parse(json['authenticatedAt'] as String), - csrfToken: json['csrfToken'] as String?, - grantedScopes: (json['grantedScopes'] as List?) - ?.map((e) => e as String) - .toList(), - ); + sessionId: json['sessionId'] as String, + userId: json['userId'] as String?, + email: json['email'] as String?, + name: json['name'] as String?, + imageUrl: json['imageUrl'] as String?, + created: DateTime.parse(json['created'] as String), + expires: DateTime.parse(json['expires'] as String), + authenticatedAt: json['authenticatedAt'] == null + ? null + : DateTime.parse(json['authenticatedAt'] as String), + csrfToken: json['csrfToken'] as String?, + grantedScopes: (json['grantedScopes'] as List?) + ?.map((e) => e as String) + .toList(), +); Map _$SessionDataToJson(SessionData instance) => { diff --git a/app/lib/account/session_cookie.dart b/app/lib/account/session_cookie.dart index c3189b091c..e6b5e4b646 100644 --- a/app/lib/account/session_cookie.dart +++ b/app/lib/account/session_cookie.dart @@ -45,15 +45,15 @@ Map createClientSessionCookie({ /// Parses the cookie values and returns the status of client session cookies. ClientSessionCookieStatus parseClientSessionCookies( - Map cookies) { + Map cookies, +) { final lax = cookies[clientSessionLaxCookieName]?.trim() ?? ''; final strict = cookies[clientSessionStrictCookieName]?.trim() ?? ''; - final needsReset = (lax.isEmpty && strict.isNotEmpty) || + final needsReset = + (lax.isEmpty && strict.isNotEmpty) || (lax.isNotEmpty && strict.isNotEmpty && lax != strict); if (needsReset) { - throw AuthenticationException.cookieInvalid( - headers: clearSessionCookies(), - ); + throw AuthenticationException.cookieInvalid(headers: clearSessionCookies()); } if (lax.isEmpty) { return ClientSessionCookieStatus.missing(); @@ -92,14 +92,9 @@ class ClientSessionCookieStatus { final bool isStrict; final String? sessionId; - ClientSessionCookieStatus.missing() - : isStrict = false, - sessionId = null; + ClientSessionCookieStatus.missing() : isStrict = false, sessionId = null; - ClientSessionCookieStatus({ - required this.sessionId, - required this.isStrict, - }); + ClientSessionCookieStatus({required this.sessionId, required this.isStrict}); bool get isPresent => sessionId?.isNotEmpty ?? false; } diff --git a/app/lib/admin/actions/actions.dart b/app/lib/admin/actions/actions.dart index 0d6ecc965b..d158d13ea7 100644 --- a/app/lib/admin/actions/actions.dart +++ b/app/lib/admin/actions/actions.dart @@ -71,9 +71,8 @@ final class AdminAction { /// Returns a JSON response, a failed invocation should throw a /// [ResponseException]. /// Any other exception will be considered an internal error. - final Future> Function( - Map arguments, - ) invoke; + final Future> Function(Map arguments) + invoke; AdminAction({ required this.name, @@ -87,8 +86,9 @@ final class AdminAction { throw ArgumentError.value(name, 'name'); } // Check that the keys for options works as command-line options - if (options.keys - .any((k) => !RegExp(r'^[a-z][a-z0-9-]{0,128}$').hasMatch(k))) { + if (options.keys.any( + (k) => !RegExp(r'^[a-z][a-z0-9-]{0,128}$').hasMatch(k), + )) { throw ArgumentError.value(options, 'options'); } } diff --git a/app/lib/admin/actions/download_counts_backfill.dart b/app/lib/admin/actions/download_counts_backfill.dart index ca60653c49..a83d03c764 100644 --- a/app/lib/admin/actions/download_counts_backfill.dart +++ b/app/lib/admin/actions/download_counts_backfill.dart @@ -13,7 +13,8 @@ final downloadCountsBackfill = AdminAction( 'sync-days': 'the number days to sync starting from "date" and going back', }, summary: 'Sync download counts data from and days back', - description: ''' + description: + ''' This action will trigger syncing of the download counts backend with download counts data from and days going back. For instance, given 3 sync days and the date '2024-05-29', data will be synced for '2024-05-27', @@ -36,12 +37,15 @@ $defaultNumberOfSyncDays days. int? parsedSyncDays; if (syncDays != null) { parsedSyncDays = int.tryParse(syncDays); - InvalidInputException.check(parsedSyncDays != null && parsedSyncDays > 0, - 'invalid sync-days, must be a positive integer'); + InvalidInputException.check( + parsedSyncDays != null && parsedSyncDays > 0, + 'invalid sync-days, must be a positive integer', + ); } await syncDownloadCounts( - date: parsedDate, - numberOfSyncDays: parsedSyncDays ?? defaultNumberOfSyncDays); + date: parsedDate, + numberOfSyncDays: parsedSyncDays ?? defaultNumberOfSyncDays, + ); return {'message': 'syncing of download counts has been triggered.'}; }, diff --git a/app/lib/admin/actions/download_counts_delete.dart b/app/lib/admin/actions/download_counts_delete.dart index 3b5cfaf004..ea72238683 100644 --- a/app/lib/admin/actions/download_counts_delete.dart +++ b/app/lib/admin/actions/download_counts_delete.dart @@ -15,12 +15,14 @@ This action will delete all "DownloadCount" entities. The entities can be restored using the "backfill-download-counts" admin action. ''', invoke: (options) async { - final result = await dbService - .deleteWithQuery(dbService.query()); + final result = await dbService.deleteWithQuery( + dbService.query(), + ); return { - 'message': 'Found ${result.found} "DownloadCount" entities and ' - 'deleted ${result.deleted} entities' + 'message': + 'Found ${result.found} "DownloadCount" entities and ' + 'deleted ${result.deleted} entities', }; }, ); diff --git a/app/lib/admin/actions/email_send.dart b/app/lib/admin/actions/email_send.dart index 97739432df..ceaa6207a1 100644 --- a/app/lib/admin/actions/email_send.dart +++ b/app/lib/admin/actions/email_send.dart @@ -32,7 +32,8 @@ The `to` argument is comma separated list of: The list of resolved emails will be deduplicated. ''', options: { - 'to': 'A comma separated list of email addresses or subjects ' + 'to': + 'A comma separated list of email addresses or subjects ' '(the recipients of the messages).', 'cc': '(optional) same as "to" with addresses that will be CC-d.', 'from': 'The email address to impersonate (`support@pub.dev` by default).', @@ -42,8 +43,8 @@ The list of resolved emails will be deduplicated. '(optional) A comma separated list of email addresses to be used in the Reply-To header.', 'in-reply-to': '(optional) The local message id of the email that this is a reply to ' - '(e.g. moderation case id). The email sender will the `In-Reply-To` and `References` ' - 'headers with the `@pub.dev` value, referencing an earlier `Message-Id`.', + '(e.g. moderation case id). The email sender will the `In-Reply-To` and `References` ' + 'headers with the `@pub.dev` value, referencing an earlier `Message-Id`.', }, invoke: (options) async { final emailSubject = options['subject']; @@ -73,17 +74,19 @@ The list of resolved emails will be deduplicated. final ccEmailList = cc == null ? null : await _resolveEmails(cc); try { - await emailSender.sendMessage(EmailMessage( - localMessageId: createUuid(), - EmailAddress(from), - emailList.map((v) => EmailAddress(v)).toList(), - emailSubject!, - emailBody!, - ccRecipients: - ccEmailList?.map((v) => EmailAddress(v)).toList() ?? const [], - inReplyToLocalMessageId: inReplyTo, - replyTos: replyTo?.split(',') ?? const [], - )); + await emailSender.sendMessage( + EmailMessage( + localMessageId: createUuid(), + EmailAddress(from), + emailList.map((v) => EmailAddress(v)).toList(), + emailSubject!, + emailBody!, + ccRecipients: + ccEmailList?.map((v) => EmailAddress(v)).toList() ?? const [], + inReplyToLocalMessageId: inReplyTo, + replyTos: replyTo?.split(',') ?? const [], + ), + ); return { 'emails': emailList, if (ccEmailList != null) 'ccEmails': ccEmailList, @@ -121,14 +124,16 @@ Future> _resolveEmails(String value) async { final list = await publisherBackend.getAdminMemberEmails(publisher); emails.addAll(list.nonNulls); } else { - final list = await accountBackend - .lookupUsersById(pkg.uploaders ?? const []); + final list = await accountBackend.lookupUsersById( + pkg.uploaders ?? const [], + ); emails.addAll(list.map((e) => e?.email).nonNulls); } break; case ModerationSubjectKind.publisher: - final list = - await publisherBackend.getAdminMemberEmails(ms.publisherId!); + final list = await publisherBackend.getAdminMemberEmails( + ms.publisherId!, + ); emails.addAll(list.nonNulls); break; case ModerationSubjectKind.user: diff --git a/app/lib/admin/actions/exported_api_sync.dart b/app/lib/admin/actions/exported_api_sync.dart index b595c60664..24bc160a23 100644 --- a/app/lib/admin/actions/exported_api_sync.dart +++ b/app/lib/admin/actions/exported_api_sync.dart @@ -29,21 +29,17 @@ Optionally, write rewrite of all files using: }, invoke: (options) async { final forceWriteString = options['force-write'] ?? 'false'; - InvalidInputException.checkAnyOf( - forceWriteString, - 'force-write', - ['true', 'false'], - ); + InvalidInputException.checkAnyOf(forceWriteString, 'force-write', [ + 'true', + 'false', + ]); final forceWrite = forceWriteString == 'true'; final packagesOpt = options['packages'] ?? ''; final syncAll = packagesOpt == 'ALL'; if (syncAll) { await apiExporter.synchronizeExportedApi(forceWrite: forceWrite); - return { - 'packages': 'ALL', - 'forceWrite': forceWrite, - }; + return {'packages': 'ALL', 'forceWrite': forceWrite}; } else { final packages = packagesOpt.split(' ').map((p) => p.trim()).toList(); for (final p in packages) { @@ -52,10 +48,7 @@ Optionally, write rewrite of all files using: for (final p in packages) { await apiExporter.synchronizePackage(p, forceWrite: forceWrite); } - return { - 'packages': packages, - 'forceWrite': forceWrite, - }; + return {'packages': packages, 'forceWrite': forceWrite}; } }, ); diff --git a/app/lib/admin/actions/merge_moderated_package_into_existing.dart b/app/lib/admin/actions/merge_moderated_package_into_existing.dart index bc996dc8fd..f41efe827b 100644 --- a/app/lib/admin/actions/merge_moderated_package_into_existing.dart +++ b/app/lib/admin/actions/merge_moderated_package_into_existing.dart @@ -30,16 +30,21 @@ Fails if that package has no existing Package entity. 'The name of ModeratedPackage to merge into its existing Package.', }, invoke: (args) async { - final packageName = args['package'] ?? + final packageName = + args['package'] ?? (throw InvalidInputException('Missing --package argument.')); await withRetryTransaction(dbService, (tx) async { // check ModeratedPackage existence - final mpKey = - dbService.emptyKey.append(ModeratedPackage, id: packageName); + final mpKey = dbService.emptyKey.append( + ModeratedPackage, + id: packageName, + ); final mp = await tx.lookupOrNull(mpKey); InvalidInputException.check( - mp != null, 'ModeratedPackage does not exists.'); + mp != null, + 'ModeratedPackage does not exists.', + ); // check Package existence final pKey = dbService.emptyKey.append(Package, id: packageName); @@ -64,9 +69,6 @@ Fails if that package has no existing Package entity. }); await triggerPackagePostUpdates(packageName).future; - return { - 'package': packageName, - 'merged': true, - }; + return {'package': packageName, 'merged': true}; }, ); diff --git a/app/lib/admin/actions/moderate_package.dart b/app/lib/admin/actions/moderate_package.dart index a083645d15..4fe16f499f 100644 --- a/app/lib/admin/actions/moderate_package.dart +++ b/app/lib/admin/actions/moderate_package.dart @@ -26,7 +26,7 @@ Note: the action may take a longer time to complete as the public archive bucket 'package': 'The package name to be moderated', 'state': 'Set moderated state true / false. Returns current state if omitted.', - 'note': 'Optional note to store (internal).' + 'note': 'Optional note to store (internal).', }, invoke: (options) async { final caseId = options['case']; @@ -35,8 +35,8 @@ Note: the action may take a longer time to complete as the public archive bucket final state = options['state']; final note = options['note']; - final refCase = - await adminBackend.loadAndVerifyModerationCaseForAdminAction(caseId); + final refCase = await adminBackend + .loadAndVerifyModerationCaseForAdminAction(caseId); return await adminMarkPackageVisibility( package, @@ -65,6 +65,7 @@ Note: the action may take a longer time to complete as the public archive bucket /// Changes the moderated or the admin-deleted flag and timestamp on a [package]. Future> adminMarkPackageVisibility( String? package, { + /// `true`, `false` or `null` required String? state, @@ -73,7 +74,8 @@ Future> adminMarkPackageVisibility( TransactionWrapper tx, Package v, bool valueToSet, - ) whenUpdating, + ) + whenUpdating, /// The debug information to return. required Map Function(Package v) valueFn, diff --git a/app/lib/admin/actions/moderate_package_versions.dart b/app/lib/admin/actions/moderate_package_versions.dart index dc3c558411..fe4b342420 100644 --- a/app/lib/admin/actions/moderate_package_versions.dart +++ b/app/lib/admin/actions/moderate_package_versions.dart @@ -30,7 +30,7 @@ Set the moderated flag on a package version (updating the flag and the timestamp 'version': 'The version to be moderated', 'state': 'Set moderated state true / false. Returns current state if omitted.', - 'note': 'Optional note to store (internal).' + 'note': 'Optional note to store (internal).', }, invoke: (options) async { final caseId = options['case']; @@ -40,8 +40,8 @@ Set the moderated flag on a package version (updating the flag and the timestamp final state = options['state']; final note = options['note']; - final refCase = - await adminBackend.loadAndVerifyModerationCaseForAdminAction(caseId); + final refCase = await adminBackend + .loadAndVerifyModerationCaseForAdminAction(caseId); return await adminMarkPackageVersionVisibility( package, @@ -72,6 +72,7 @@ Set the moderated flag on a package version (updating the flag and the timestamp Future> adminMarkPackageVersionVisibility( String? package, String? version, { + /// `true`, `false` or `null` required String? state, @@ -80,7 +81,8 @@ Future> adminMarkPackageVersionVisibility( TransactionWrapper tx, PackageVersion v, bool valueToSet, - ) whenUpdating, + ) + whenUpdating, /// The debug information to return. required Map Function(PackageVersion v) valueFn, @@ -116,9 +118,11 @@ Future> adminMarkPackageVersionVisibility( PackageVersion? pv2; if (valueToSet != null) { final currentDartSdk = await getCachedDartSdkVersion( - lastKnownStable: toolStableDartSdkVersion); + lastKnownStable: toolStableDartSdkVersion, + ); final currentFlutterSdk = await getCachedFlutterSdkVersion( - lastKnownStable: toolStableFlutterSdkVersion); + lastKnownStable: toolStableFlutterSdkVersion, + ); pv2 = await withRetryTransaction(dbService, (tx) async { final v = await tx.lookupValue(pv.key); await whenUpdating(tx, v, valueToSet!); diff --git a/app/lib/admin/actions/moderate_publisher.dart b/app/lib/admin/actions/moderate_publisher.dart index 02d379b14c..b89d77586c 100644 --- a/app/lib/admin/actions/moderate_publisher.dart +++ b/app/lib/admin/actions/moderate_publisher.dart @@ -25,7 +25,7 @@ can't be updated, administrators must not be able to update publisher options. 'publisher': 'The publisherId to be moderated', 'state': 'Set moderated state true / false. Returns current state if omitted.', - 'note': 'Optional note to store (internal).' + 'note': 'Optional note to store (internal).', }, invoke: (options) async { final caseId = options['case']; @@ -38,7 +38,9 @@ can't be updated, administrators must not be able to update publisher options. final publisher = await publisherBackend.lookupPublisher(publisherId!); InvalidInputException.check( - publisher != null, 'Unable to locate publisher.'); + publisher != null, + 'Unable to locate publisher.', + ); final state = options['state']; bool? valueToSet; @@ -53,8 +55,8 @@ can't be updated, administrators must not be able to update publisher options. final note = options['note']; - final refCase = - await adminBackend.loadAndVerifyModerationCaseForAdminAction(caseId); + final refCase = await adminBackend + .loadAndVerifyModerationCaseForAdminAction(caseId); Publisher? publisher2; if (valueToSet != null) { diff --git a/app/lib/admin/actions/moderate_user.dart b/app/lib/admin/actions/moderate_user.dart index 4cebe49bf7..130b1f8147 100644 --- a/app/lib/admin/actions/moderate_user.dart +++ b/app/lib/admin/actions/moderate_user.dart @@ -34,7 +34,7 @@ The active web sessions of the user will be expired. 'The reason for user moderation. One of ${UserModeratedReason.values.join(', ')}.', 'state': 'Set moderated state true / false. Returns current state if omitted.', - 'note': 'Optional note to store (internal).' + 'note': 'Optional note to store (internal).', }, invoke: (options) async { final caseId = options['case']; @@ -48,16 +48,18 @@ The active web sessions of the user will be expired. final moderatedReason = options['reason']; final note = options['note']; - final refCase = - await adminBackend.loadAndVerifyModerationCaseForAdminAction(caseId); + final refCase = await adminBackend + .loadAndVerifyModerationCaseForAdminAction(caseId); User? user; if (looksLikeUserId(userIdOrEmail!)) { user = await accountBackend.lookupUserById(userIdOrEmail); } else { final users = await accountBackend.lookupUsersByEmail(userIdOrEmail); - InvalidInputException.check(users.length == 1, - 'Expected a single User, got ${users.length}: ${users.map((e) => e.userId).join(', ')}.'); + InvalidInputException.check( + users.length == 1, + 'Expected a single User, got ${users.length}: ${users.map((e) => e.userId).join(', ')}.', + ); user = users.single; } InvalidInputException.check(user != null, 'Unable to locate user.'); @@ -85,8 +87,9 @@ The active web sessions of the user will be expired. user2 = await accountBackend.lookupUserById(user.userId); if (valueToSet) { - await for (final p - in packageBackend.streamPackagesWhereUserIsUploader(user.userId)) { + await for (final p in packageBackend.streamPackagesWhereUserIsUploader( + user.userId, + )) { await withRetryTransaction(dbService, (tx) async { final key = dbService.emptyKey.append(Package, id: p); final pkg = await tx.lookupOrNull(key); @@ -99,8 +102,9 @@ The active web sessions of the user will be expired. }); } - final publishers = - await publisherBackend.listPublishersForUser(user.userId); + final publishers = await publisherBackend.listPublishersForUser( + user.userId, + ); for (final e in publishers.publishers!) { final p = await publisherBackend.lookupPublisher(e.publisherId); if (p == null) { @@ -108,8 +112,9 @@ The active web sessions of the user will be expired. } // Only restrict publishers where the user was a single active admin. // Note: at this point the User.isModerated flag is already set. - final members = - await publisherBackend.listPublisherMembers(e.publisherId); + final members = await publisherBackend.listPublisherMembers( + e.publisherId, + ); var nonBlockedCount = 0; for (final member in members) { final mu = await accountBackend.lookupUserById(member.userId); diff --git a/app/lib/admin/actions/moderation_case_create.dart b/app/lib/admin/actions/moderation_case_create.dart index feab6fe2f1..ee396dc2f1 100644 --- a/app/lib/admin/actions/moderation_case_create.dart +++ b/app/lib/admin/actions/moderation_case_create.dart @@ -24,7 +24,7 @@ Returns the fields on the newly created moderation case. 'source': 'The source of the moderation case. (default value: `trusted-flagger`)', 'subject': 'The subject of the moderation case.', - 'url': 'The url of the moderation case (optional).' + 'url': 'The url of the moderation case (optional).', }, invoke: (options) async { final reporterEmail = options['reporter-email'] ?? KnownAgents.pubSupport; @@ -35,11 +35,15 @@ Returns the fields on the newly created moderation case. final kind = options['kind'] ?? ModerationKind.notification; InvalidInputException.check( - ModerationKind.isValidKind(kind), 'invalid kind'); + ModerationKind.isValidKind(kind), + 'invalid kind', + ); final source = options['source'] ?? ModerationSource.trustedFlagger; InvalidInputException.check( - ModerationSource.isValidSource(source), 'invalid source'); + ModerationSource.isValidSource(source), + 'invalid source', + ); final subject = options['subject']; InvalidInputException.check( diff --git a/app/lib/admin/actions/moderation_case_delete.dart b/app/lib/admin/actions/moderation_case_delete.dart index b3716b69f4..2d43f8ada1 100644 --- a/app/lib/admin/actions/moderation_case_delete.dart +++ b/app/lib/admin/actions/moderation_case_delete.dart @@ -13,9 +13,7 @@ final moderationCaseDelete = AdminAction( description: ''' Deletes a moderation case. ''', - options: { - 'case': 'The caseId to be deleted.', - }, + options: {'case': 'The caseId to be deleted.'}, invoke: (options) async { final caseId = options['case']; InvalidInputException.check( @@ -25,7 +23,8 @@ Deletes a moderation case. await withRetryTransaction(dbService, (tx) async { final mc = await tx.lookupOrNull( - dbService.emptyKey.append(ModerationCase, id: caseId)); + dbService.emptyKey.append(ModerationCase, id: caseId), + ); if (mc != null) { tx.delete(mc.key); } else { @@ -33,8 +32,6 @@ Deletes a moderation case. } }); - return { - 'deleted': true, - }; + return {'deleted': true}; }, ); diff --git a/app/lib/admin/actions/moderation_case_info.dart b/app/lib/admin/actions/moderation_case_info.dart index 3e63e4f779..df2da0eabe 100644 --- a/app/lib/admin/actions/moderation_case_info.dart +++ b/app/lib/admin/actions/moderation_case_info.dart @@ -12,9 +12,7 @@ final moderationCaseInfo = AdminAction( description: ''' Loads and displays the moderation case information. ''', - options: { - 'case': 'The caseId to be loaded.', - }, + options: {'case': 'The caseId to be loaded.'}, invoke: (options) async { final caseId = options['case']; InvalidInputException.check( diff --git a/app/lib/admin/actions/moderation_case_list.dart b/app/lib/admin/actions/moderation_case_list.dart index 3f183ab786..7497bfbd81 100644 --- a/app/lib/admin/actions/moderation_case_list.dart +++ b/app/lib/admin/actions/moderation_case_list.dart @@ -17,13 +17,14 @@ List ModerationCase entities with filter options. ''', options: { 'sort': 'Sort by the given attribute: `opened` (default), `resolved`.', - 'status': '`pending` | `resolved` | ' + 'status': + '`pending` | `resolved` | ' '${ModerationStatus.resolveValues.map((v) => '`$v`').join(' | ')}', 'kind': '`appeal` | `notification`', 'subject': 'The (substring of) the subject on the moderation case.', 'density': '`caseIds` (default) | `compact` | `expanded`', 'past': - 'Limit the results opened (or resolved depending on `sort`) using "2w" or other time ranges.' + 'Limit the results opened (or resolved depending on `sort`) using "2w" or other time ranges.', }, invoke: (options) async { final sort = options['sort'] ?? 'opened'; diff --git a/app/lib/admin/actions/moderation_case_resolve.dart b/app/lib/admin/actions/moderation_case_resolve.dart index 48a8029a1d..30fe0d511a 100644 --- a/app/lib/admin/actions/moderation_case_resolve.dart +++ b/app/lib/admin/actions/moderation_case_resolve.dart @@ -17,16 +17,20 @@ Closes the moderation case and updates the status based on the actions logged on ''', options: { 'case': 'The caseId to be closed.', - 'status': 'The resolved status of the case ' + 'status': + 'The resolved status of the case ' '(optional, will be automatically inferred if absent). ' 'One of: ${ModerationStatus.resolveValues.join(', ')}.', - 'grounds': 'The grounds for the moderation actions ' + 'grounds': + 'The grounds for the moderation actions ' '(if moderation action was taken). ' 'One of: ${ModerationGrounds.resolveValues.join(', ')}.', - 'violation': 'The high-level category of the violation reason ' + 'violation': + 'The high-level category of the violation reason ' '(if moderation action was taken). ' 'One of: ${ModerationViolation.violationValues.join(', ')}.', - 'reason': 'The text from SOR statement sent to the user ' + 'reason': + 'The text from SOR statement sent to the user ' '(if moderation action was taken).', }, invoke: (options) async { @@ -51,7 +55,8 @@ Closes the moderation case and updates the status based on the actions logged on final mc = await withRetryTransaction(dbService, (tx) async { final mc = await tx.lookupOrNull( - dbService.emptyKey.append(ModerationCase, id: caseId!)); + dbService.emptyKey.append(ModerationCase, id: caseId!), + ); if (mc == null) { throw NotFoundException.resource(caseId); } @@ -69,11 +74,12 @@ Closes the moderation case and updates the status based on the actions logged on ? ModerationStatus.moderationApplied : ModerationStatus.noAction; } else if (mc.kind == ModerationKind.appeal) { - final appealedCase = await tx.lookupValue(dbService - .emptyKey - .append(ModerationCase, id: mc.appealedCaseId!)); - final appealHadModeratedAction = - appealedCase.getActionLog().isNotEmpty; + final appealedCase = await tx.lookupValue( + dbService.emptyKey.append(ModerationCase, id: mc.appealedCaseId!), + ); + final appealHadModeratedAction = appealedCase + .getActionLog() + .isNotEmpty; if (appealHadModeratedAction) { status = hasModeratedAction ? ModerationStatus.moderationReverted @@ -107,9 +113,13 @@ Closes the moderation case and updates the status based on the actions logged on InvalidInputException.checkStringLength(reason, 'reason', minimum: 10); } else { InvalidInputException.check( - grounds == ModerationGrounds.none, '"grounds" must be `none`'); - InvalidInputException.check(violation == ModerationViolation.none, - '"violation" must be `none`'); + grounds == ModerationGrounds.none, + '"grounds" must be `none`', + ); + InvalidInputException.check( + violation == ModerationViolation.none, + '"violation" must be `none`', + ); InvalidInputException.checkNull(reason, 'reason'); } diff --git a/app/lib/admin/actions/moderation_transparency_metrics.dart b/app/lib/admin/actions/moderation_transparency_metrics.dart index dea76138be..63009d3895 100644 --- a/app/lib/admin/actions/moderation_transparency_metrics.dart +++ b/app/lib/admin/actions/moderation_transparency_metrics.dart @@ -78,10 +78,9 @@ required for the transparency metrics report. violations.increment(mc.violation ?? ''); sources.increment(mc.source); - final hasUserRestriction = mc - .getActionLog() - .entries - .any((e) => ModerationSubject.tryParse(e.subject)!.isUser); + final hasUserRestriction = mc.getActionLog().entries.any( + (e) => ModerationSubject.tryParse(e.subject)!.isUser, + ); // If actions resulted in a user being blocked, then we count it as // "provision", even if packages were also removed. // Reasoning that it's natural that blocking a user would also @@ -209,32 +208,23 @@ required for the transparency metrics report. ['Number of actions taken, by detection method', ''], [ 'Automated detection', - sources[ModerationSource.automatedDetection] ?? 0 + sources[ModerationSource.automatedDetection] ?? 0, ], [ 'Non-automated detection', sources.entries .where((e) => e.key != ModerationSource.automatedDetection) .map((e) => e.value) - .fold(0, (a, b) => a + b) + .fold(0, (a, b) => a + b), ], ['Number of actions taken, by type of restriction applied', ''], - [ - 'Restrictions of Visibility', - restrictions['visibility'] ?? 0, - ], - [ - 'Restrictions of Monetisation', - restrictions['monetisation'] ?? 0, - ], + ['Restrictions of Visibility', restrictions['visibility'] ?? 0], + ['Restrictions of Monetisation', restrictions['monetisation'] ?? 0], [ 'Restrictions of Provision of the Service', restrictions['provision'] ?? 0, ], - [ - 'Restrictions of an Account', - restrictions['account'] ?? 0, - ], + ['Restrictions of an Account', restrictions['account'] ?? 0], // --------------------------------------- ['Complaints received through internal complaint handling systems', ''], @@ -243,18 +233,9 @@ required for the transparency metrics report. ['CONTENT_ACCOUNT_OWNER_APPEAL', contentOwnerAppealCount], ['REPORTER_APPEAL', totalAppealCount - contentOwnerAppealCount], ['Number of complaints received, by outcome', ''], - [ - 'Initial decision upheld', - appealOutcomes['upheld'] ?? 0, - ], - [ - 'Initial decision reversed', - appealOutcomes['reverted'] ?? 0, - ], - [ - 'Decision omitted', - appealOutcomes['omitted'] ?? 0, - ], + ['Initial decision upheld', appealOutcomes['upheld'] ?? 0], + ['Initial decision reversed', appealOutcomes['reverted'] ?? 0], + ['Decision omitted', appealOutcomes['omitted'] ?? 0], [ 'Median time to action a complaint (days)', appealMedianTimeToActionDays, @@ -290,9 +271,7 @@ required for the transparency metrics report. 'outcomes': appealOutcomes, 'medianTimeToActionDays': appealMedianTimeToActionDays, }, - 'users': { - 'suspensions': reasonCounts, - } + 'users': {'suspensions': reasonCounts}, }; }, ); @@ -310,14 +289,16 @@ String toCsV(List> data) { if (value is int) { sb.write(value); } else if (value is String) { - final mustEscape = value.contains(',') || + final mustEscape = + value.contains(',') || value.contains('"') || value.contains('\r') || value.contains('\n'); sb.write(mustEscape ? '"${value.replaceAll('"', '""')}"' : value); } else { throw UnimplementedError( - 'Unhandled CSV type: ${value.runtimeType}/$value'); + 'Unhandled CSV type: ${value.runtimeType}/$value', + ); } } sb.write('\r\n'); diff --git a/app/lib/admin/actions/package_info.dart b/app/lib/admin/actions/package_info.dart index b7bb15ff83..2f04399754 100644 --- a/app/lib/admin/actions/package_info.dart +++ b/app/lib/admin/actions/package_info.dart @@ -13,9 +13,7 @@ final packageInfo = AdminAction( description: ''' Loads and displays the package information. ''', - options: { - 'package': 'The package to be loaded.', - }, + options: {'package': 'The package to be loaded.'}, invoke: (options) async { final package = options['package']; InvalidInputException.check( @@ -31,9 +29,9 @@ Loads and displays the package information. final uploaderIds = p.uploaders; List? uploaderEmails; if (uploaderIds != null) { - uploaderEmails = (await accountBackend.getEmailsOfUserIds(uploaderIds)) - .nonNulls - .toList(); + uploaderEmails = (await accountBackend.getEmailsOfUserIds( + uploaderIds, + )).nonNulls.toList(); } return { diff --git a/app/lib/admin/actions/package_invite_uploader.dart b/app/lib/admin/actions/package_invite_uploader.dart index 8b8b223966..2533a80d10 100644 --- a/app/lib/admin/actions/package_invite_uploader.dart +++ b/app/lib/admin/actions/package_invite_uploader.dart @@ -20,10 +20,12 @@ Sends an invite to to become uploader of . 'email': 'email to send invitation to', }, invoke: (options) async { - final packageName = options['package'] ?? + final packageName = + options['package'] ?? (throw InvalidInputException('Missing --package argument.')); - final invitedEmail = options['email'] ?? + final invitedEmail = + options['email'] ?? (throw InvalidInputException('Missing --email argument.')); final package = await packageBackend.lookupPackage(packageName); @@ -32,22 +34,30 @@ Sends an invite to to become uploader of . } if (package.publisherId != null) { throw OperationForbiddenException.publisherOwnedPackageNoUploader( - packageName, package.publisherId!); + packageName, + package.publisherId!, + ); } - final authenticatedAgent = - await requireAuthenticatedAdmin(AdminPermission.invokeAction); + final authenticatedAgent = await requireAuthenticatedAdmin( + AdminPermission.invokeAction, + ); final inviteStatus = await consentBackend.invitePackageUploader( - packageName: packageName, - uploaderEmail: invitedEmail, - agent: authenticatedAgent); - - final uploaderUsers = - await accountBackend.lookupUsersById(package.uploaders!); - final isNotUploaderYet = - !uploaderUsers.any((u) => u!.email == invitedEmail); + packageName: packageName, + uploaderEmail: invitedEmail, + agent: authenticatedAgent, + ); + + final uploaderUsers = await accountBackend.lookupUsersById( + package.uploaders!, + ); + final isNotUploaderYet = !uploaderUsers.any( + (u) => u!.email == invitedEmail, + ); InvalidInputException.check( - isNotUploaderYet, '`$invitedEmail` is already an uploader.'); + isNotUploaderYet, + '`$invitedEmail` is already an uploader.', + ); return { 'message': 'Invited user', diff --git a/app/lib/admin/actions/package_latest_update.dart b/app/lib/admin/actions/package_latest_update.dart index 2099a4b983..8b9bb0a8f7 100644 --- a/app/lib/admin/actions/package_latest_update.dart +++ b/app/lib/admin/actions/package_latest_update.dart @@ -25,15 +25,12 @@ When no package is specified, all packages will be updated. if (package != null) { final updated = await packageBackend.updatePackageVersions(package); - return { - 'updated': updated, - }; + return {'updated': updated}; } else { final stat = await packageBackend.updateAllPackageVersions( - concurrency: concurrency); - return { - 'updatedCount': stat, - }; + concurrency: concurrency, + ); + return {'updatedCount': stat}; } }, ); diff --git a/app/lib/admin/actions/package_reservation_create.dart b/app/lib/admin/actions/package_reservation_create.dart index f06cce1d75..b31c903024 100644 --- a/app/lib/admin/actions/package_reservation_create.dart +++ b/app/lib/admin/actions/package_reservation_create.dart @@ -23,7 +23,7 @@ able to claim it. ''', options: { 'package': 'The package name to be reserved.', - 'emails': 'The list of email addresses, separated by comma.' + 'emails': 'The list of email addresses, separated by comma.', }, invoke: (options) async { final package = options['package']; @@ -44,7 +44,8 @@ able to claim it. final entry = await withRetryTransaction(dbService, (tx) async { final existing = await tx.lookupOrNull( - dbService.emptyKey.append(ReservedPackage, id: package)); + dbService.emptyKey.append(ReservedPackage, id: package), + ); final entry = existing ?? ReservedPackage.init(package); entry.emails = {...?emails}.toList(); tx.insert(entry); diff --git a/app/lib/admin/actions/package_reservation_delete.dart b/app/lib/admin/actions/package_reservation_delete.dart index bf163c5ce5..6d6cc6be7a 100644 --- a/app/lib/admin/actions/package_reservation_delete.dart +++ b/app/lib/admin/actions/package_reservation_delete.dart @@ -13,9 +13,7 @@ final packageReservationDelete = AdminAction( description: ''' Deletes a ReservedPackage entity, allowing the package name use by any user. ''', - options: { - 'package': 'The package reservation to be deleted.', - }, + options: {'package': 'The package reservation to be deleted.'}, invoke: (options) async { final package = options['package']; InvalidInputException.check( diff --git a/app/lib/admin/actions/package_reservation_list.dart b/app/lib/admin/actions/package_reservation_list.dart index 35d6c92610..65be5eb332 100644 --- a/app/lib/admin/actions/package_reservation_list.dart +++ b/app/lib/admin/actions/package_reservation_list.dart @@ -20,10 +20,7 @@ Returns the list of all ReservedPackage entities and the allowed emails. return { 'packages': list - .map((rp) => { - 'name': rp.name, - 'emails': rp.emails, - }) + .map((rp) => {'name': rp.name, 'emails': rp.emails}) .toList(), }; }, diff --git a/app/lib/admin/actions/package_version_retraction.dart b/app/lib/admin/actions/package_version_retraction.dart index 6c7795f6ff..5d9b4c32e2 100644 --- a/app/lib/admin/actions/package_version_retraction.dart +++ b/app/lib/admin/actions/package_version_retraction.dart @@ -9,14 +9,14 @@ import '../../package/models.dart'; import '../../shared/datastore.dart'; final packageVersionRetraction = AdminAction( - name: 'package-version-retraction', - options: { - 'package': 'name of the package', - 'version': 'version to update', - 'set-retracted': 'true/false, whether to retract the package version', - }, - summary: 'Update retraction status for a package version', - description: ''' + name: 'package-version-retraction', + options: { + 'package': 'name of the package', + 'version': 'version to update', + 'set-retracted': 'true/false, whether to retract the package version', + }, + summary: 'Update retraction status for a package version', + description: ''' This action will view/update retraction status for a package version. The `package` option specifies which package, and the `version` option specifies which version. @@ -25,73 +25,71 @@ If the `set-retracted` option is not specified, this will return current status. Otherwise, it will set the package version as retracted depending on the value of `set-retracted`, which should either be `true` or `false`. ''', - invoke: (args) async { - final packageName = args['package']; - if (packageName == null) { - throw InvalidInputException('Missing --package argument.'); - } - final version = args['version']; - if (version == null) { - throw InvalidInputException('Missing --version argument.'); - } - final isRetracted = switch (args['set-retracted']) { - 'true' => true, - 'false' => false, - null => null, - _ => throw InvalidInputException('Invalid --set-retracted'), - }; + invoke: (args) async { + final packageName = args['package']; + if (packageName == null) { + throw InvalidInputException('Missing --package argument.'); + } + final version = args['version']; + if (version == null) { + throw InvalidInputException('Missing --version argument.'); + } + final isRetracted = switch (args['set-retracted']) { + 'true' => true, + 'false' => false, + null => null, + _ => throw InvalidInputException('Invalid --set-retracted'), + }; - final pkg = await packageBackend.lookupPackage(packageName); - if (pkg == null) { - throw NotFoundException.resource(packageName); - } + final pkg = await packageBackend.lookupPackage(packageName); + if (pkg == null) { + throw NotFoundException.resource(packageName); + } + + final packageVersion = await packageBackend.lookupPackageVersion( + packageName, + version, + ); + if (packageVersion == null) { + throw NotFoundException.resource(version); + } + final before = { + 'package': packageVersion.package, + 'version': packageVersion.version, + 'isRetracted': packageVersion.isRetracted, + }; + if (isRetracted == null) { + return {'before': before}; + } - final packageVersion = - await packageBackend.lookupPackageVersion(packageName, version); - if (packageVersion == null) { + final versionKey = pkg.key.append(PackageVersion, id: version); + final after = await withRetryTransaction(dbService, (tx) async { + final p = await tx.lookupValue(pkg.key); + final pv = await tx.lookupOrNull(versionKey); + if (pv == null) { throw NotFoundException.resource(version); } - final before = { - 'package': packageVersion.package, - 'version': packageVersion.version, - 'isRetracted': packageVersion.isRetracted, - }; - if (isRetracted == null) { - return { - 'before': before, - }; + if (pv.isNotVisible) { + throw ModeratedException.packageVersion(packageName, version); } - final versionKey = pkg.key.append(PackageVersion, id: version); - final after = await withRetryTransaction(dbService, (tx) async { - final p = await tx.lookupValue(pkg.key); - final pv = await tx.lookupOrNull(versionKey); - if (pv == null) { - throw NotFoundException.resource(version); - } - if (pv.isNotVisible) { - throw ModeratedException.packageVersion(packageName, version); - } - - if (isRetracted != pv.isRetracted) { - await packageBackend.doUpdateRetractedStatus( - SupportAgent(), - tx, - p, - pv, - isRetracted, - ); - } - return { - 'package': pv.package, - 'version': pv.version, - 'isRetracted': pv.isRetracted, - }; - }); - triggerPackagePostUpdates(packageName); - + if (isRetracted != pv.isRetracted) { + await packageBackend.doUpdateRetractedStatus( + SupportAgent(), + tx, + p, + pv, + isRetracted, + ); + } return { - 'before': before, - 'after': after, + 'package': pv.package, + 'version': pv.version, + 'isRetracted': pv.isRetracted, }; }); + triggerPackagePostUpdates(packageName); + + return {'before': before, 'after': after}; + }, +); diff --git a/app/lib/admin/actions/publisher_create.dart b/app/lib/admin/actions/publisher_create.dart index 44b0928158..ccabae5074 100644 --- a/app/lib/admin/actions/publisher_create.dart +++ b/app/lib/admin/actions/publisher_create.dart @@ -11,60 +11,61 @@ import 'package:pub_dev/publisher/models.dart'; import 'package:pub_dev/shared/datastore.dart'; final publisherCreate = AdminAction( - name: 'publisher-create', - options: { - 'publisher': 'name of publisher to create', - 'member-email': 'email of user to add', - }, - summary: - 'Creates a new publisher and adds as the single member.', - description: ''' + name: 'publisher-create', + options: { + 'publisher': 'name of publisher to create', + 'member-email': 'email of user to add', + }, + summary: + 'Creates a new publisher and adds as the single member.', + description: ''' Creates a new publisher and adds as the single member. This should generally only be done with PM approval as it skips actual domain verification. ''', - invoke: (args) async { - final publisherId = args['publisher']; - final userEmail = args['member-email']; - if (publisherId == null) { - throw InvalidInputException('Missing --publisher argument.'); - } - if (userEmail == null) { - throw InvalidInputException('Missing --member-email argument.'); - } - final users = await accountBackend.lookupUsersByEmail(userEmail); - if (users.isEmpty) { - throw InvalidInputException('Unknown user: $userEmail'); - } - if (users.length > 1) { - throw InvalidInputException('more than one user: $userEmail'); - } - final user = users.single; + invoke: (args) async { + final publisherId = args['publisher']; + final userEmail = args['member-email']; + if (publisherId == null) { + throw InvalidInputException('Missing --publisher argument.'); + } + if (userEmail == null) { + throw InvalidInputException('Missing --member-email argument.'); + } + final users = await accountBackend.lookupUsersByEmail(userEmail); + if (users.isEmpty) { + throw InvalidInputException('Unknown user: $userEmail'); + } + if (users.length > 1) { + throw InvalidInputException('more than one user: $userEmail'); + } + final user = users.single; - // Create the publisher - final now = clock.now().toUtc(); - await withRetryTransaction(dbService, (tx) async { - final key = dbService.emptyKey.append(Publisher, id: publisherId); - final p = await tx.lookupOrNull(key); - if (p != null) { - // Check that publisher is the same as what we would create. - if (p.created!.isBefore(now.subtract(Duration(minutes: 10))) || - p.updated!.isBefore(now.subtract(Duration(minutes: 10))) || - p.contactEmail != user.email || - p.description != '' || - p.websiteUrl != defaultPublisherWebsite(publisherId)) { - throw ConflictException.publisherAlreadyExists(publisherId); - } - // Avoid creating the same publisher again, this end-point is idempotent - // if we just do nothing here. - return { - 'message': 'Publisher already exists.', - 'publisherId': publisherId, - }; + // Create the publisher + final now = clock.now().toUtc(); + await withRetryTransaction(dbService, (tx) async { + final key = dbService.emptyKey.append(Publisher, id: publisherId); + final p = await tx.lookupOrNull(key); + if (p != null) { + // Check that publisher is the same as what we would create. + if (p.created!.isBefore(now.subtract(Duration(minutes: 10))) || + p.updated!.isBefore(now.subtract(Duration(minutes: 10))) || + p.contactEmail != user.email || + p.description != '' || + p.websiteUrl != defaultPublisherWebsite(publisherId)) { + throw ConflictException.publisherAlreadyExists(publisherId); } + // Avoid creating the same publisher again, this end-point is idempotent + // if we just do nothing here. + return { + 'message': 'Publisher already exists.', + 'publisherId': publisherId, + }; + } - // Create publisher - tx.queueMutations(inserts: [ + // Create publisher + tx.queueMutations( + inserts: [ Publisher.init( parentKey: dbService.emptyKey, publisherId: publisherId, @@ -81,11 +82,13 @@ This should generally only be done with PM approval as it skips actual domain ve user: user, publisherId: publisherId, ), - ]); - }); - return { - 'message': 'Publisher created.', - 'publisherId': publisherId, - 'member-email': userEmail, - }; + ], + ); }); + return { + 'message': 'Publisher created.', + 'publisherId': publisherId, + 'member-email': userEmail, + }; + }, +); diff --git a/app/lib/admin/actions/publisher_delete.dart b/app/lib/admin/actions/publisher_delete.dart index 78c9eea217..b06752225e 100644 --- a/app/lib/admin/actions/publisher_delete.dart +++ b/app/lib/admin/actions/publisher_delete.dart @@ -8,12 +8,10 @@ import 'package:pub_dev/publisher/models.dart'; import 'package:pub_dev/shared/datastore.dart'; final publisherDelete = AdminAction( - name: 'publisher-delete', - options: { - 'publisher': 'name of publisher to delete', - }, - summary: 'Deletes publisher .', - description: ''' + name: 'publisher-delete', + options: {'publisher': 'name of publisher to delete'}, + summary: 'Deletes publisher .', + description: ''' Deletes publisher . The publisher must have no packages. If not the operation will fail. @@ -22,39 +20,41 @@ All member-info will be lost. The publisher can be regenerated later (no tombstoning). ''', - invoke: (args) async { - final publisherId = args['publisher']; - if (publisherId == null) { - throw InvalidInputException('Missing `publisher` argument'); + invoke: (args) async { + final publisherId = args['publisher']; + if (publisherId == null) { + throw InvalidInputException('Missing `publisher` argument'); + } + + final packagesQuery = dbService.query() + ..filter('publisherId =', publisherId); + final packages = await packagesQuery.run().toList(); + if (packages.isNotEmpty) { + throw NotAcceptableException( + 'Publisher "$publisherId" cannot be deleted, as it has package(s): ' + '${packages.map((e) => e.name!).join(', ')}.', + ); + } + + int? memberCount; + await withRetryTransaction(dbService, (tx) async { + final key = dbService.emptyKey.append(Publisher, id: publisherId); + final publisher = await tx.lookupOrNull(key); + final membersQuery = tx.query(key); + final members = await membersQuery.run().toList(); + memberCount = members.length; + if (publisher != null) { + tx.delete(key); } - - final packagesQuery = dbService.query() - ..filter('publisherId =', publisherId); - final packages = await packagesQuery.run().toList(); - if (packages.isNotEmpty) { - throw NotAcceptableException( - 'Publisher "$publisherId" cannot be deleted, as it has package(s): ' - '${packages.map((e) => e.name!).join(', ')}.'); + for (final m in members) { + tx.delete(m.key); } - - int? memberCount; - await withRetryTransaction(dbService, (tx) async { - final key = dbService.emptyKey.append(Publisher, id: publisherId); - final publisher = await tx.lookupOrNull(key); - final membersQuery = tx.query(key); - final members = await membersQuery.run().toList(); - memberCount = members.length; - if (publisher != null) { - tx.delete(key); - } - for (final m in members) { - tx.delete(m.key); - } - }); - - return { - 'message': 'Publisher and all members deleted.', - 'publisherId': publisherId, - 'members-count': memberCount, - }; }); + + return { + 'message': 'Publisher and all members deleted.', + 'publisherId': publisherId, + 'members-count': memberCount, + }; + }, +); diff --git a/app/lib/admin/actions/publisher_info.dart b/app/lib/admin/actions/publisher_info.dart index 2d996ac5ba..ba54752134 100644 --- a/app/lib/admin/actions/publisher_info.dart +++ b/app/lib/admin/actions/publisher_info.dart @@ -12,9 +12,7 @@ final publisherInfo = AdminAction( description: ''' Loads and displays the publisher information. ''', - options: { - 'publisher': 'The publisherId to be loaded.', - }, + options: {'publisher': 'The publisherId to be loaded.'}, invoke: (options) async { final publisherId = options['publisher']; InvalidInputException.check( diff --git a/app/lib/admin/actions/publisher_member_invite.dart b/app/lib/admin/actions/publisher_member_invite.dart index 30d7b8afe7..a64e6fe7ff 100644 --- a/app/lib/admin/actions/publisher_member_invite.dart +++ b/app/lib/admin/actions/publisher_member_invite.dart @@ -21,10 +21,12 @@ Sends an invite to to become a member of . 'email': 'email to send invitation to', }, invoke: (options) async { - final publisherId = options['publisher'] ?? + final publisherId = + options['publisher'] ?? (throw InvalidInputException('Missing --publisher argument.')); - final invitedEmail = options['email'] ?? + final invitedEmail = + options['email'] ?? (throw InvalidInputException('Missing --email argument.')); final publisher = await publisherBackend.lookupPublisher(publisherId); @@ -32,11 +34,14 @@ Sends an invite to to become a member of . throw NotFoundException.resource(publisherId); } - final authenticatedAgent = - await requireAuthenticatedAdmin(AdminPermission.invokeAction); + final authenticatedAgent = await requireAuthenticatedAdmin( + AdminPermission.invokeAction, + ); await publisherBackend.verifyPublisherMemberInvite( - publisherId, InviteMemberRequest(email: invitedEmail)); + publisherId, + InviteMemberRequest(email: invitedEmail), + ); await consentBackend.invitePublisherMember( authenticatedAgent: authenticatedAgent, publisherId: publisherId, diff --git a/app/lib/admin/actions/publisher_members_list.dart b/app/lib/admin/actions/publisher_members_list.dart index de870b3401..75290a4aab 100644 --- a/app/lib/admin/actions/publisher_members_list.dart +++ b/app/lib/admin/actions/publisher_members_list.dart @@ -11,11 +11,10 @@ final publisherMembersList = AdminAction( description: ''' Get information about a publisher and list all its members. ''', - options: { - 'publisher': 'Publisher for which to list members, eg `dart.dev`', - }, + options: {'publisher': 'Publisher for which to list members, eg `dart.dev`'}, invoke: (options) async { - final publisherId = options['publisher'] ?? + final publisherId = + options['publisher'] ?? (throw InvalidInputException('Missing --publisher argument.')); final publisher = await publisherBackend.lookupPublisher(publisherId); @@ -31,11 +30,7 @@ Get information about a publisher and list all its members. 'contact': publisher.contactEmail, 'created': publisher.created?.toIso8601String(), 'members': members - .map((m) => { - 'email': m.email, - 'role': m.role, - 'userId': m.userId, - }) + .map((m) => {'email': m.email, 'role': m.role, 'userId': m.userId}) .toList(), }; }, diff --git a/app/lib/admin/actions/publisher_package_remove.dart b/app/lib/admin/actions/publisher_package_remove.dart index 668a17c4a2..32cf530468 100644 --- a/app/lib/admin/actions/publisher_package_remove.dart +++ b/app/lib/admin/actions/publisher_package_remove.dart @@ -12,13 +12,13 @@ import '../../publisher/backend.dart'; import '../../shared/datastore.dart'; final publisherPackageRemove = AdminAction( - name: 'publisher-package-remove', - options: { - 'package': 'name of the package to remove from its current publisher' - }, - summary: - 'Removes from its current publisher, and makes that publisher\'s owners uploaders', - description: ''' + name: 'publisher-package-remove', + options: { + 'package': 'name of the package to remove from its current publisher', + }, + summary: + 'Removes from its current publisher, and makes that publisher\'s owners uploaders', + description: ''' Removes from its current publisher, and makes that publisher\'s owners uploaders. @@ -32,45 +32,47 @@ the publisher and assign a@example.com and b@example.com as uploaders. If the publisher has no members, the package will end up without uploaders. ''', - invoke: (args) async { - final packageName = args['package']; - if (packageName == null) { - throw InvalidInputException('The argument package must be given'); - } - final package = (await packageBackend.lookupPackage(packageName))!; - final currentPublisherId = package.publisherId; - if (currentPublisherId == null) { - throw NotAcceptableException( - 'Package $packageName is not currently in a publisher'); - } - final currentPublisherMembers = - (await publisherBackend.listPublisherMembers(currentPublisherId)); + invoke: (args) async { + final packageName = args['package']; + if (packageName == null) { + throw InvalidInputException('The argument package must be given'); + } + final package = (await packageBackend.lookupPackage(packageName))!; + final currentPublisherId = package.publisherId; + if (currentPublisherId == null) { + throw NotAcceptableException( + 'Package $packageName is not currently in a publisher', + ); + } + final currentPublisherMembers = (await publisherBackend + .listPublisherMembers(currentPublisherId)); - await withRetryTransaction(dbService, (tx) async { - final pkg = await tx.lookupValue(package.key); - pkg.publisherId = null; - pkg.uploaders = currentPublisherMembers.map((e) => e.userId).toList(); - pkg.updated = clock.now().toUtc(); - tx.insert(pkg); - tx.insert( - await AuditLogRecord.packageRemovedFromPublisher( - package: packageName, - fromPublisherId: currentPublisherId, - ), - ); - }); - triggerPackagePostUpdates(packageName, - skipReanalysis: true, skipVersionsExport: true); - await purgePublisherCache(publisherId: currentPublisherId); - return { - 'previousPublisher': currentPublisherId, - 'package': package.name, - 'uploaders': [ - for (final member in currentPublisherMembers) - { - 'email': member.email, - 'userId': member.userId, - } - ], - }; + await withRetryTransaction(dbService, (tx) async { + final pkg = await tx.lookupValue(package.key); + pkg.publisherId = null; + pkg.uploaders = currentPublisherMembers.map((e) => e.userId).toList(); + pkg.updated = clock.now().toUtc(); + tx.insert(pkg); + tx.insert( + await AuditLogRecord.packageRemovedFromPublisher( + package: packageName, + fromPublisherId: currentPublisherId, + ), + ); }); + triggerPackagePostUpdates( + packageName, + skipReanalysis: true, + skipVersionsExport: true, + ); + await purgePublisherCache(publisherId: currentPublisherId); + return { + 'previousPublisher': currentPublisherId, + 'package': package.name, + 'uploaders': [ + for (final member in currentPublisherMembers) + {'email': member.email, 'userId': member.userId}, + ], + }; + }, +); diff --git a/app/lib/admin/actions/task_bump_priority.dart b/app/lib/admin/actions/task_bump_priority.dart index d177e6c53a..5041e0b5b5 100644 --- a/app/lib/admin/actions/task_bump_priority.dart +++ b/app/lib/admin/actions/task_bump_priority.dart @@ -20,11 +20,10 @@ times will have no effect, it will always set `pendingAt` to the same value. This is intended for debugging, or solving one-off issues. ''', - options: { - 'package': 'Name of package whose priority should be bumped', - }, + options: {'package': 'Name of package whose priority should be bumped'}, invoke: (options) async { - final package = options['package'] ?? + final package = + options['package'] ?? (throw InvalidInputException('Needs a package name')); InvalidInputException.checkPackageName(package); // Make sure package exists. diff --git a/app/lib/admin/actions/tool_execute.dart b/app/lib/admin/actions/tool_execute.dart index 3d7c6e6462..d63025c3b0 100644 --- a/app/lib/admin/actions/tool_execute.dart +++ b/app/lib/admin/actions/tool_execute.dart @@ -17,9 +17,11 @@ possible to call them with arguments that contain comma. 'args': 'comma separated list of arguments', }, invoke: (options) async { - final toolName = options['tool'] ?? + final toolName = + options['tool'] ?? (throw InvalidInputException('Missing --tool argument')); - final argsOption = options['args'] ?? + final argsOption = + options['args'] ?? (throw InvalidInputException('Missing --args argument')); final args = argsOption.split(','); InvalidInputException.check(toolName.isNotEmpty, 'tool must given'); diff --git a/app/lib/admin/actions/tool_list.dart b/app/lib/admin/actions/tool_list.dart index a38d6e87f0..d8268e6ee4 100644 --- a/app/lib/admin/actions/tool_list.dart +++ b/app/lib/admin/actions/tool_list.dart @@ -15,11 +15,7 @@ command. options: {}, invoke: (options) async { return { - 'tools': availableTools.keys - .map((k) => { - 'tool': k, - }) - .toList(), + 'tools': availableTools.keys.map((k) => {'tool': k}).toList(), }; }, ); diff --git a/app/lib/admin/actions/uploader_count_report.dart b/app/lib/admin/actions/uploader_count_report.dart index 96d53ca14d..3148ba6239 100644 --- a/app/lib/admin/actions/uploader_count_report.dart +++ b/app/lib/admin/actions/uploader_count_report.dart @@ -23,39 +23,42 @@ const _monthNames = { }; final uploaderCountReport = AdminAction( - name: 'uploader-count-report', - options: {}, - summary: - 'Returns a report of the number of uploaders for each of the last 12 months.', - description: ''' + name: 'uploader-count-report', + options: {}, + summary: + 'Returns a report of the number of uploaders for each of the last 12 months.', + description: ''' Returns a report of the number of uploaders for each of the last 12 months. The report is based on "UploadEvents". ''', - invoke: (args) async { - final buckets = >{}; - final now = clock.now(); + invoke: (args) async { + final buckets = >{}; + final now = clock.now(); - await for (final record in accountBackend.getUploadEvents( - begin: DateTime(now.year, now.month - 12), - )) { - final created = record.created; - final agent = record.agent; - if (created == null) continue; - if (agent == null) continue; - final bucket = buckets.putIfAbsent( - DateTime(created.year, created.month), () => {}); - bucket.add(agent); - } - final report = >[]; - for (int i = 11; i >= 0; i--) { - final month = DateTime(now.year, now.month - i); - final bucket = buckets[month] ?? {}; - report.add({ - 'year': month.year, - 'month': _monthNames[month.month]!, - 'unique uploading users': bucket.length, - }); - } - return {'report': report}; - }); + await for (final record in accountBackend.getUploadEvents( + begin: DateTime(now.year, now.month - 12), + )) { + final created = record.created; + final agent = record.agent; + if (created == null) continue; + if (agent == null) continue; + final bucket = buckets.putIfAbsent( + DateTime(created.year, created.month), + () => {}, + ); + bucket.add(agent); + } + final report = >[]; + for (int i = 11; i >= 0; i--) { + final month = DateTime(now.year, now.month - i); + final bucket = buckets[month] ?? {}; + report.add({ + 'year': month.year, + 'month': _monthNames[month.month]!, + 'unique uploading users': bucket.length, + }); + } + return {'report': report}; + }, +); diff --git a/app/lib/admin/actions/user_info.dart b/app/lib/admin/actions/user_info.dart index 043864db57..1ae1ff3601 100644 --- a/app/lib/admin/actions/user_info.dart +++ b/app/lib/admin/actions/user_info.dart @@ -34,9 +34,7 @@ Output is on this form: } ``` ''', - options: { - 'user': 'The user-id or the email of the user to inspect', - }, + options: {'user': 'The user-id or the email of the user to inspect'}, invoke: (options) async { final userIdOrEmail = options['user']; InvalidInputException.check( @@ -56,8 +54,9 @@ Output is on this form: } final result = []; for (final user in users) { - final publishers = - await publisherBackend.listPublishersForUser(user.userId); + final publishers = await publisherBackend.listPublishersForUser( + user.userId, + ); final packages = await packageBackend .streamPackagesWhereUserIsUploader(user.userId) @@ -67,8 +66,9 @@ Output is on this form: 'userId': user.userId, 'email': user.email, 'packages': packages, - 'publishers': - (publishers.publishers ?? []).map((d) => d.publisherId).toList(), + 'publishers': (publishers.publishers ?? []) + .map((d) => d.publisherId) + .toList(), 'moderated': user.isModerated, if (user.isModerated) 'moderatedAt': user.moderatedAt, 'created': user.created?.toIso8601String(), diff --git a/app/lib/admin/backend.dart b/app/lib/admin/backend.dart index 048cefcff7..6f60d653f8 100644 --- a/app/lib/admin/backend.dart +++ b/app/lib/admin/backend.dart @@ -107,8 +107,9 @@ class AdminBackend { // NOTE: we should fix https://github.com/dart-lang/gcloud/issues/23 // and remove the toDatastoreKey conversion here. - final key = - _db.modelDB.toDatastoreKey(_db.emptyKey.append(User, id: lastId)); + final key = _db.modelDB.toDatastoreKey( + _db.emptyKey.append(User, id: lastId), + ); query.filter('__key__ >', key); query.order('__key__'); } else { @@ -137,8 +138,10 @@ class AdminBackend { final user = await accountBackend.lookupUserById(userId); if (user == null) return; if (user.isDeleted) return; - _logger.info('${caller.displayId}) initiated the delete ' - 'of ${user.userId} (${user.email})'); + _logger.info( + '${caller.displayId}) initiated the delete ' + 'of ${user.userId} (${user.email})', + ); await _removeUser(user); } @@ -150,8 +153,9 @@ class AdminBackend { final futures = []; final pkgQuery = _db.query()..filter('uploaders =', user.userId); await for (final p in pkgQuery.run()) { - final f = pool - .withResource(() => _removeUploaderFromPackage(p.key, user.userId)); + final f = pool.withResource( + () => _removeUploaderFromPackage(p.key, user.userId), + ); futures.add(f); } await Future.wait(futures); @@ -179,8 +183,9 @@ class AdminBackend { final pool = Pool(5); final futures = []; for (final like in await likeBackend.listPackageLikes(user.userId)) { - final f = pool - .withResource(() => likeBackend.unlikePackage(user, like.package!)); + final f = pool.withResource( + () => likeBackend.unlikePackage(user, like.package!), + ); futures.add(f); } await Future.wait(futures); @@ -199,8 +204,10 @@ class AdminBackend { } Future _removeMember(User user, PublisherMember member) async { - final seniorMember = - await _remainingSeniorMember(member.publisherKey, member.userId!); + final seniorMember = await _remainingSeniorMember( + member.publisherKey, + member.userId!, + ); await withRetryTransaction(_db, (tx) async { final p = await tx.lookupValue(member.publisherKey); if (seniorMember == null) { @@ -208,8 +215,9 @@ class AdminBackend { p.contactEmail = null; // TODO: consider deleting Publisher if there are no other references to it } else if (p.contactEmail == user.email) { - final seniorUser = - await accountBackend.lookupUserById(seniorMember.userId!); + final seniorUser = await accountBackend.lookupUserById( + seniorMember.userId!, + ); p.contactEmail = seniorUser!.email; } tx.queueMutations(inserts: [p], deletes: [member.key]); @@ -242,7 +250,9 @@ class AdminBackend { /// /// If there are no more admins left, the "oldest" non-admin member is returned. Future _remainingSeniorMember( - Key publisherKey, String excludeUserId) async { + Key publisherKey, + String excludeUserId, + ) async { final otherMembers = await _db .query(ancestorKey: publisherKey) .run() @@ -267,8 +277,10 @@ class AdminBackend { final u = await tx.lookupValue(user.key); final deleteKeys = []; if (user.oauthUserId != null) { - final mappingKey = - _db.emptyKey.append(OAuthUserID, id: user.oauthUserId); + final mappingKey = _db.emptyKey.append( + OAuthUserID, + id: user.oauthUserId, + ); final mapping = await tx.lookupOrNull(mappingKey); if (mapping != null) { deleteKeys.add(mappingKey); @@ -291,32 +303,35 @@ class AdminBackend { /// Datastore representing the removed package. No new package with the same /// name can be published. Future< - ({ - int deletedPackages, - int deletedPackageVersions, - int deletedPackageVersionInfos, - int deletedPackageVersionAssets, - int deletedLikes, - int deletedAuditLogs, - int replacedByFixes, - })> removePackage( - String packageName, { - DateTime? moderated, - }) async { + ({ + int deletedPackages, + int deletedPackageVersions, + int deletedPackageVersionInfos, + int deletedPackageVersionAssets, + int deletedLikes, + int deletedAuditLogs, + int replacedByFixes, + }) + > + removePackage(String packageName, {DateTime? moderated}) async { final packageKey = _db.emptyKey.append(Package, id: packageName); - final versions = (await _db - .query(ancestorKey: packageKey) - .run() - .map((pv) => pv.version!) - .toList()) - .toSet(); + final versions = + (await _db + .query(ancestorKey: packageKey) + .run() + .map((pv) => pv.version!) + .toList()) + .toSet(); final pool = Pool(10); final futures = []; for (final v in versions) { // Deleting public and canonical archives, 404 errors are ignored. - futures.add(pool.withResource( - () => packageBackend.removePackageTarball(packageName, v))); + futures.add( + pool.withResource( + () => packageBackend.removePackageTarball(packageName, v), + ), + ); } await Future.wait(futures); await pool.close(); @@ -341,63 +356,76 @@ class AdminBackend { _logger.info('Removing package from PackageVersionInfo ...'); final deletedPackageVersionInfos = await _db.deleteWithQuery( - _db.query()..filter('package =', packageName)); + _db.query()..filter('package =', packageName), + ); _logger.info('Removing package from PackageVersionAsset ...'); final deletedPackageVersionAssets = await _db.deleteWithQuery( - _db.query()..filter('package =', packageName)); + _db.query()..filter('package =', packageName), + ); _logger.info('Removing package from Like ...'); final deletedLikes = await _db.deleteWithQuery( - _db.query()..filter('packageName =', packageName)); + _db.query()..filter('packageName =', packageName), + ); _logger.info('Removing package from AuditLogRecord...'); final deletedAuditLogRecords = await _db.deleteWithQuery( - _db.query()..filter('packages =', packageName)); + _db.query()..filter('packages =', packageName), + ); _logger.info('Removing Package from Datastore...'); var deletedPackages = 0; await withRetryTransaction(_db, (tx) async { final package = await tx.lookupOrNull(packageKey); if (package == null) { - _logger - .info('Package $packageName not found. Removing related elements.'); + _logger.info( + 'Package $packageName not found. Removing related elements.', + ); // Returning early makes sure we are not creating ghost `ModeratedPackage` // entities because of a typo. return; } tx.delete(packageKey); deletedPackages = 1; - final moderatedPkgKey = - _db.emptyKey.append(ModeratedPackage, id: packageName); - final moderatedPkg = - await _db.lookupOrNull(moderatedPkgKey); + final moderatedPkgKey = _db.emptyKey.append( + ModeratedPackage, + id: packageName, + ); + final moderatedPkg = await _db.lookupOrNull( + moderatedPkgKey, + ); if (moderatedPkg == null) { // Refresh versions to make sure we are not missing a freshly uploaded one. - versions.addAll(await tx - .query(packageKey) - .run() - .map((pv) => pv.version!) - .toList()); + versions.addAll( + await tx + .query(packageKey) + .run() + .map((pv) => pv.version!) + .toList(), + ); versions.addAll(package.deletedVersions ?? const []); - tx.insert(ModeratedPackage() - ..parentKey = _db.emptyKey - ..id = packageName - ..name = packageName - ..moderated = moderated ?? clock.now().toUtc() - ..versions = versions.toList() - ..publisherId = package.publisherId - ..uploaders = package.uploaders); + tx.insert( + ModeratedPackage() + ..parentKey = _db.emptyKey + ..id = packageName + ..name = packageName + ..moderated = moderated ?? clock.now().toUtc() + ..versions = versions.toList() + ..publisherId = package.publisherId + ..uploaders = package.uploaders, + ); _logger.info('Adding package to moderated packages ...'); } }); _logger.info('Removing package from PackageVersion ...'); - await _db - .deleteWithQuery(_db.query(ancestorKey: packageKey)); + await _db.deleteWithQuery( + _db.query(ancestorKey: packageKey), + ); triggerPackagePostUpdates(packageName); @@ -409,7 +437,7 @@ class AdminBackend { deletedPackageVersionAssets: deletedPackageVersionAssets.deleted, deletedLikes: deletedLikes.deleted, deletedAuditLogs: deletedAuditLogRecords.deleted, - replacedByFixes: replacedByFixes + replacedByFixes: replacedByFixes, ); } @@ -418,33 +446,48 @@ class AdminBackend { /// It is safe to call [updateVersionOptions] on an version with the same /// options values (e.g. same retracted status), as the call is idempotent. Future updateVersionOptions( - String packageName, String version, VersionOptions options) async { + String packageName, + String version, + VersionOptions options, + ) async { checkPackageVersionParams(packageName, version); - InvalidInputException.check(options.isRetracted != null, - 'Only updating "isRetracted" is implemented.'); - final caller = - await requireAuthenticatedAdmin(AdminPermission.manageRetraction); + InvalidInputException.check( + options.isRetracted != null, + 'Only updating "isRetracted" is implemented.', + ); + final caller = await requireAuthenticatedAdmin( + AdminPermission.manageRetraction, + ); if (options.isRetracted != null) { final isRetracted = options.isRetracted!; - _logger.info('${caller.displayId}) initiated the isRetracted status ' - 'of package $packageName $version to be $isRetracted.'); + _logger.info( + '${caller.displayId}) initiated the isRetracted status ' + 'of package $packageName $version to be $isRetracted.', + ); await withRetryTransaction(_db, (tx) async { final p = await tx.lookupOrNull( - _db.emptyKey.append(Package, id: packageName)); + _db.emptyKey.append(Package, id: packageName), + ); if (p == null) { throw NotFoundException.resource(packageName); } final pv = await tx.lookupOrNull( - p.key.append(PackageVersion, id: version)); + p.key.append(PackageVersion, id: version), + ); if (pv == null) { throw NotFoundException.resource(version); } if (pv.isRetracted != isRetracted) { await packageBackend.doUpdateRetractedStatus( - caller, tx, p, pv, isRetracted); + caller, + tx, + p, + pv, + isRetracted, + ); } }); triggerPackagePostUpdates(packageName); @@ -456,16 +499,20 @@ class AdminBackend { /// removed version, as the call is idempotent. @visibleForTesting Future< - ({ - int deletedPackageVersions, - int deletedPackageVersionInfos, - int deletedPackageVersionAssets, - })> removePackageVersion(String packageName, String version) async { + ({ + int deletedPackageVersions, + int deletedPackageVersionInfos, + int deletedPackageVersionAssets, + }) + > + removePackageVersion(String packageName, String version) async { var deletedPackageVersions = 0; final currentDartSdk = await getCachedDartSdkVersion( - lastKnownStable: toolStableDartSdkVersion); + lastKnownStable: toolStableDartSdkVersion, + ); final currentFlutterSdk = await getCachedFlutterSdkVersion( - lastKnownStable: toolStableFlutterSdkVersion); + lastKnownStable: toolStableFlutterSdkVersion, + ); _logger.info('Removing GCS objects ...'); await packageBackend.removePackageTarball(packageName, version); @@ -500,7 +547,8 @@ class AdminBackend { if (versionNames.length == 1 && versionNames.single == version) { throw Exception( - 'Last version detected. Use full package removal without the version qualifier.'); + 'Last version detected. Use full package removal without the version qualifier.', + ); } package.updateVersions( @@ -531,9 +579,7 @@ class AdminBackend { /// return anything secret. This is because the `/admin/` section is only /// intended to be exposed to administrators. Users can read the assigned-tags /// through API that returns list of package tags. - Future handleGetAssignedTags( - String packageName, - ) async { + Future handleGetAssignedTags(String packageName) async { checkPackageVersionParams(packageName); await requireAuthenticatedAdmin(AdminPermission.manageAssignedTags); final package = await packageBackend.lookupPackage(packageName); @@ -541,9 +587,7 @@ class AdminBackend { throw NotFoundException.resource(packageName); } - return api.AssignedTags( - assignedTags: package.assignedTags!, - ); + return api.AssignedTags(assignedTags: package.assignedTags!); } /// Handles `POST '/api/admin/packages//assigned-tags'`. @@ -554,8 +598,9 @@ class AdminBackend { await requireAuthenticatedAdmin(AdminPermission.manageAssignedTags); InvalidInputException.check( - body.assignedTagsAdded - .every((tag) => allowedTagPrefixes.any(tag.startsWith)), + body.assignedTagsAdded.every( + (tag) => allowedTagPrefixes.any(tag.startsWith), + ), 'Only following tag-prefixes are allowed "${allowedTagPrefixes.join("\", ")}"', ); InvalidInputException.check( @@ -567,10 +612,9 @@ class AdminBackend { ); return await withRetryTransaction(_db, (tx) async { - final package = await tx.lookupOrNull(_db.emptyKey.append( - Package, - id: packageName, - )); + final package = await tx.lookupOrNull( + _db.emptyKey.append(Package, id: packageName), + ); if (package == null) { throw NotFoundException.resource(packageName); @@ -585,9 +629,7 @@ class AdminBackend { tx.insert(package); } - return api.AssignedTags( - assignedTags: package.assignedTags!, - ); + return api.AssignedTags(assignedTags: package.assignedTags!); }); } @@ -604,12 +646,12 @@ class AdminBackend { throw NotFoundException.resource(packageName); } InvalidInputException.check( - package.publisherId == null, 'Package must not be under a publisher.'); + package.publisherId == null, + 'Package must not be under a publisher.', + ); final users = await accountBackend.lookupUsersById(package.uploaders!); - return api.PackageUploaders( - uploaders: _convertUsers(users), - ); + return api.PackageUploaders(uploaders: _convertUsers(users)); } List _convertUsers(Iterable users) { @@ -629,10 +671,13 @@ class AdminBackend { /// /// Returns the list of uploaders for a package. Future handleAddPackageUploader( - String packageName, String email) async { + String packageName, + String email, + ) async { checkPackageVersionParams(packageName); - final authenticatedAgent = - await requireAuthenticatedAdmin(AdminPermission.managePackageOwnership); + final authenticatedAgent = await requireAuthenticatedAdmin( + AdminPermission.managePackageOwnership, + ); final package = await packageBackend.lookupPackage(packageName); if (package == null) { throw NotFoundException.resource(packageName); @@ -640,7 +685,9 @@ class AdminBackend { final uploaderEmail = email.toLowerCase(); InvalidInputException.check( - isValidEmail(uploaderEmail), 'Not a valid email: `$uploaderEmail`.'); + isValidEmail(uploaderEmail), + 'Not a valid email: `$uploaderEmail`.', + ); await consentBackend.invitePackageUploader( agent: authenticatedAgent, @@ -654,10 +701,13 @@ class AdminBackend { /// /// Returns the list of uploaders for a package. Future handleRemovePackageUploader( - String packageName, String email) async { + String packageName, + String email, + ) async { checkPackageVersionParams(packageName); - final authenticatedAgent = - await requireAuthenticatedAdmin(AdminPermission.managePackageOwnership); + final authenticatedAgent = await requireAuthenticatedAdmin( + AdminPermission.managePackageOwnership, + ); final package = await packageBackend.lookupPackage(packageName); if (package == null) { throw NotFoundException.resource(packageName); @@ -665,37 +715,48 @@ class AdminBackend { final uploaderEmail = email.toLowerCase(); InvalidInputException.check( - isValidEmail(uploaderEmail), 'Not a valid email: `$uploaderEmail`.'); - final uploaderUsers = - await accountBackend.lookupUsersByEmail(uploaderEmail); - InvalidInputException.check(uploaderUsers.isNotEmpty, - 'No users found for email: `$uploaderEmail`.'); + isValidEmail(uploaderEmail), + 'Not a valid email: `$uploaderEmail`.', + ); + final uploaderUsers = await accountBackend.lookupUsersByEmail( + uploaderEmail, + ); + InvalidInputException.check( + uploaderUsers.isNotEmpty, + 'No users found for email: `$uploaderEmail`.', + ); await withRetryTransaction(_db, (tx) async { final p = await tx.lookupValue(package.key); InvalidInputException.check( - p.publisherId == null, 'Package must not be under a publisher.'); + p.publisherId == null, + 'Package must not be under a publisher.', + ); var removed = false; for (final uploaderUser in uploaderUsers) { final r = p.uploaders!.remove(uploaderUser.userId); if (r) { removed = true; - tx.insert(await AuditLogRecord.uploaderRemoved( - agent: authenticatedAgent, - package: packageName, - uploaderUser: uploaderUser, - )); + tx.insert( + await AuditLogRecord.uploaderRemoved( + agent: authenticatedAgent, + package: packageName, + uploaderUser: uploaderUser, + ), + ); } } if (removed) { if (p.uploaders!.isEmpty) { p.isDiscontinued = true; - tx.insert(await AuditLogRecord.packageOptionsUpdated( - agent: authenticatedAgent, - package: packageName, - publisherId: p.publisherId, - options: ['discontinued'], - )); + tx.insert( + await AuditLogRecord.packageOptionsUpdated( + agent: authenticatedAgent, + package: packageName, + publisherId: p.publisherId, + options: ['discontinued'], + ), + ); } p.updated = clock.now().toUtc(); tx.insert(p); @@ -735,8 +796,9 @@ class AdminBackend { } // Don't allow unknown arguments - final unknownArgs = - args.keys.toSet().difference(action.options.keys.toSet()); + final unknownArgs = args.keys.toSet().difference( + action.options.keys.toSet(), + ); InvalidInputException.check( unknownArgs.isEmpty, 'Unknown options: ${unknownArgs.join(',')}', @@ -751,7 +813,8 @@ class AdminBackend { Future lookupModerationCase(String caseId) async { return await dbService.lookupOrNull( - dbService.emptyKey.append(ModerationCase, id: caseId)); + dbService.emptyKey.append(ModerationCase, id: caseId), + ); } /// Returns a valid [ModerationCase] if it exists. @@ -812,10 +875,7 @@ class AdminBackend { } _logger.info('Deleting moderated package: ${package.name}'); - await removePackage( - package.name!, - moderated: package.moderatedAt, - ); + await removePackage(package.name!, moderated: package.moderatedAt); _logger.info('Deleted moderated package: ${package.name}'); } @@ -830,10 +890,12 @@ class AdminBackend { } _logger.info( - 'Deleting moderated package version: ${version.qualifiedVersionKey}'); + 'Deleting moderated package version: ${version.qualifiedVersionKey}', + ); await removePackageVersion(version.package, version.version!); _logger.info( - 'Deleted moderated package version: ${version.qualifiedVersionKey}'); + 'Deleted moderated package version: ${version.qualifiedVersionKey}', + ); } // delete publishers @@ -866,7 +928,8 @@ class AdminBackend { // removes publisher members await _db.deleteWithQuery( - _db.query(ancestorKey: publisher.key)); + _db.query(ancestorKey: publisher.key), + ); // removes publisher entity await _db.commit(deletes: [publisher.key]); @@ -921,10 +984,12 @@ class AdminBackend { } _logger.info( - 'Deleting admin-deleted package version: ${version.qualifiedVersionKey}'); + 'Deleting admin-deleted package version: ${version.qualifiedVersionKey}', + ); await removePackageVersion(version.package, version.version!); _logger.info( - 'Deleted moderated package version: ${version.qualifiedVersionKey}'); + 'Deleted moderated package version: ${version.qualifiedVersionKey}', + ); } } diff --git a/app/lib/admin/models.dart b/app/lib/admin/models.dart index e2ebf3aac5..59f815beae 100644 --- a/app/lib/admin/models.dart +++ b/app/lib/admin/models.dart @@ -129,9 +129,7 @@ class ModerationCase extends db.ExpandoModel { } /// Generates a short case id like `
I0123456789`. - static String generateCaseId({ - DateTime? now, - }) { + static String generateCaseId({DateTime? now}) { now ??= clock.now(); return '${now.toIso8601String().split('T').first.replaceAll('-', '')}' 'I' @@ -143,7 +141,8 @@ class ModerationCase extends db.ExpandoModel { return ModerationActionLog(entries: []); } return ModerationActionLog.fromJson( - json.decode(actionLogField!) as Map); + json.decode(actionLogField!) as Map, + ); } void setActionLog(ModerationActionLog value) { @@ -157,12 +156,14 @@ class ModerationCase extends db.ExpandoModel { String? note, ) { final log = getActionLog(); - log.entries.add(ModerationActionLogEntry( - timestamp: clock.now().toUtc(), - subject: subject, - moderationAction: moderationAction, - note: note, - )); + log.entries.add( + ModerationActionLogEntry( + timestamp: clock.now().toUtc(), + subject: subject, + moderationAction: moderationAction, + note: note, + ), + ); setActionLog(log); } @@ -220,10 +221,7 @@ abstract class ModerationKind { static const notification = 'notification'; static const appeal = 'appeal'; - static const _values = [ - notification, - appeal, - ]; + static const _values = [notification, appeal]; static bool isValidKind(String value) => _values.contains(value); } @@ -244,10 +242,7 @@ abstract class ModerationStatus { moderationUpheld, moderationReverted, ]; - static const _values = [ - pending, - ...resolveValues, - ]; + static const _values = [pending, ...resolveValues]; static bool isValidStatus(String value) => _values.contains(value); static bool wasModerationApplied(String value) => @@ -260,10 +255,7 @@ abstract class ModerationGrounds { static const illegal = 'illegal'; static const policy = 'policy'; - static final resolveValues = [ - illegal, - policy, - ]; + static final resolveValues = [illegal, policy]; } abstract class ModerationViolation { @@ -414,11 +406,7 @@ class ModerationSubject { ); case ModerationSubjectKind.user: final email = parts[1]; - return ModerationSubject._( - kind: kind, - localName: email, - email: email, - ); + return ModerationSubject._(kind: kind, localName: email, email: email); default: return null; } @@ -454,9 +442,7 @@ class ModerationSubjectKind { class ModerationActionLog { final List entries; - ModerationActionLog({ - required this.entries, - }); + ModerationActionLog({required this.entries}); factory ModerationActionLog.fromJson(Map json) => _$ModerationActionLogFromJson(json); @@ -466,10 +452,7 @@ class ModerationActionLog { bool get isNotEmpty => entries.isNotEmpty; } -enum ModerationAction { - apply, - revert, -} +enum ModerationAction { apply, revert } @JsonSerializable(includeIfNull: false, explicitToJson: true) class ModerationActionLogEntry { diff --git a/app/lib/admin/models.g.dart b/app/lib/admin/models.g.dart index 06cba922c1..a0e41a35d2 100644 --- a/app/lib/admin/models.g.dart +++ b/app/lib/admin/models.g.dart @@ -9,35 +9,38 @@ part of 'models.dart'; ModerationActionLog _$ModerationActionLogFromJson(Map json) => ModerationActionLog( entries: (json['entries'] as List) - .map((e) => - ModerationActionLogEntry.fromJson(e as Map)) + .map( + (e) => ModerationActionLogEntry.fromJson(e as Map), + ) .toList(), ); Map _$ModerationActionLogToJson( - ModerationActionLog instance) => - { - 'entries': instance.entries.map((e) => e.toJson()).toList(), - }; + ModerationActionLog instance, +) => { + 'entries': instance.entries.map((e) => e.toJson()).toList(), +}; ModerationActionLogEntry _$ModerationActionLogEntryFromJson( - Map json) => - ModerationActionLogEntry( - timestamp: DateTime.parse(json['timestamp'] as String), - subject: json['subject'] as String, - moderationAction: - $enumDecode(_$ModerationActionEnumMap, json['moderationAction']), - note: json['note'] as String?, - ); + Map json, +) => ModerationActionLogEntry( + timestamp: DateTime.parse(json['timestamp'] as String), + subject: json['subject'] as String, + moderationAction: $enumDecode( + _$ModerationActionEnumMap, + json['moderationAction'], + ), + note: json['note'] as String?, +); Map _$ModerationActionLogEntryToJson( - ModerationActionLogEntry instance) => - { - 'timestamp': instance.timestamp.toIso8601String(), - 'subject': instance.subject, - 'moderationAction': _$ModerationActionEnumMap[instance.moderationAction]!, - if (instance.note case final value?) 'note': value, - }; + ModerationActionLogEntry instance, +) => { + 'timestamp': instance.timestamp.toIso8601String(), + 'subject': instance.subject, + 'moderationAction': _$ModerationActionEnumMap[instance.moderationAction]!, + if (instance.note case final value?) 'note': value, +}; const _$ModerationActionEnumMap = { ModerationAction.apply: 'apply', diff --git a/app/lib/admin/tools/delete_all_staging.dart b/app/lib/admin/tools/delete_all_staging.dart index 040600b12b..c246f4719e 100644 --- a/app/lib/admin/tools/delete_all_staging.dart +++ b/app/lib/admin/tools/delete_all_staging.dart @@ -15,10 +15,18 @@ import '../../task/models.dart'; import '../../tool/neat_task/datastore_status_provider.dart'; final _argParser = ArgParser() - ..addOption('concurrency', - abbr: 'c', defaultsTo: '1', help: 'Number of concurrent processing.') - ..addFlag('dry-run', - abbr: 'n', defaultsTo: false, help: 'Do not change Datastore.') + ..addOption( + 'concurrency', + abbr: 'c', + defaultsTo: '1', + help: 'Number of concurrent processing.', + ) + ..addFlag( + 'dry-run', + abbr: 'n', + defaultsTo: false, + help: 'Do not change Datastore.', + ) ..addFlag('help', abbr: 'h', defaultsTo: false, help: 'Show help.'); /// Deletes every (used) entity on staging. @@ -59,14 +67,10 @@ Future executeDeleteAllStaging(List args) async { final pool = Pool(concurrency); for (final entity in entities.entries) { final futures = []; - await _batchedQuery( - entity.key, - (keys) { - final f = pool.withResource(() => _commit(keys, dryRun)); - futures.add(f); - }, - maxBatchSize: entity.value, - ); + await _batchedQuery(entity.key, (keys) { + final f = pool.withResource(() => _commit(keys, dryRun)); + futures.add(f); + }, maxBatchSize: entity.value); await Future.wait(futures); } await pool.close(); diff --git a/app/lib/admin/tools/package_publisher.dart b/app/lib/admin/tools/package_publisher.dart index f137fe95d0..292aa9cfcd 100644 --- a/app/lib/admin/tools/package_publisher.dart +++ b/app/lib/admin/tools/package_publisher.dart @@ -42,8 +42,11 @@ Future executeSetPackagePublisher(List args) async { tx.insert(pkg); }); await purgePublisherCache(publisherId: publisherId); - triggerPackagePostUpdates(packageName, - skipReanalysis: true, skipVersionsExport: true); + triggerPackagePostUpdates( + packageName, + skipReanalysis: true, + skipVersionsExport: true, + ); if (currentPublisherId != null) { await purgePublisherCache(publisherId: currentPublisherId); } diff --git a/app/lib/admin/tools/recent_uploaders.dart b/app/lib/admin/tools/recent_uploaders.dart index 060affa369..aeb0be6f55 100644 --- a/app/lib/admin/tools/recent_uploaders.dart +++ b/app/lib/admin/tools/recent_uploaders.dart @@ -18,8 +18,11 @@ import 'package:pub_dev/shared/datastore.dart'; Future executeRecentUploaders(List args) async { final parser = ArgParser() - ..addOption('max-age', - defaultsTo: '7', help: 'The maximum age of the package in days.') + ..addOption( + 'max-age', + defaultsTo: '7', + help: 'The maximum age of the package in days.', + ) ..addOption('output', help: 'The report output file (or stdout otherwise)'); final argv = parser.parse(args); final maxAgeDays = int.parse(argv['max-age'] as String); @@ -39,8 +42,9 @@ Future executeRecentUploaders(List args) async { .putIfAbsent(p.publisherId!, () => []) .add(p.name ?? ''); } else { - final uploaderEmails = - await accountBackend.getEmailsOfUserIds(p.uploaders!); + final uploaderEmails = await accountBackend.getEmailsOfUserIds( + p.uploaders!, + ); uploaderEmails.forEach((email) { byUploaders .putIfAbsent(email ?? '', () => []) diff --git a/app/lib/admin/tools/user_merger.dart b/app/lib/admin/tools/user_merger.dart index 25afd32aae..e3819e7a31 100644 --- a/app/lib/admin/tools/user_merger.dart +++ b/app/lib/admin/tools/user_merger.dart @@ -16,14 +16,21 @@ import 'package:pub_dev/shared/exceptions.dart'; final _logger = Logger('user_merger'); final _argParser = ArgParser() - ..addOption('concurrency', - abbr: 'c', defaultsTo: '10', help: 'Number of concurrent processing.') + ..addOption( + 'concurrency', + abbr: 'c', + defaultsTo: '10', + help: 'Number of concurrent processing.', + ) ..addOption('oauth-user-id', help: 'The specific OAuthUserId object to fix.') - ..addOption('from-user-id', - help: 'The User that will be removed. (must be in pair with to-user-id)') - ..addOption('to-user-id', - help: - 'The User that will be extended. (must be in pair with from-user-id)') + ..addOption( + 'from-user-id', + help: 'The User that will be removed. (must be in pair with to-user-id)', + ) + ..addOption( + 'to-user-id', + help: 'The User that will be extended. (must be in pair with from-user-id)', + ) ..addFlag('help', abbr: 'h', defaultsTo: false, help: 'Show help.'); int? concurrency; @@ -68,9 +75,9 @@ class UserMerger { required DatastoreDB db, int? concurrency = 1, bool? omitEmailCheck, - }) : _db = db, - _concurrency = concurrency, - _omitEmailCheck = omitEmailCheck ?? false; + }) : _db = db, + _concurrency = concurrency, + _omitEmailCheck = omitEmailCheck ?? false; /// Fixes all OAuthUserID issues. Future fixAll() async { @@ -104,7 +111,8 @@ class UserMerger { _logger.info('Users: ${users.map((u) => u.userId).join(', ')}'); final mapping = await _db.lookupValue( - _db.emptyKey.append(OAuthUserID, id: oauthUserId)); + _db.emptyKey.append(OAuthUserID, id: oauthUserId), + ); _logger.info('Primary User: ${mapping.userId}'); if (!users.any((u) => u.userId == mapping.userId)) { throw StateError('Primary User is missing!'); @@ -118,7 +126,8 @@ class UserMerger { for (int i = 1; i < users.length; i++) { if (users[0].email != users[i].email) { throw StateError( - 'User e-mail does not match: ${users[0].email} != ${users[i].email}'); + 'User e-mail does not match: ${users[0].email} != ${users[i].email}', + ); } } } @@ -141,11 +150,13 @@ class UserMerger { final fromUserMapping = fromUser!.oauthUserId == null ? null : await _db.lookupOrNull( - _db.emptyKey.append(OAuthUserID, id: fromUser.oauthUserId)); + _db.emptyKey.append(OAuthUserID, id: fromUser.oauthUserId), + ); final toUserMapping = toUser!.oauthUserId == null ? null : await _db.lookupOrNull( - _db.emptyKey.append(OAuthUserID, id: toUser.oauthUserId)); + _db.emptyKey.append(OAuthUserID, id: toUser.oauthUserId), + ); // Package await _processConcurrently( @@ -177,17 +188,16 @@ class UserMerger { ); // Like - await _processConcurrently( - _db.query(ancestorKey: fromUserKey), - (Like like) async { - await withRetryTransaction(_db, (tx) async { - tx.queueMutations( - inserts: [like.changeParentUser(toUserKey)], - deletes: [like.key], - ); - }); - }, - ); + await _processConcurrently(_db.query(ancestorKey: fromUserKey), ( + Like like, + ) async { + await withRetryTransaction(_db, (tx) async { + tx.queueMutations( + inserts: [like.changeParentUser(toUserKey)], + deletes: [like.key], + ); + }); + }); // UserSession await _processConcurrently( @@ -235,32 +245,40 @@ class UserMerger { // AuditLogRecord: agent await _processConcurrently( - _db.query()..filter('agent =', fromUserId), - (AuditLogRecord alr) async { - await withRetryTransaction(_db, (tx) async { - final r = await _db.lookupValue(alr.key); - r.agent = toUserId; - r.data = r.data?.map((key, value) => MapEntry( - key, value == fromUserId ? toUserId : value)); - tx.insert(r); - }); - }); + _db.query()..filter('agent =', fromUserId), + (AuditLogRecord alr) async { + await withRetryTransaction(_db, (tx) async { + final r = await _db.lookupValue(alr.key); + r.agent = toUserId; + r.data = r.data?.map( + (key, value) => MapEntry( + key, + value == fromUserId ? toUserId : value, + ), + ); + tx.insert(r); + }); + }, + ); // AuditLogRecord: users await _processConcurrently( - _db.query()..filter('users =', fromUserId), - (AuditLogRecord alr) async { - await withRetryTransaction(_db, (tx) async { - final r = await _db.lookupValue(alr.key); - r.users!.remove(fromUserId); - r.users!.add(toUserId); - r.data = r.data?.map( - (key, value) => MapEntry( - key, value == fromUserId ? toUserId : value), - ); - tx.insert(r); - }); - }); + _db.query()..filter('users =', fromUserId), + (AuditLogRecord alr) async { + await withRetryTransaction(_db, (tx) async { + final r = await _db.lookupValue(alr.key); + r.users!.remove(fromUserId); + r.users!.add(toUserId); + r.data = r.data?.map( + (key, value) => MapEntry( + key, + value == fromUserId ? toUserId : value, + ), + ); + tx.insert(r); + }); + }, + ); await withRetryTransaction(_db, (tx) async { final u = await _db.lookupValue(toUserKey); @@ -285,7 +303,9 @@ class UserMerger { } Future _processConcurrently( - Query query, Future Function(T) fn) async { + Query query, + Future Function(T) fn, + ) async { final pool = Pool(_concurrency!); final futures = []; await for (final m in query.run()) { diff --git a/app/lib/audit/backend.dart b/app/lib/audit/backend.dart index 6c2a0c2f63..3cc63c6b8e 100644 --- a/app/lib/audit/backend.dart +++ b/app/lib/audit/backend.dart @@ -36,14 +36,18 @@ class AuditBackend { String value, DateTime? before, ) async { - assert(propertyName == 'users' || - propertyName == 'packages' || - propertyName == 'packageVersions' || - propertyName == 'publishers'); + assert( + propertyName == 'users' || + propertyName == 'packages' || + propertyName == 'packageVersions' || + propertyName == 'publishers', + ); final query = _db.query() ..filter('$propertyName =', value) ..filter( - 'created <=', before ?? clock.now().toUtc().add(Duration(minutes: 5))) + 'created <=', + before ?? clock.now().toUtc().add(Duration(minutes: 5)), + ) ..order('-created') ..limit(_maxAuditLogBatchSize); // TODO: consider using repeated queries to filter already expired records, @@ -107,8 +111,11 @@ class AuditBackend { @visibleForTesting String nextTimestamp(DateTime last, DateTime next) { - final nextDayStart = - DateTime.utc(next.year, next.month, next.day).add(Duration(days: 1)); + final nextDayStart = DateTime.utc( + next.year, + next.month, + next.day, + ).add(Duration(days: 1)); return nextDayStart.isBefore(last) && nextDayStart.isAfter(next) ? nextDayStart.toIso8601String().split('T').first : next.toIso8601String(); @@ -126,12 +133,19 @@ class AuditBackend { if (param.length == 10) { final m = _shortBeforeFormat.matchAsPrefix(param); if (m != null) { - final parsed = DateTime.utc(int.parse(m.group(1)!), - int.parse(m.group(2)!), int.parse(m.group(3)!)); + final parsed = DateTime.utc( + int.parse(m.group(1)!), + int.parse(m.group(2)!), + int.parse(m.group(3)!), + ); InvalidInputException.check( - parsed.year >= 2000, '`before` is too far in the past.'); + parsed.year >= 2000, + '`before` is too far in the past.', + ); InvalidInputException.check( - parsed.isBefore(now), '`before` is in the future.'); + parsed.isBefore(now), + '`before` is in the future.', + ); return parsed; } } diff --git a/app/lib/audit/models.dart b/app/lib/audit/models.dart index 66dae5a415..0222b65a74 100644 --- a/app/lib/audit/models.dart +++ b/app/lib/audit/models.dart @@ -16,8 +16,9 @@ import '../shared/utils.dart' show createUuid; @visibleForTesting final auditLogRecordExpiresInFarFuture = DateTime.utc(9999, 12, 31, 23, 59, 59); -final _shortTermExpireThreshold = - auditLogRecordExpiresInFarFuture.subtract(Duration(days: 1)); +final _shortTermExpireThreshold = auditLogRecordExpiresInFarFuture.subtract( + Duration(days: 1), +); final _defaultExpires = Duration(days: 61); /// A single page of log records. @@ -155,23 +156,23 @@ class AuditLogRecord extends db.ExpandoModel { required List options, }) { final optionsStr = options.map((o) => '`$o`').join(', '); - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.packageOptionsUpdated - ..agent = agent.agentId - ..summary = - '`${agent.displayId}` updated $optionsStr of package `$package`.' - ..data = { - 'package': package, - if (agent.email != null) 'email': agent.email, - if (publisherId != null) 'publisherId': publisherId, - 'options': options, - } - ..users = [ - if (agent is AuthenticatedUser) agent.userId, - ] - ..packages = [package] - ..packageVersions = [] - ..publishers = [if (publisherId != null) publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.packageOptionsUpdated + ..agent = agent.agentId + ..summary = + '`${agent.displayId}` updated $optionsStr of package `$package`.' + ..data = { + 'package': package, + if (agent.email != null) 'email': agent.email, + if (publisherId != null) 'publisherId': publisherId, + 'options': options, + } + ..users = [if (agent is AuthenticatedUser) agent.userId] + ..packages = [package] + ..packageVersions = [] + ..publishers = [if (publisherId != null) publisherId], + ); } /// Returns [AuditLogRecord] for the package automatic publishing settings updated operation. @@ -182,20 +183,22 @@ class AuditLogRecord extends db.ExpandoModel { required String? publisherId, required User user, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.packagePublicationAutomationUpdated - ..agent = user.userId - ..summary = - '`${user.email}` updated the publication automation config of package `$package`.' - ..data = { - 'package': package, - 'user': user.email, - if (publisherId != null) 'publisherId': publisherId, - } - ..users = [user.userId] - ..packages = [package] - ..packageVersions = [] - ..publishers = [if (publisherId != null) publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.packagePublicationAutomationUpdated + ..agent = user.userId + ..summary = + '`${user.email}` updated the publication automation config of package `$package`.' + ..data = { + 'package': package, + 'user': user.email, + if (publisherId != null) 'publisherId': publisherId, + } + ..users = [user.userId] + ..packages = [package] + ..packageVersions = [] + ..publishers = [if (publisherId != null) publisherId], + ); } /// Returns [AuditLogRecord] for the package version options updated operation. @@ -209,24 +212,25 @@ class AuditLogRecord extends db.ExpandoModel { required List options, }) { final optionsStr = options.map((o) => '`$o`').join(', '); - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.packageVersionOptionsUpdated - ..agent = agent.agentId - ..summary = '`${agent.displayId}` updated $optionsStr of ' - 'package `$package` version `$version`.' - ..data = { - 'package': package, - 'version': version, - if (agent.email != null) 'email': agent.email, - if (publisherId != null) 'publisherId': publisherId, - 'options': options, - } - ..users = [ - if (agent is AuthenticatedUser) agent.userId, - ] - ..packages = [package] - ..packageVersions = ['$package/$version'] - ..publishers = [if (publisherId != null) publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.packageVersionOptionsUpdated + ..agent = agent.agentId + ..summary = + '`${agent.displayId}` updated $optionsStr of ' + 'package `$package` version `$version`.' + ..data = { + 'package': package, + 'version': version, + if (agent.email != null) 'email': agent.email, + if (publisherId != null) 'publisherId': publisherId, + 'options': options, + } + ..users = [if (agent is AuthenticatedUser) agent.userId] + ..packages = [package] + ..packageVersions = ['$package/$version'] + ..publishers = [if (publisherId != null) publisherId], + ); } static Map _dataForPublishing({ @@ -272,13 +276,10 @@ class AuditLogRecord extends db.ExpandoModel { } else if (uploader is AuthenticatedGcpServiceAccount) { return [ ...prefix, - ' was published by Google Cloud service account: `${uploader.payload.email}`.' + ' was published by Google Cloud service account: `${uploader.payload.email}`.', ].join(); } else { - return [ - ...prefix, - ' was published by `${uploader.displayId}`.', - ].join(); + return [...prefix, ' was published by `${uploader.displayId}`.'].join(); } } @@ -356,28 +357,30 @@ class AuditLogRecord extends db.ExpandoModel { required String? fromPublisherId, required String toPublisherId, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.packageTransferred - ..agent = user.userId - ..summary = [ - 'Package `$package` ', - if (fromPublisherId != null) 'from `$fromPublisherId` ', - 'was transferred to publisher `$toPublisherId` ', - 'by `${user.email}`.', - ].join() - ..data = { - 'package': package, - if (fromPublisherId != null) 'fromPublisherId': fromPublisherId, - 'toPublisherId': toPublisherId, - 'user': user.email, - } - ..users = [user.userId] - ..packages = [package] - ..packageVersions = [] - ..publishers = [ - if (fromPublisherId != null) fromPublisherId, - toPublisherId, - ]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.packageTransferred + ..agent = user.userId + ..summary = [ + 'Package `$package` ', + if (fromPublisherId != null) 'from `$fromPublisherId` ', + 'was transferred to publisher `$toPublisherId` ', + 'by `${user.email}`.', + ].join() + ..data = { + 'package': package, + if (fromPublisherId != null) 'fromPublisherId': fromPublisherId, + 'toPublisherId': toPublisherId, + 'user': user.email, + } + ..users = [user.userId] + ..packages = [package] + ..packageVersions = [] + ..publishers = [ + if (fromPublisherId != null) fromPublisherId, + toPublisherId, + ], + ); } /// Returns [AuditLogRecord] for the package removed from publisher operation. @@ -389,23 +392,25 @@ class AuditLogRecord extends db.ExpandoModel { }) { // For now this is always done as an administrative action, so we hardcode // the email. - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.packageRemovedFromPublisher - ..agent = KnownAgents.pubSupport - ..summary = [ - 'Package `$package` ', - 'was removed from `$fromPublisherId` ', - 'by `${KnownAgents.pubSupport}`.', - ].join() - ..data = { - 'package': package, - 'fromPublisherId': fromPublisherId, - 'user': KnownAgents.pubSupport, - } - ..users = [] - ..packages = [package] - ..packageVersions = [] - ..publishers = [fromPublisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.packageRemovedFromPublisher + ..agent = KnownAgents.pubSupport + ..summary = [ + 'Package `$package` ', + 'was removed from `$fromPublisherId` ', + 'by `${KnownAgents.pubSupport}`.', + ].join() + ..data = { + 'package': package, + 'fromPublisherId': fromPublisherId, + 'user': KnownAgents.pubSupport, + } + ..users = [] + ..packages = [package] + ..packageVersions = [] + ..publishers = [fromPublisherId], + ); } /// Returns [AuditLogRecord] for the publisher contact invite accepted operation. @@ -416,22 +421,24 @@ class AuditLogRecord extends db.ExpandoModel { required String publisherId, required String contactEmail, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherContactInviteAccepted - ..agent = user.userId - ..summary = [ - '`${user.email}` accepted `$contactEmail` ', - 'to be contact email for publisher `$publisherId`.', - ].join() - ..data = { - 'publisherId': publisherId, - 'contactEmail': contactEmail, - 'user': user.email, - } - ..users = [user.userId] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherContactInviteAccepted + ..agent = user.userId + ..summary = [ + '`${user.email}` accepted `$contactEmail` ', + 'to be contact email for publisher `$publisherId`.', + ].join() + ..data = { + 'publisherId': publisherId, + 'contactEmail': contactEmail, + 'user': user.email, + } + ..users = [user.userId] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher contact invited operation. @@ -442,22 +449,24 @@ class AuditLogRecord extends db.ExpandoModel { required String publisherId, required String contactEmail, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherContactInvited - ..agent = user.userId - ..summary = [ - '`${user.email}` invited `$contactEmail` ', - 'to be contact email for publisher `$publisherId`.', - ].join() - ..data = { - 'publisherId': publisherId, - 'contactEmail': contactEmail, - 'user': user.email, - } - ..users = [user.userId] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherContactInvited + ..agent = user.userId + ..summary = [ + '`${user.email}` invited `$contactEmail` ', + 'to be contact email for publisher `$publisherId`.', + ].join() + ..data = { + 'publisherId': publisherId, + 'contactEmail': contactEmail, + 'user': user.email, + } + ..users = [user.userId] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher contact invite expired operation. @@ -468,22 +477,26 @@ class AuditLogRecord extends db.ExpandoModel { required String publisherId, required String contactEmail, }) { - final fromUserId = - fromAgent != null && looksLikeUserId(fromAgent) ? fromAgent : null; - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherContactInviteExpired - ..agent = KnownAgents.pubSupport - ..summary = 'Contact invite for publisher `$publisherId` expired, ' - '`$contactEmail` did not respond.' - ..data = { - if (fromUserId != null) 'fromUserId': fromUserId, - 'publisherId': publisherId, - 'contactEmail': contactEmail, - } - ..users = [if (fromUserId != null) fromUserId] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + final fromUserId = fromAgent != null && looksLikeUserId(fromAgent) + ? fromAgent + : null; + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherContactInviteExpired + ..agent = KnownAgents.pubSupport + ..summary = + 'Contact invite for publisher `$publisherId` expired, ' + '`$contactEmail` did not respond.' + ..data = { + if (fromUserId != null) 'fromUserId': fromUserId, + 'publisherId': publisherId, + 'contactEmail': contactEmail, + } + ..users = [if (fromUserId != null) fromUserId] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher contact invite rejected operation. @@ -498,29 +511,32 @@ class AuditLogRecord extends db.ExpandoModel { required String? userId, required String? userEmail, }) { - final fromUserId = - fromAgent != null && looksLikeUserId(fromAgent) ? fromAgent : null; + final fromUserId = fromAgent != null && looksLikeUserId(fromAgent) + ? fromAgent + : null; final summary = (userEmail == null || userEmail == contactEmail) ? '`$contactEmail` rejected contact invite for publisher `$publisherId`.' : '`$userEmail` rejected contact invite of `$contactEmail` for publisher `$publisherId`.'; - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherContactInviteRejected - ..agent = userId ?? KnownAgents.pubSupport - ..summary = summary - ..data = { - if (fromUserId != null) 'fromUserId': fromUserId, - 'publisherId': publisherId, - 'contactEmail': contactEmail, - if (userId != null) 'userId': userId, - if (userEmail != null) 'userEmail': userEmail, - } - ..users = [ - if (fromUserId != null) fromUserId, - if (userId != null) userId, - ] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherContactInviteRejected + ..agent = userId ?? KnownAgents.pubSupport + ..summary = summary + ..data = { + if (fromUserId != null) 'fromUserId': fromUserId, + 'publisherId': publisherId, + 'contactEmail': contactEmail, + if (userId != null) 'userId': userId, + if (userEmail != null) 'userEmail': userEmail, + } + ..users = [ + if (fromUserId != null) fromUserId, + if (userId != null) userId, + ] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher created operation. @@ -530,18 +546,17 @@ class AuditLogRecord extends db.ExpandoModel { required User user, required String publisherId, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherCreated - ..agent = user.userId - ..summary = '`${user.email}` created publisher `$publisherId`.' - ..data = { - 'publisherId': publisherId, - 'user': user.email, - } - ..users = [user.userId] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherCreated + ..agent = user.userId + ..summary = '`${user.email}` created publisher `$publisherId`.' + ..data = {'publisherId': publisherId, 'user': user.email} + ..users = [user.userId] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher member invited operation. @@ -552,22 +567,24 @@ class AuditLogRecord extends db.ExpandoModel { required String publisherId, required String memberEmail, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherMemberInvited - ..agent = agent.agentId - ..summary = [ - '`${agent.displayId}` invited `$memberEmail` ', - 'to be a member for publisher `$publisherId`.', - ].join() - ..data = { - 'publisherId': publisherId, - 'memberEmail': memberEmail, - if (agent.email != null) 'email': agent.email, - } - ..users = [if (agent is AuthenticatedUser) agent.userId] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherMemberInvited + ..agent = agent.agentId + ..summary = [ + '`${agent.displayId}` invited `$memberEmail` ', + 'to be a member for publisher `$publisherId`.', + ].join() + ..data = { + 'publisherId': publisherId, + 'memberEmail': memberEmail, + if (agent.email != null) 'email': agent.email, + } + ..users = [if (agent is AuthenticatedUser) agent.userId] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher member invite accepted operation. @@ -577,19 +594,18 @@ class AuditLogRecord extends db.ExpandoModel { required User user, required String publisherId, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherMemberInviteAccepted - ..agent = user.userId - ..summary = - '`${user.email}` accepted member invite for publisher `$publisherId`.' - ..data = { - 'publisherId': publisherId, - 'user': user.email, - } - ..users = [user.userId] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherMemberInviteAccepted + ..agent = user.userId + ..summary = + '`${user.email}` accepted member invite for publisher `$publisherId`.' + ..data = {'publisherId': publisherId, 'user': user.email} + ..users = [user.userId] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher member invite expired operation. @@ -600,22 +616,26 @@ class AuditLogRecord extends db.ExpandoModel { required String publisherId, required String memberEmail, }) { - final fromUserId = - fromAgent != null && looksLikeUserId(fromAgent) ? fromAgent : null; - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherMemberInviteExpired - ..agent = KnownAgents.pubSupport - ..summary = 'Member invite for publisher `$publisherId` expired, ' - '`$memberEmail` did not respond.' - ..data = { - if (fromUserId != null) 'fromUserId': fromUserId, - 'publisherId': publisherId, - 'memberEmail': memberEmail, - } - ..users = [if (fromUserId != null) fromUserId] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + final fromUserId = fromAgent != null && looksLikeUserId(fromAgent) + ? fromAgent + : null; + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherMemberInviteExpired + ..agent = KnownAgents.pubSupport + ..summary = + 'Member invite for publisher `$publisherId` expired, ' + '`$memberEmail` did not respond.' + ..data = { + if (fromUserId != null) 'fromUserId': fromUserId, + 'publisherId': publisherId, + 'memberEmail': memberEmail, + } + ..users = [if (fromUserId != null) fromUserId] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher member invite rejected operation. @@ -629,26 +649,29 @@ class AuditLogRecord extends db.ExpandoModel { /// Optional, in the future we may allow invite rejection without sign-in. required String? userId, }) { - final fromUserId = - fromAgent != null && looksLikeUserId(fromAgent) ? fromAgent : null; - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherMemberInviteRejected - ..agent = userId ?? KnownAgents.pubSupport - ..summary = - '`$memberEmail` rejected member invite for publisher `$publisherId`.' - ..data = { - if (fromUserId != null) 'fromUserId': fromUserId, - 'publisherId': publisherId, - 'memberEmail': memberEmail, - if (userId != null) 'userId': userId, - } - ..users = [ - if (fromUserId != null) fromUserId, - if (userId != null) userId, - ] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + final fromUserId = fromAgent != null && looksLikeUserId(fromAgent) + ? fromAgent + : null; + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherMemberInviteRejected + ..agent = userId ?? KnownAgents.pubSupport + ..summary = + '`$memberEmail` rejected member invite for publisher `$publisherId`.' + ..data = { + if (fromUserId != null) 'fromUserId': fromUserId, + 'publisherId': publisherId, + 'memberEmail': memberEmail, + if (userId != null) 'userId': userId, + } + ..users = [ + if (fromUserId != null) fromUserId, + if (userId != null) userId, + ] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher member removed operation. @@ -659,22 +682,24 @@ class AuditLogRecord extends db.ExpandoModel { required String publisherId, required User memberToRemove, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherMemberRemoved - ..agent = activeUser.userId - ..summary = [ - '`${activeUser.email}` removed `${memberToRemove.email}` ', - 'from publisher `$publisherId`.', - ].join() - ..data = { - 'publisherId': publisherId, - 'memberEmail': memberToRemove.email, - 'user': activeUser.email, - } - ..users = [activeUser.userId, memberToRemove.userId] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherMemberRemoved + ..agent = activeUser.userId + ..summary = [ + '`${activeUser.email}` removed `${memberToRemove.email}` ', + 'from publisher `$publisherId`.', + ].join() + ..data = { + 'publisherId': publisherId, + 'memberEmail': memberToRemove.email, + 'user': activeUser.email, + } + ..users = [activeUser.userId, memberToRemove.userId] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the publisher updated operation. @@ -684,18 +709,17 @@ class AuditLogRecord extends db.ExpandoModel { required User user, required String publisherId, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.publisherUpdated - ..agent = user.userId - ..summary = '`${user.email}` updated publisher `$publisherId`.' - ..data = { - 'publisherId': publisherId, - 'user': user.email, - } - ..users = [user.userId] - ..packages = [] - ..packageVersions = [] - ..publishers = [publisherId]); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.publisherUpdated + ..agent = user.userId + ..summary = '`${user.email}` updated publisher `$publisherId`.' + ..data = {'publisherId': publisherId, 'user': user.email} + ..users = [user.userId] + ..packages = [] + ..packageVersions = [] + ..publishers = [publisherId], + ); } /// Returns [AuditLogRecord] for the package uploader added operation. @@ -706,22 +730,24 @@ class AuditLogRecord extends db.ExpandoModel { required String package, required User uploaderUser, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.uploaderAdded - ..agent = activeUser.userId - ..summary = [ - '`${activeUser.email}` added `${uploaderUser.email}` ', - 'to the uploaders of package `$package`.', - ].join() - ..data = { - 'package': package, - 'uploaderEmail': uploaderUser.email, - 'user': activeUser.email, - } - ..users = [activeUser.userId, uploaderUser.userId] - ..packages = [package] - ..packageVersions = [] - ..publishers = []); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.uploaderAdded + ..agent = activeUser.userId + ..summary = [ + '`${activeUser.email}` added `${uploaderUser.email}` ', + 'to the uploaders of package `$package`.', + ].join() + ..data = { + 'package': package, + 'uploaderEmail': uploaderUser.email, + 'user': activeUser.email, + } + ..users = [activeUser.userId, uploaderUser.userId] + ..packages = [package] + ..packageVersions = [] + ..publishers = [], + ); } /// Returns [AuditLogRecord] for the package uploader invited operation. @@ -732,24 +758,24 @@ class AuditLogRecord extends db.ExpandoModel { required String package, required String uploaderEmail, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.uploaderInvited - ..agent = agent.agentId - ..summary = [ - '`${agent.displayId}` invited `$uploaderEmail` ', - 'to be an uploader for package `$package`.', - ].join() - ..data = { - 'package': package, - 'uploaderEmail': uploaderEmail, - if (agent.email != null) 'email': agent.email, - } - ..users = [ - if (agent is AuthenticatedUser) agent.userId, - ] - ..packages = [package] - ..packageVersions = [] - ..publishers = []); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.uploaderInvited + ..agent = agent.agentId + ..summary = [ + '`${agent.displayId}` invited `$uploaderEmail` ', + 'to be an uploader for package `$package`.', + ].join() + ..data = { + 'package': package, + 'uploaderEmail': uploaderEmail, + if (agent.email != null) 'email': agent.email, + } + ..users = [if (agent is AuthenticatedUser) agent.userId] + ..packages = [package] + ..packageVersions = [] + ..publishers = [], + ); } /// Returns [AuditLogRecord] for the package uploader invite accepted operation. @@ -759,20 +785,19 @@ class AuditLogRecord extends db.ExpandoModel { required User user, required String package, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.uploaderInviteAccepted - ..agent = user.userId - ..summary = [ - '`${user.email}` accepted uploader invite for package `$package`.', - ].join() - ..data = { - 'package': package, - 'user': user.email, - } - ..users = [user.userId] - ..packages = [package] - ..packageVersions = [] - ..publishers = []); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.uploaderInviteAccepted + ..agent = user.userId + ..summary = [ + '`${user.email}` accepted uploader invite for package `$package`.', + ].join() + ..data = {'package': package, 'user': user.email} + ..users = [user.userId] + ..packages = [package] + ..packageVersions = [] + ..publishers = [], + ); } /// Returns [AuditLogRecord] for the package uploader invite expired operation. @@ -783,22 +808,26 @@ class AuditLogRecord extends db.ExpandoModel { required String package, required String uploaderEmail, }) { - final fromUserId = - fromAgent != null && looksLikeUserId(fromAgent) ? fromAgent : null; - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.uploaderInviteExpired - ..agent = KnownAgents.pubSupport - ..summary = 'Uploader invite for package `$package` expired, ' - '`$uploaderEmail` did not respond.' - ..data = { - if (fromUserId != null) 'fromUserId': fromUserId, - 'package': package, - 'uploaderEmail': uploaderEmail, - } - ..users = [if (fromUserId != null) fromUserId] - ..packages = [package] - ..packageVersions = [] - ..publishers = []); + final fromUserId = fromAgent != null && looksLikeUserId(fromAgent) + ? fromAgent + : null; + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.uploaderInviteExpired + ..agent = KnownAgents.pubSupport + ..summary = + 'Uploader invite for package `$package` expired, ' + '`$uploaderEmail` did not respond.' + ..data = { + if (fromUserId != null) 'fromUserId': fromUserId, + 'package': package, + 'uploaderEmail': uploaderEmail, + } + ..users = [if (fromUserId != null) fromUserId] + ..packages = [package] + ..packageVersions = [] + ..publishers = [], + ); } /// Returns [AuditLogRecord] for the package uploader invite rejected operation. @@ -812,26 +841,29 @@ class AuditLogRecord extends db.ExpandoModel { /// Optional, in the future we may allow invite rejection without sign-in. required String? userId, }) { - final fromUserId = - fromAgent != null && looksLikeUserId(fromAgent) ? fromAgent : null; - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.uploaderInviteRejected - ..agent = userId ?? KnownAgents.pubSupport - ..summary = - '`$uploaderEmail` rejected uploader invite for package `$package`.' - ..data = { - if (fromUserId != null) 'fromUserId': fromUserId, - 'package': package, - 'uploaderEmail': uploaderEmail, - if (userId != null) 'userId': userId, - } - ..users = [ - if (fromUserId != null) fromUserId, - if (userId != null) userId, - ] - ..packages = [package] - ..packageVersions = [] - ..publishers = []); + final fromUserId = fromAgent != null && looksLikeUserId(fromAgent) + ? fromAgent + : null; + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.uploaderInviteRejected + ..agent = userId ?? KnownAgents.pubSupport + ..summary = + '`$uploaderEmail` rejected uploader invite for package `$package`.' + ..data = { + if (fromUserId != null) 'fromUserId': fromUserId, + 'package': package, + 'uploaderEmail': uploaderEmail, + if (userId != null) 'userId': userId, + } + ..users = [ + if (fromUserId != null) fromUserId, + if (userId != null) userId, + ] + ..packages = [package] + ..packageVersions = [] + ..publishers = [], + ); } /// Returns [AuditLogRecord] for the package uploader removed operation. @@ -842,25 +874,27 @@ class AuditLogRecord extends db.ExpandoModel { required String package, required User uploaderUser, }) { - return _checkRateLimit(AuditLogRecord._init() - ..kind = AuditLogRecordKind.uploaderRemoved - ..agent = agent.agentId - ..summary = [ - '`${agent.displayId}` removed `${uploaderUser.email}` ', - 'from the uploaders of package `$package`.', - ].join() - ..data = { - 'package': package, - 'uploaderEmail': uploaderUser.email, - if (agent.email != null) 'email': agent.email, - } - ..users = [ - if (agent is AuthenticatedUser) agent.userId, - uploaderUser.userId, - ] - ..packages = [package] - ..packageVersions = [] - ..publishers = []); + return _checkRateLimit( + AuditLogRecord._init() + ..kind = AuditLogRecordKind.uploaderRemoved + ..agent = agent.agentId + ..summary = [ + '`${agent.displayId}` removed `${uploaderUser.email}` ', + 'from the uploaders of package `$package`.', + ].join() + ..data = { + 'package': package, + 'uploaderEmail': uploaderUser.email, + if (agent.email != null) 'email': agent.email, + } + ..users = [ + if (agent is AuthenticatedUser) agent.userId, + uploaderUser.userId, + ] + ..packages = [package] + ..packageVersions = [] + ..publishers = [], + ); } } diff --git a/app/lib/dartdoc/dartdoc_page.dart b/app/lib/dartdoc/dartdoc_page.dart index 0f368bd136..2bb89ced2d 100644 --- a/app/lib/dartdoc/dartdoc_page.dart +++ b/app/lib/dartdoc/dartdoc_page.dart @@ -86,241 +86,260 @@ extension DartDocPageRender on DartDocPage { return true; } - d.Node _renderHead(DartDocPageOptions options) => - d.element('head', children: [ - // HACK: noindex logic is pub.dev specific - if (!_allowsRobotsIndexing(options)) - d.meta(name: 'robots', content: 'noindex'), - d.script( - type: 'text/javascript', - async: true, - src: 'https://www.googletagmanager.com/gtm.js?id=GTM-MX6DBN9'), - d.script(type: 'text/javascript', src: staticUrls.gtmJs), - d.meta(charset: 'utf-8'), - d.meta(httpEquiv: 'X-UA-Compatible', content: 'IE=edge'), - d.meta( - name: 'viewport', - content: - 'width=device-width, height=device-height, initial-scale=1, user-scalable=no'), - d.meta(name: 'generator', content: 'made with love by dartdoc'), - d.meta(name: 'description', content: description), - d.element('title', text: _pageTitle(options)), - d.link(rel: 'canonical', href: options.canonicalUrl), - // HACK: Inject alternate link, if not is latest stable version - if (!options.isLatestStable) - d.meta(rel: 'alternate', href: options.latestStableDocumentationUrl), - d.link(rel: 'preconnect', href: 'https://fonts.gstatic.com'), - d.link( - rel: 'stylesheet', - href: - 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,300;0,400;0,500;0,700;1,400&display=swap', - ), - d.link( - rel: 'stylesheet', - href: - 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0', - ), + d.Node _renderHead(DartDocPageOptions options) => d.element( + 'head', + children: [ + // HACK: noindex logic is pub.dev specific + if (!_allowsRobotsIndexing(options)) + d.meta(name: 'robots', content: 'noindex'), + d.script( + type: 'text/javascript', + async: true, + src: 'https://www.googletagmanager.com/gtm.js?id=GTM-MX6DBN9', + ), + d.script(type: 'text/javascript', src: staticUrls.gtmJs), + d.meta(charset: 'utf-8'), + d.meta(httpEquiv: 'X-UA-Compatible', content: 'IE=edge'), + d.meta( + name: 'viewport', + content: + 'width=device-width, height=device-height, initial-scale=1, user-scalable=no', + ), + d.meta(name: 'generator', content: 'made with love by dartdoc'), + d.meta(name: 'description', content: description), + d.element('title', text: _pageTitle(options)), + d.link(rel: 'canonical', href: options.canonicalUrl), + // HACK: Inject alternate link, if not is latest stable version + if (!options.isLatestStable) + d.meta(rel: 'alternate', href: options.latestStableDocumentationUrl), + d.link(rel: 'preconnect', href: 'https://fonts.gstatic.com'), + d.link( + rel: 'stylesheet', + href: + 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,300;0,400;0,500;0,700;1,400&display=swap', + ), + d.link( + rel: 'stylesheet', + href: + 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0', + ), - d.link(rel: 'stylesheet', href: staticUrls.dartdocCss), - d.link(rel: 'icon', href: staticUrls.smallDartFavicon), - ]); + d.link(rel: 'stylesheet', href: staticUrls.dartdocCss), + d.link(rel: 'icon', href: staticUrls.smallDartFavicon), + ], + ); // HACK: Breadcrumbs are different from what dartdoc produces! List _breadcrumbs(DartDocPageOptions options) => [ - // Prepend a ' package' breadcrumb leading back to pub.dev - Breadcrumb( - title: '${options.package} package', - href: options.pubPackagePageUrl, - ), - // Change the title of the first breadcrumb to be 'documentation' - if (breadcrumbs.firstOrNull != null) - Breadcrumb( - title: 'documentation', - href: breadcrumbs.first.href, - ), - ...breadcrumbs.skip(1), - ]; + // Prepend a ' package' breadcrumb leading back to pub.dev + Breadcrumb( + title: '${options.package} package', + href: options.pubPackagePageUrl, + ), + // Change the title of the first breadcrumb to be 'documentation' + if (breadcrumbs.firstOrNull != null) + Breadcrumb(title: 'documentation', href: breadcrumbs.first.href), + ...breadcrumbs.skip(1), + ]; - d.Node _renderHeader(DartDocPageOptions options) => - d.element('header', id: 'title', children: [ - d.span( - id: 'sidenav-left-toggle', - classes: ['material-symbols-outlined'], - attributes: {'role': 'button', 'tabindex': '0'}, - text: 'menu', - ), - d.a( - href: '/', - classes: ['hidden-xs'], - child: d.img( - attributes: { - 'aria-label': 'Go to the landing page of pub.dev', - // TODO: Move this into a class - 'style': 'height: 30px; margin-right: 1em;', - }, - image: d.Image.decorative( - src: staticUrls.dartLogoSvg, - height: 30, - width: 30, - ), + d.Node _renderHeader(DartDocPageOptions options) => d.element( + 'header', + id: 'title', + children: [ + d.span( + id: 'sidenav-left-toggle', + classes: ['material-symbols-outlined'], + attributes: {'role': 'button', 'tabindex': '0'}, + text: 'menu', + ), + d.a( + href: '/', + classes: ['hidden-xs'], + child: d.img( + attributes: { + 'aria-label': 'Go to the landing page of pub.dev', + // TODO: Move this into a class + 'style': 'height: 30px; margin-right: 1em;', + }, + image: d.Image.decorative( + src: staticUrls.dartLogoSvg, + height: 30, + width: 30, ), ), - d.element( - 'ol', - classes: ['breadcrumbs', 'gt-separated', 'dark', 'hidden-xs'], - children: [ - ..._breadcrumbs(options).map((crumb) => crumb.href != null - ? d.li(child: d.a(text: crumb.title, href: crumb.href)) - : d.li(text: crumb.title, classes: ['self-crumb'])) - ], - ), - d.div( - classes: ['self-name'], - text: breadcrumbs.lastOrNull?.title ?? '', + ), + d.element( + 'ol', + classes: ['breadcrumbs', 'gt-separated', 'dark', 'hidden-xs'], + children: [ + ..._breadcrumbs(options).map( + (crumb) => crumb.href != null + ? d.li( + child: d.a(text: crumb.title, href: crumb.href), + ) + : d.li(text: crumb.title, classes: ['self-crumb']), + ), + ], + ), + d.div(classes: ['self-name'], text: breadcrumbs.lastOrNull?.title ?? ''), + d.form( + classes: ['search', 'navbar-right'], + attributes: {'role': 'search'}, + child: d.input( + type: 'text', + id: 'search-box', + autocomplete: 'off', + disabled: false, + classes: ['form-control typeahead'], + placeholder: 'Loading search...', ), - d.form( - classes: ['search', 'navbar-right'], + ), + d.button( + id: 'theme-button', + classes: ['toggle'], + ariaLabel: 'Light and dark mode toggle', + attributes: {'title': 'Toggle between light and dark mode'}, + children: [ + d.span( + id: 'dark-theme-button', + classes: ['material-symbols-outlined'], + attributes: {'aria-hidden': 'true'}, + text: 'dark_mode', + ), + d.span( + id: 'light-theme-button', + classes: ['material-symbols-outlined'], + attributes: {'aria-hidden': 'true'}, + text: 'light_mode', + ), + ], + ), + ], + ); + + d.Node _renderMainContent(DartDocPageOptions options) => d.div( + id: 'dartdoc-main-content', + classes: ['main-content'], + attributes: { + if (aboveSidebarUrl != null) 'data-above-sidebar': aboveSidebarUrl!, + if (belowSidebarUrl != null) 'data-below-sidebar': belowSidebarUrl!, + }, + child: _content, + ); + + d.Node _renderLeftSideBar(DartDocPageOptions options) => d.div( + id: 'dartdoc-sidebar-left', + classes: ['sidebar', 'sidebar-offcanvas-left'], + children: [ + d.element( + 'header', + id: 'header-search-sidebar', + classes: ['hidden-l'], + child: d.form( + classes: ['search-sidebar'], attributes: {'role': 'search'}, child: d.input( + id: 'search-sidebar', type: 'text', - id: 'search-box', autocomplete: 'off', disabled: false, - classes: ['form-control typeahead'], + classes: ['form-control', 'typeahead'], placeholder: 'Loading search...', ), ), - d.button( - id: 'theme-button', - classes: ['toggle'], - ariaLabel: 'Light and dark mode toggle', - attributes: {'title': 'Toggle between light and dark mode'}, - children: [ - d.span( - id: 'dark-theme-button', - classes: ['material-symbols-outlined'], - attributes: {'aria-hidden': 'true'}, - text: 'dark_mode', - ), - d.span( - id: 'light-theme-button', - classes: ['material-symbols-outlined'], - attributes: {'aria-hidden': 'true'}, - text: 'light_mode', - ), - ], - ), - ]); - - d.Node _renderMainContent(DartDocPageOptions options) => d.div( - id: 'dartdoc-main-content', - classes: ['main-content'], - attributes: { - if (aboveSidebarUrl != null) 'data-above-sidebar': aboveSidebarUrl!, - if (belowSidebarUrl != null) 'data-below-sidebar': belowSidebarUrl!, - }, - child: _content, - ); - - d.Node _renderLeftSideBar(DartDocPageOptions options) => d.div( - id: 'dartdoc-sidebar-left', - classes: ['sidebar', 'sidebar-offcanvas-left'], + ), + d.element( + 'ol', + id: 'sidebar-nav', + classes: ['breadcrumbs', 'gt-separated', 'dark', 'hidden-l'], children: [ - d.element( - 'header', - id: 'header-search-sidebar', - classes: ['hidden-l'], - child: d.form( - classes: ['search-sidebar'], - attributes: {'role': 'search'}, - child: d.input( - id: 'search-sidebar', - type: 'text', - autocomplete: 'off', - disabled: false, - classes: ['form-control', 'typeahead'], - placeholder: 'Loading search...', - ), - ), - ), - d.element( - 'ol', - id: 'sidebar-nav', - classes: ['breadcrumbs', 'gt-separated', 'dark', 'hidden-l'], - children: [ - ..._breadcrumbs(options).map((crumb) => crumb.href != null - ? d.li( - child: d.a( + ..._breadcrumbs(options).map( + (crumb) => crumb.href != null + ? d.li( + child: d.a( text: crumb.titleWithoutDotDart, href: crumb.href, - )) - : d.li(text: crumb.title, classes: ['self-crumb'])) - ], + ), + ) + : d.li(text: crumb.title, classes: ['self-crumb']), ), - _left, ], - ); + ), + _left, + ], + ); d.Node _renderRightSideBar(DartDocPageOptions options) => d.div( - id: 'dartdoc-sidebar-right', - classes: ['sidebar', 'sidebar-offcanvas-right'], - child: _right, - ); + id: 'dartdoc-sidebar-right', + classes: ['sidebar', 'sidebar-offcanvas-right'], + child: _right, + ); - d.Node _renderMain(DartDocPageOptions options) => - d.element('main', children: [ - _renderMainContent(options), - _renderLeftSideBar(options), - _renderRightSideBar(options), - ]); + d.Node _renderMain(DartDocPageOptions options) => d.element( + 'main', + children: [ + _renderMainContent(options), + _renderLeftSideBar(options), + _renderRightSideBar(options), + ], + ); - d.Node _renderFooter(DartDocPageOptions options) => - d.element('footer', children: [ - d.span( - classes: ['no-break'], - text: '${options.package} ${options.version}', - ), - ]); + d.Node _renderFooter(DartDocPageOptions options) => d.element( + 'footer', + children: [ + d.span( + classes: ['no-break'], + text: '${options.package} ${options.version}', + ), + ], + ); d.Node _renderBody(DartDocPageOptions options) { final dataBaseHref = p.relative('', from: p.dirname(options.path)); - return d.element('body', classes: [ - 'light-theme', - ], attributes: { - 'data-base-href': - baseHref ?? (dataBaseHref == '.' ? '' : '$dataBaseHref/'), - 'data-using-base-href': usingBaseHref ?? 'false', - if (activeConfiguration.isStaging) 'data-staging': '1', - }, children: [ - if (activeConfiguration.isStaging) - d.div(classes: ['staging-ribbon'], text: 'staging'), - d.element('noscript', - child: d.element('iframe', attributes: { - 'src': 'https://www.googletagmanager.com/ns.html?id=GTM-MX6DBN9', - 'height': '0', - 'width': '0', - 'style': 'display:none;visibility:hidden', - })), - // NOTE: dartdoc's own initialization will still run, but it is not in conflict - // with the current script. - d.script(src: staticUrls.getAssetUrl('/static/js/dark-init.js')), - d.div(id: 'overlay-under-drawer'), - _renderHeader(options), - _renderMain(options), - _renderFooter(options), - // TODO: Consider using highlightjs we also use on pub.dev - d.script(src: staticUrls.dartdochighlightJs), - d.script(src: staticUrls.dartdocScriptJs), - ]); + return d.element( + 'body', + classes: ['light-theme'], + attributes: { + 'data-base-href': + baseHref ?? (dataBaseHref == '.' ? '' : '$dataBaseHref/'), + 'data-using-base-href': usingBaseHref ?? 'false', + if (activeConfiguration.isStaging) 'data-staging': '1', + }, + children: [ + if (activeConfiguration.isStaging) + d.div(classes: ['staging-ribbon'], text: 'staging'), + d.element( + 'noscript', + child: d.element( + 'iframe', + attributes: { + 'src': 'https://www.googletagmanager.com/ns.html?id=GTM-MX6DBN9', + 'height': '0', + 'width': '0', + 'style': 'display:none;visibility:hidden', + }, + ), + ), + // NOTE: dartdoc's own initialization will still run, but it is not in conflict + // with the current script. + d.script(src: staticUrls.getAssetUrl('/static/js/dark-init.js')), + d.div(id: 'overlay-under-drawer'), + _renderHeader(options), + _renderMain(options), + _renderFooter(options), + // TODO: Consider using highlightjs we also use on pub.dev + d.script(src: staticUrls.dartdochighlightJs), + d.script(src: staticUrls.dartdocScriptJs), + ], + ); } d.Node render(DartDocPageOptions options) => d.fragment([ - d.unsafeRawHtml('\n'), - d.element('html', attributes: { - 'lang': 'en' - }, children: [ - _renderHead(options), - _renderBody(options), - ]), - ]); + d.unsafeRawHtml('\n'), + d.element( + 'html', + attributes: {'lang': 'en'}, + children: [_renderHead(options), _renderBody(options)], + ), + ]); } diff --git a/app/lib/dartdoc/models.dart b/app/lib/dartdoc/models.dart index 73e61fa3d4..c16e35fc2d 100644 --- a/app/lib/dartdoc/models.dart +++ b/app/lib/dartdoc/models.dart @@ -29,10 +29,9 @@ class ResolvedDocUrlVersion { this.message, }); - ResolvedDocUrlVersion.empty({ - required this.message, - }) : version = '', - urlSegment = ''; + ResolvedDocUrlVersion.empty({required this.message}) + : version = '', + urlSegment = ''; factory ResolvedDocUrlVersion.fromJson(Map json) => _$ResolvedDocUrlVersionFromJson(json); @@ -50,11 +49,7 @@ class DocPageStatus { final String? redirectPath; final String? errorMessage; - DocPageStatus({ - required this.code, - this.redirectPath, - this.errorMessage, - }); + DocPageStatus({required this.code, this.redirectPath, this.errorMessage}); factory DocPageStatus.ok() { return DocPageStatus(code: DocPageStatusCode.ok); diff --git a/app/lib/dartdoc/models.g.dart b/app/lib/dartdoc/models.g.dart index 9846eff808..06e57e4289 100644 --- a/app/lib/dartdoc/models.g.dart +++ b/app/lib/dartdoc/models.g.dart @@ -7,20 +7,20 @@ part of 'models.dart'; // ************************************************************************** ResolvedDocUrlVersion _$ResolvedDocUrlVersionFromJson( - Map json) => - ResolvedDocUrlVersion( - version: json['version'] as String, - urlSegment: json['urlSegment'] as String, - message: json['message'] as String?, - ); + Map json, +) => ResolvedDocUrlVersion( + version: json['version'] as String, + urlSegment: json['urlSegment'] as String, + message: json['message'] as String?, +); Map _$ResolvedDocUrlVersionToJson( - ResolvedDocUrlVersion instance) => - { - 'version': instance.version, - 'urlSegment': instance.urlSegment, - if (instance.message case final value?) 'message': value, - }; + ResolvedDocUrlVersion instance, +) => { + 'version': instance.version, + 'urlSegment': instance.urlSegment, + if (instance.message case final value?) 'message': value, +}; DocPageStatus _$DocPageStatusFromJson(Map json) => DocPageStatus( diff --git a/app/lib/fake/backend/fake_auth_provider.dart b/app/lib/fake/backend/fake_auth_provider.dart index c2dfa495a4..6a46567646 100644 --- a/app/lib/fake/backend/fake_auth_provider.dart +++ b/app/lib/fake/backend/fake_auth_provider.dart @@ -38,9 +38,7 @@ class FakeAuthProvider extends BaseAuthProvider { Future close() async {} @override - Future callGetUserinfo({ - required String accessToken, - }) { + Future callGetUserinfo({required String accessToken}) { // Since we don't use getAccountProfile from the base class, this method // won't get called. throw AssertionError(); @@ -53,10 +51,13 @@ class FakeAuthProvider extends BaseAuthProvider { final token = JsonWebToken.tryParse(accessToken); if (token == null) { throw oauth2_v2.ApiRequestError( - 'Unable to parse access token: $accessToken'); + 'Unable to parse access token: $accessToken', + ); } final goodSignature = await verifyTokenSignature( - token: token, openIdDataFetch: () async => throw AssertionError()); + token: token, + openIdDataFetch: () async => throw AssertionError(), + ); if (!goodSignature) { throw oauth2_v2.ApiRequestError(null); } @@ -77,17 +78,16 @@ class FakeAuthProvider extends BaseAuthProvider { return http.Response(json.encode({}), 400); } final goodSignature = await verifyTokenSignature( - token: token, openIdDataFetch: () async => throw AssertionError()); + token: token, + openIdDataFetch: () async => throw AssertionError(), + ); if (!goodSignature) { return http.Response(json.encode({}), 400); } return http.Response( - json.encode({ - ...token.header, - ...token.payload, - 'email_verified': true, - }), - 200); + json.encode({...token.header, ...token.payload, 'email_verified': true}), + 200, + ); } @override @@ -141,13 +141,13 @@ class FakeAuthProvider extends BaseAuthProvider { final email = authResult.email; // using the user part as name - final name = - email.split('@').first.replaceAll('-', ' ').replaceAll('.', ' '); + final name = email + .split('@') + .first + .replaceAll('-', ' ') + .replaceAll('.', ' '); - return AccountProfile( - name: name, - imageUrl: staticUrls.defaultProfilePng, - ); + return AccountProfile(name: name, imageUrl: staticUrls.defaultProfilePng); } @override @@ -167,17 +167,12 @@ class FakeAuthProvider extends BaseAuthProvider { email: email, audience: activeConfiguration.pubServerAudience!, signature: null, - extraPayload: { - 'nonce': nonce, - }, + extraPayload: {'nonce': nonce}, scope: includeScopes?.join(' '), ); - return Uri.parse(getOauthRedirectUri()).replace( - queryParameters: { - 'state': encodeState(state), - 'code': token, - }, - ); + return Uri.parse( + getOauthRedirectUri(), + ).replace(queryParameters: {'state': encodeState(state), 'code': token}); } @override @@ -194,8 +189,11 @@ class FakeAuthProvider extends BaseAuthProvider { } final email = token.payload['email'] as String; // using the user part as name - final name = - email.split('@').first.replaceAll('-', ' ').replaceAll('.', ' '); + final name = email + .split('@') + .first + .replaceAll('-', ' ') + .replaceAll('.', ' '); return AuthResult( oauthUserId: token.payload['sub'] as String, @@ -214,13 +212,11 @@ class FakeAuthProvider extends BaseAuthProvider { } @visibleForTesting -String createFakeAuthTokenForEmail( - String email, { - String? audience, -}) { +String createFakeAuthTokenForEmail(String email, {String? audience}) { return Uri( - path: email.replaceAll('.', '-dot-').replaceAll('@', '-at-'), - queryParameters: {'aud': audience ?? 'fake-site-audience'}).toString(); + path: email.replaceAll('.', '-dot-').replaceAll('@', '-at-'), + queryParameters: {'aud': audience ?? 'fake-site-audience'}, + ).toString(); } @visibleForTesting @@ -246,10 +242,7 @@ String _createGcpToken({ String? scope, }) { final token = JsonWebToken( - header: { - 'alg': 'RS256', - 'typ': 'JWT', - }, + header: {'alg': 'RS256', 'typ': 'JWT'}, payload: { 'email': email, 'sub': fakeOauthUserIdFromEmail(email), @@ -287,16 +280,14 @@ String createFakeGitHubActionToken({ refType = refType.substring(0, refType.length - 1); } final token = JsonWebToken( - header: { - 'alg': 'RS256', - 'typ': 'JWT', - }, + header: {'alg': 'RS256', 'typ': 'JWT'}, payload: { 'aud': audience ?? 'https://pub.dev', 'repository': repository, 'repository_id': repositoryId ?? repository.hashCode.abs().toString(), 'repository_owner': repository.split('/').first, - 'repository_owner_id': repositoryOwnerId ?? + 'repository_owner_id': + repositoryOwnerId ?? repository.split('/').first.hashCode.abs().toString(), 'event_name': eventName ?? 'push', 'ref': ref, @@ -332,24 +323,27 @@ Future _acquireFakeSessionId({ String? pubHostedUrl, List? scopes, }) async { - final baseUri = - Uri.parse(pubHostedUrl ?? activeConfiguration.primarySiteUri.toString()); + final baseUri = Uri.parse( + pubHostedUrl ?? activeConfiguration.primarySiteUri.toString(), + ); final client = http.Client(); try { - final rs = await client.send(http.Request( - 'GET', - Uri( - scheme: baseUri.scheme, - host: baseUri.host, - port: baseUri.port, - path: '/sign-in', - queryParameters: { - 'fake-email': email, - 'go': '/help', - if (scopes != null) 'scope': scopes.join(' '), - }, - ), - )..followRedirects = false); + final rs = await client.send( + http.Request( + 'GET', + Uri( + scheme: baseUri.scheme, + host: baseUri.host, + port: baseUri.port, + path: '/sign-in', + queryParameters: { + 'fake-email': email, + 'go': '/help', + if (scopes != null) 'scope': scopes.join(' '), + }, + ), + )..followRedirects = false, + ); if (rs.statusCode != 303) { throw Exception('Unexpected status code: ${rs.statusCode}'); } @@ -382,20 +376,24 @@ Future _acquireCsrfToken({ required String sessionId, String? pubHostedUrl, }) async { - final baseUri = - Uri.parse(pubHostedUrl ?? activeConfiguration.primarySiteUri.toString()); + final baseUri = Uri.parse( + pubHostedUrl ?? activeConfiguration.primarySiteUri.toString(), + ); final client = http.Client(); try { - final rs = await client.send(http.Request( - 'GET', - Uri( - scheme: baseUri.scheme, - host: baseUri.host, - port: baseUri.port, - path: '/my-liked-packages', - ), - )..headers['cookie'] = - '$clientSessionLaxCookieName=$sessionId; $clientSessionStrictCookieName=$sessionId'); + final rs = await client.send( + http.Request( + 'GET', + Uri( + scheme: baseUri.scheme, + host: baseUri.host, + port: baseUri.port, + path: '/my-liked-packages', + ), + ) + ..headers['cookie'] = + '$clientSessionLaxCookieName=$sessionId; $clientSessionStrictCookieName=$sessionId', + ); if (rs.statusCode != 200) { throw Exception('Unexpected status code: ${rs.statusCode}.'); } @@ -460,10 +458,7 @@ Future createFakeAuthPubApiClient({ sessionId: sessionId, pubHostedUrl: pubHostedUrl, ); - return createPubApiClient( - sessionId: sessionId, - csrfToken: csrfToken, - ); + return createPubApiClient(sessionId: sessionId, csrfToken: csrfToken); } /// Creates a request context scope with the provided [email] using the fake @@ -478,15 +473,18 @@ Future withFakeAuthRequestContext( final sessionData = await accountBackend.getSessionData(sessionId); final experimentalFlags = requestContext.experimentalFlags; return await ss.fork(() async { - registerRequestContext(RequestContext( - clientSessionCookieStatus: ClientSessionCookieStatus( - sessionId: sessionId, - isStrict: true, - ), - sessionData: sessionData, - csrfToken: csrfToken, - experimentalFlags: experimentalFlags, - )); - return await fn(); - }) as R; + registerRequestContext( + RequestContext( + clientSessionCookieStatus: ClientSessionCookieStatus( + sessionId: sessionId, + isStrict: true, + ), + sessionData: sessionData, + csrfToken: csrfToken, + experimentalFlags: experimentalFlags, + ), + ); + return await fn(); + }) + as R; } diff --git a/app/lib/fake/backend/fake_download_counts.dart b/app/lib/fake/backend/fake_download_counts.dart index 9b3070a105..61b6722af0 100644 --- a/app/lib/fake/backend/fake_download_counts.dart +++ b/app/lib/fake/backend/fake_download_counts.dart @@ -24,18 +24,16 @@ Future generateFakeDownloadCountsInDatastore() async { await for (final p in query.run()) { final r = math.Random(p.name.hashCode.abs()); final count = (math.min(p.likes * p.likes, 50) + r.nextInt(50)); - await downloadCountsBackend.updateDownloadCounts( - p.name!, - { - p.latestVersion!: count, - }, - clock.now(), - ); + await downloadCountsBackend.updateDownloadCounts(p.name!, { + p.latestVersion!: count, + }, clock.now()); } } Future uploadFakeDownloadCountsToBucket( - String downloadCountsFileName, String dataFilePath) async { + String downloadCountsFileName, + String dataFilePath, +) async { final file = File(dataFilePath).readAsBytesSync(); await storageService .bucket(activeConfiguration.downloadCountsBucketName!) @@ -46,7 +44,9 @@ Future generateFake30DaysTotals(Map totals) async { await storageService .bucket(activeConfiguration.reportsBucketName!) .writeBytesWithRetry( - downloadCounts30DaysTotalsFileName, jsonUtf8Encoder.convert(totals)); + downloadCounts30DaysTotalsFileName, + jsonUtf8Encoder.convert(totals), + ); } Future generateFakeTrendScores(Map trends) async { diff --git a/app/lib/fake/backend/fake_email_sender.dart b/app/lib/fake/backend/fake_email_sender.dart index 006d3e2c60..6e1c00c91a 100644 --- a/app/lib/fake/backend/fake_email_sender.dart +++ b/app/lib/fake/backend/fake_email_sender.dart @@ -19,9 +19,7 @@ class FakeEmailSender implements EmailSender { final sentMessages = []; int failNextMessageCount = 0; - FakeEmailSender({ - String? outputDir, - }) : _outputDir = outputDir; + FakeEmailSender({String? outputDir}) : _outputDir = outputDir; @override bool get shouldBackoff => false; diff --git a/app/lib/fake/backend/fake_pana_runner.dart b/app/lib/fake/backend/fake_pana_runner.dart index 5edd4daea6..96d3d80191 100644 --- a/app/lib/fake/backend/fake_pana_runner.dart +++ b/app/lib/fake/backend/fake_pana_runner.dart @@ -46,8 +46,9 @@ Future fakePanaSummary({ 'platform:windows', ].where((p) => hasher(p, max: 5) > 0).toList() : []; - final licenseSpdx = - hasher('license', max: 5) == 0 ? 'unknown' : 'BSD-3-Clause'; + final licenseSpdx = hasher('license', max: 5) == 0 + ? 'unknown' + : 'BSD-3-Clause'; String? fakeUrlCheck(String key, String? url) { return hasher(key, max: 20) > 0 ? url : null; @@ -55,17 +56,23 @@ Future fakePanaSummary({ final homepageUrl = fakeUrlCheck('pubspec.homepage', pubspec.homepage); final repositoryUrl = fakeUrlCheck('pubspec.repository', pubspec.repository); - final issueTrackerUrl = - fakeUrlCheck('pubspec.issueTracker', pubspec.issueTracker); - final documentationUrl = - fakeUrlCheck('pubspec.documentation', pubspec.documentation); - final verifiedUrl = - Repository.tryParseUrl(repositoryUrl ?? homepageUrl ?? ''); + final issueTrackerUrl = fakeUrlCheck( + 'pubspec.issueTracker', + pubspec.issueTracker, + ); + final documentationUrl = fakeUrlCheck( + 'pubspec.documentation', + pubspec.documentation, + ); + final verifiedUrl = Repository.tryParseUrl( + repositoryUrl ?? homepageUrl ?? '', + ); final hasVerifiedRepository = verifiedUrl != null && hasher('verifiedRepository', max: 20) > 0; Repository? repository; if (hasVerifiedRepository) { - final verifiedRepositoryBranch = verifiedUrl.branch ?? + final verifiedRepositoryBranch = + verifiedUrl.branch ?? (hasher('verifiedRepository.branch', max: 5) > 0 ? 'main' : null); repository = Repository( provider: verifiedUrl.provider, @@ -77,20 +84,23 @@ Future fakePanaSummary({ } final contributingUrl = fakeUrlCheck( - 'contributingUrl', repository?.tryResolveUrl('CONTRIBUTING.md')); + 'contributingUrl', + repository?.tryResolveUrl('CONTRIBUTING.md'), + ); final result = AnalysisResult( - homepageUrl: homepageUrl, - repositoryUrl: repositoryUrl, - issueTrackerUrl: issueTrackerUrl, - documentationUrl: documentationUrl, - repository: repository, - fundingUrls: pubspec.funding - .map((e) => e.toString()) - .where((url) => fakeUrlCheck('funding', url) != null) - .toList(), - contributingUrl: contributingUrl, - licenses: [License(path: '', spdxIdentifier: licenseSpdx)]); + homepageUrl: homepageUrl, + repositoryUrl: repositoryUrl, + issueTrackerUrl: issueTrackerUrl, + documentationUrl: documentationUrl, + repository: repository, + fundingUrls: pubspec.funding + .map((e) => e.toString()) + .where((url) => fakeUrlCheck('funding', url) != null) + .toList(), + contributingUrl: contributingUrl, + licenses: [License(path: '', spdxIdentifier: licenseSpdx)], + ); return Summary( createdAt: clock.now().toUtc(), packageName: package, @@ -139,8 +149,9 @@ Future fakePanaSummary({ grantedPoints: examplePoints, maxPoints: 40, ), - status: - examplePoints > 20 ? ReportStatus.passed : ReportStatus.partial, + status: examplePoints > 20 + ? ReportStatus.passed + : ReportStatus.partial, ), ], ), @@ -178,8 +189,9 @@ String _renderSimpleSectionSummary({ required int grantedPoints, required int maxPoints, }) { - final mark = - grantedPoints == 0 ? 'x' : (grantedPoints == maxPoints ? '*' : '~'); + final mark = grantedPoints == 0 + ? 'x' + : (grantedPoints == maxPoints ? '*' : '~'); return [ '### [$mark] $grantedPoints/$maxPoints points: $title', ' * $description', diff --git a/app/lib/fake/backend/fake_pub_worker.dart b/app/lib/fake/backend/fake_pub_worker.dart index 484f0a9d95..c431b7a050 100644 --- a/app/lib/fake/backend/fake_pub_worker.dart +++ b/app/lib/fake/backend/fake_pub_worker.dart @@ -61,10 +61,7 @@ Future processTasksLocallyWithPubWorker() async { await instancesDeleted; // Stop the task backend, and instance execution - await Future.wait([ - taskBackend.stop(), - cloud.stopInstanceExecution(), - ]); + await Future.wait([taskBackend.stop(), cloud.stopInstanceExecution()]); } /// Process analysis tasks locally, using either: @@ -101,8 +98,10 @@ Future _fakeAnalysis(Payload payload) async { try { final api = PubApiClient(payload.pubHostedUrl, client: client); await withTempDirectory((tempDir) async { - final packageStatus = - await scoreCardBackend.getPackageStatus(payload.package, v.version); + final packageStatus = await scoreCardBackend.getPackageStatus( + payload.package, + v.version, + ); final random = Random('${payload.package}/${v.version}'.hashCode); final documented = random.nextInt(21); @@ -132,8 +131,9 @@ Future _fakeAnalysis(Payload payload) async { final builder = IndexedBlobBuilder(blobFile.openWrite()); Future addFileAsStringGzipped(String path, String content) async { - final stream = - Stream.fromIterable([gzip.encode(utf8.encode(content))]); + final stream = Stream.fromIterable([ + gzip.encode(utf8.encode(content)), + ]); await builder.addFile(path, stream); } @@ -141,7 +141,9 @@ Future _fakeAnalysis(Payload payload) async { await addFileAsStringGzipped('doc/${e.key}', e.value); } await addFileAsStringGzipped( - 'summary.json', json.encode(summary.toJson())); + 'summary.json', + json.encode(summary.toJson()), + ); await addFileAsStringGzipped('log.txt', 'started\nstopped\n'); final index = await builder.buildIndex(r.blobId); @@ -198,7 +200,8 @@ Future _analyzeWorker() async { final exitCode = await p.exitCode; if (exitCode != 0) { throw Exception( - 'Failed to analyze ${payload.package} with exitCode $exitCode'); + 'Failed to analyze ${payload.package} with exitCode $exitCode', + ); } }); }); @@ -206,7 +209,8 @@ Future _analyzeWorker() async { Future fakeCloudComputeInstanceRunner(FakeCloudInstance instance) async { final payload = Payload.fromJson( - json.decode(instance.arguments.first) as Map); + json.decode(instance.arguments.first) as Map, + ); await _fakeAnalysis(payload); } @@ -217,43 +221,44 @@ Map _fakeDartdocFiles( required int total, }) { final pubData = { - 'coverage': { - 'documented': documented, - 'total': total, - }, + 'coverage': {'documented': documented, 'total': total}, 'apiElements': [ // TODO: add fake library elements ], }; return { - 'index.html': json.encode(DartDocPage( - title: 'index', - description: 'index description', - breadcrumbs: [], - content: 'content', - left: 'left', - right: 'right', - baseHref: null, - usingBaseHref: null, - aboveSidebarUrl: null, - belowSidebarUrl: null, - redirectPath: null, - ).toJson()), + 'index.html': json.encode( + DartDocPage( + title: 'index', + description: 'index description', + breadcrumbs: [], + content: 'content', + left: 'left', + right: 'right', + baseHref: null, + usingBaseHref: null, + aboveSidebarUrl: null, + belowSidebarUrl: null, + redirectPath: null, + ).toJson(), + ), 'index.json': '{}', 'pub-data.json': json.encode(pubData), - 'search.html': json.encode(DartDocPage( - title: 'search', - description: 'search description', - breadcrumbs: [], - content: 'content', - left: 'left', - right: 'right', - baseHref: null, - usingBaseHref: null, - aboveSidebarUrl: null, - belowSidebarUrl: null, - redirectPath: null, - ).toJson()), + 'search.html': json.encode( + DartDocPage( + title: 'search', + description: 'search description', + breadcrumbs: [], + content: 'content', + left: 'left', + right: 'right', + baseHref: null, + usingBaseHref: null, + aboveSidebarUrl: null, + belowSidebarUrl: null, + redirectPath: null, + ).toJson(), + ), }; } @@ -270,39 +275,40 @@ Future _upload( int length, { required String filename, String contentType = 'application/octet-stream', -}) async => - await retry(() async { - final req = MultipartRequest('POST', Uri.parse(destination.url)) - ..fields.addAll(destination.fields ?? {}) - ..followRedirects = false - ..files.add(MultipartFile( - 'file', - content(), - length, - filename: filename, - contentType: MediaType.parse(contentType), - )); - final res = await Response.fromStream(await client.send(req)); - - if (400 <= res.statusCode && res.statusCode < 500) { - throw UploadException( - 'HTTP error, status = ${res.statusCode}, body: ${res.body}', - ); - } - if (500 <= res.statusCode && res.statusCode < 600) { - throw IntermittentUploadException( - 'HTTP intermittent error, status = ${res.statusCode}, body: ${res.body}', - ); - } - if (200 <= res.statusCode && res.statusCode < 300) { - return; - } +}) async => await retry(() async { + final req = MultipartRequest('POST', Uri.parse(destination.url)) + ..fields.addAll(destination.fields ?? {}) + ..followRedirects = false + ..files.add( + MultipartFile( + 'file', + content(), + length, + filename: filename, + contentType: MediaType.parse(contentType), + ), + ); + final res = await Response.fromStream(await client.send(req)); - // Unhandled response code -> retry - throw UploadException( - 'Unhandled HTTP status = ${res.statusCode}, body: ${res.body}', - ); - }, retryIf: (e) => e is IOException || e is IntermittentUploadException); + if (400 <= res.statusCode && res.statusCode < 500) { + throw UploadException( + 'HTTP error, status = ${res.statusCode}, body: ${res.body}', + ); + } + if (500 <= res.statusCode && res.statusCode < 600) { + throw IntermittentUploadException( + 'HTTP intermittent error, status = ${res.statusCode}, body: ${res.body}', + ); + } + if (200 <= res.statusCode && res.statusCode < 300) { + return; + } + + // Unhandled response code -> retry + throw UploadException( + 'Unhandled HTTP status = ${res.statusCode}, body: ${res.body}', + ); +}, retryIf: (e) => e is IOException || e is IntermittentUploadException); final class UploadException implements Exception { final String message; diff --git a/app/lib/fake/backend/fake_topics.dart b/app/lib/fake/backend/fake_topics.dart index 81d44603da..9bdd82f392 100644 --- a/app/lib/fake/backend/fake_topics.dart +++ b/app/lib/fake/backend/fake_topics.dart @@ -12,6 +12,8 @@ import '../../shared/utils.dart'; Future generateFakeTopicValues() async { await storageService .bucket(activeConfiguration.reportsBucketName!) - .writeBytesWithRetry(topicsJsonFileName, - jsonUtf8Encoder.convert({'ffi': 7, 'ui': 5, 'network': 6})); + .writeBytesWithRetry( + topicsJsonFileName, + jsonUtf8Encoder.convert({'ffi': 7, 'ui': 5, 'network': 6}), + ); } diff --git a/app/lib/fake/server/fake_analyzer_service.dart b/app/lib/fake/server/fake_analyzer_service.dart index 318e8045db..acd0568f7f 100644 --- a/app/lib/fake/server/fake_analyzer_service.dart +++ b/app/lib/fake/server/fake_analyzer_service.dart @@ -33,39 +33,42 @@ class FakeAnalyzerService { required Configuration configuration, }) async { await withFakeServices( - configuration: configuration, - datastore: _datastore, - storage: _storage, - cloudCompute: _cloudCompute, - fn: () async { - await generateFakeDownloadCountsInDatastore(); + configuration: configuration, + datastore: _datastore, + storage: _storage, + cloudCompute: _cloudCompute, + fn: () async { + await generateFakeDownloadCountsInDatastore(); - final handler = wrapHandler(_logger, analyzerServiceHandler); - final server = await IOServer.bind('localhost', port); - serveRequests(server.server, (request) async { - return (await ss.fork(() async { - if (request.requestedUri.path == '/fake-update-all') { - return shelf.Response.badRequest( - body: 'endpoint no longer supported'); - } - return await handler(request); - }) as shelf.Response?)!; - }); - _logger.info('running on port $port'); + final handler = wrapHandler(_logger, analyzerServiceHandler); + final server = await IOServer.bind('localhost', port); + serveRequests(server.server, (request) async { + return (await ss.fork(() async { + if (request.requestedUri.path == '/fake-update-all') { + return shelf.Response.badRequest( + body: 'endpoint no longer supported', + ); + } + return await handler(request); + }) + as shelf.Response?)!; + }); + _logger.info('running on port $port'); - (taskWorkerCloudCompute as FakeCloudCompute).startInstanceExecution(); - ss.registerScopeExitCallback( - (taskWorkerCloudCompute as FakeCloudCompute) - .stopInstanceExecution); - await taskBackend.backfillTrackingState(); - await taskBackend.start(); + (taskWorkerCloudCompute as FakeCloudCompute).startInstanceExecution(); + ss.registerScopeExitCallback( + (taskWorkerCloudCompute as FakeCloudCompute).stopInstanceExecution, + ); + await taskBackend.backfillTrackingState(); + await taskBackend.start(); - await ProcessSignal.sigint.watch().first; + await ProcessSignal.sigint.watch().first; - _logger.info('shutting down'); - await server.close(); - _logger.info('closing'); - }); + _logger.info('shutting down'); + await server.close(); + _logger.info('closing'); + }, + ); _logger.info('closed'); } } diff --git a/app/lib/fake/server/fake_default_service.dart b/app/lib/fake/server/fake_default_service.dart index b4718f3996..4280da8d6c 100644 --- a/app/lib/fake/server/fake_default_service.dart +++ b/app/lib/fake/server/fake_default_service.dart @@ -29,9 +29,12 @@ class FakePubServer { final bool _watch; final FakeCloudCompute _cloudCompute; - FakePubServer(this._datastore, this._storage, this._cloudCompute, - {bool? watch}) - : _watch = watch ?? false; + FakePubServer( + this._datastore, + this._storage, + this._cloudCompute, { + bool? watch, + }) : _watch = watch ?? false; Future run({ required int port, @@ -39,39 +42,44 @@ class FakePubServer { required shelf.Handler extraHandler, }) async { await withFakeServices( - configuration: configuration, - datastore: _datastore, - storage: _storage, - cloudCompute: _cloudCompute, - fn: () async { - if (_watch) { - await watchForResourceChanges(); - } + configuration: configuration, + datastore: _datastore, + storage: _storage, + cloudCompute: _cloudCompute, + fn: () async { + if (_watch) { + await watchForResourceChanges(); + } - await generateFakeDownloadCountsInDatastore(); - await generateFakeTopicValues(); - await nameTracker.startTracking(); + await generateFakeDownloadCountsInDatastore(); + await generateFakeTopicValues(); + await nameTracker.startTracking(); - final appHandler = createAppHandler(); - final handler = wrapHandler(Logger('fake_server.default'), appHandler, - sanitize: true); + final appHandler = createAppHandler(); + final handler = wrapHandler( + Logger('fake_server.default'), + appHandler, + sanitize: true, + ); - final server = await IOServer.bind('localhost', port); - serveRequests(server.server, (request) async { - return (await ss.fork(() async { - final rs = await extraHandler(request); - if (rs.statusCode != 404) return rs; - return await handler(request); - }) as shelf.Response?)!; - }); - print('running on port $port'); + final server = await IOServer.bind('localhost', port); + serveRequests(server.server, (request) async { + return (await ss.fork(() async { + final rs = await extraHandler(request); + if (rs.statusCode != 404) return rs; + return await handler(request); + }) + as shelf.Response?)!; + }); + print('running on port $port'); - await waitForProcessSignalTermination(); + await waitForProcessSignalTermination(); - _logger.info('shutting down'); - await server.close(); - _logger.info('closing'); - }); + _logger.info('shutting down'); + await server.close(); + _logger.info('closing'); + }, + ); _logger.info('closed'); } } diff --git a/app/lib/fake/server/fake_search_service.dart b/app/lib/fake/server/fake_search_service.dart index 8c4ad559fa..efbac3b8fe 100644 --- a/app/lib/fake/server/fake_search_service.dart +++ b/app/lib/fake/server/fake_search_service.dart @@ -34,38 +34,40 @@ class FakeSearchService { required Configuration configuration, }) async { await withFakeServices( - configuration: configuration, - datastore: _datastore, - storage: _storage, - cloudCompute: _cloudCompute, - fn: () async { - registerSdkIndex(await createSdkMemIndex()); - final handler = wrapHandler(_logger, searchServiceHandler); - final server = await IOServer.bind('localhost', port); - serveRequests(server.server, (request) async { - return (await ss.fork(() async { - if (request.requestedUri.path == '/fake-update-all') { - _logger.info('Triggered update all...'); - // ignore: invalid_use_of_visible_for_testing_member - await indexUpdater.updateAllPackages(); - _logger.info('Completed update all...'); - return shelf.Response.ok(''); - } - return await handler(request); - }) as shelf.Response?)!; - }); - _logger.info('running on port $port'); + configuration: configuration, + datastore: _datastore, + storage: _storage, + cloudCompute: _cloudCompute, + fn: () async { + registerSdkIndex(await createSdkMemIndex()); + final handler = wrapHandler(_logger, searchServiceHandler); + final server = await IOServer.bind('localhost', port); + serveRequests(server.server, (request) async { + return (await ss.fork(() async { + if (request.requestedUri.path == '/fake-update-all') { + _logger.info('Triggered update all...'); + // ignore: invalid_use_of_visible_for_testing_member + await indexUpdater.updateAllPackages(); + _logger.info('Completed update all...'); + return shelf.Response.ok(''); + } + return await handler(request); + }) + as shelf.Response?)!; + }); + _logger.info('running on port $port'); - await generateFakeDownloadCountsInDatastore(); - // ignore: invalid_use_of_visible_for_testing_member - await indexUpdater.updateAllPackages(); + await generateFakeDownloadCountsInDatastore(); + // ignore: invalid_use_of_visible_for_testing_member + await indexUpdater.updateAllPackages(); - await ProcessSignal.sigint.watch().first; + await ProcessSignal.sigint.watch().first; - _logger.info('shutting down'); - await server.close(); - _logger.info('closing'); - }); + _logger.info('shutting down'); + await server.close(); + _logger.info('closing'); + }, + ); _logger.info('closed'); } } diff --git a/app/lib/fake/server/fake_server_entrypoint.dart b/app/lib/fake/server/fake_server_entrypoint.dart index 63c51b516e..c7233c3d00 100644 --- a/app/lib/fake/server/fake_server_entrypoint.dart +++ b/app/lib/fake/server/fake_server_entrypoint.dart @@ -33,23 +33,38 @@ class FakeServerCommand extends Command { FakeServerCommand() { argParser - ..addOption('port', - defaultsTo: '8080', help: 'The HTTP port of the fake pub server.') - ..addOption('storage-port', - defaultsTo: '8081', - help: 'The HTTP port for the fake storage server.') - ..addOption('search-port', - defaultsTo: '8082', - help: 'The HTTP port for the fake search service.') - ..addOption('analyzer-port', - defaultsTo: '8083', - help: 'The HTTP port for the fake analyzer service.') - ..addOption('data-file', - help: 'The file to read and also to store the local state.') - ..addFlag('watch', - help: 'Monitor changes of local files and reload them.') - ..addFlag('read-only', - help: 'Only read the data from the data-file, do not store it.'); + ..addOption( + 'port', + defaultsTo: '8080', + help: 'The HTTP port of the fake pub server.', + ) + ..addOption( + 'storage-port', + defaultsTo: '8081', + help: 'The HTTP port for the fake storage server.', + ) + ..addOption( + 'search-port', + defaultsTo: '8082', + help: 'The HTTP port for the fake search service.', + ) + ..addOption( + 'analyzer-port', + defaultsTo: '8083', + help: 'The HTTP port for the fake analyzer service.', + ) + ..addOption( + 'data-file', + help: 'The file to read and also to store the local state.', + ) + ..addFlag( + 'watch', + help: 'Monitor changes of local files and reload them.', + ) + ..addFlag( + 'read-only', + help: 'Only read the data from the data-file, do not store it.', + ); } @override @@ -96,13 +111,16 @@ class FakeServerCommand extends Command { ); Future _updateUpstream(int port) async { - final rs = - await post(Uri.parse('http://localhost:$port/fake-update-all')); + final rs = await post( + Uri.parse('http://localhost:$port/fake-update-all'), + ); if (rs.statusCode == 200) { return shelf.Response.ok('OK'); } else { - return shelf.Response(503, - body: 'Upstream service ($port) returned ${rs.statusCode}.'); + return shelf.Response( + 503, + body: 'Upstream service ($port) returned ${rs.statusCode}.', + ); } } @@ -140,25 +158,16 @@ class FakeServerCommand extends Command { return shelf.Response.notFound('Not Found.'); } - await Future.wait( - [ - storageServer.run(port: storagePort), - pubServer.run( - port: port, - configuration: configuration, - extraHandler: forwardUpdatesHandler, - ), - searchService.run( - port: searchPort, - configuration: configuration, - ), - analyzerService.run( - port: analyzerPort, - configuration: configuration, - ), - ], - eagerError: true, - ); + await Future.wait([ + storageServer.run(port: storagePort), + pubServer.run( + port: port, + configuration: configuration, + extraHandler: forwardUpdatesHandler, + ), + searchService.run(port: searchPort, configuration: configuration), + analyzerService.run(port: analyzerPort, configuration: configuration), + ], eagerError: true); if (!readOnly && dataFile != null) { await state.save(dataFile); @@ -169,8 +178,9 @@ class FakeServerCommand extends Command { Future _testProfile(shelf.Request rq) async { final map = json.decode(await rq.readAsString()) as Map; - final profile = - TestProfile.fromJson(map['testProfile'] as Map); + final profile = TestProfile.fromJson( + map['testProfile'] as Map, + ); // ignore: invalid_use_of_visible_for_testing_member await importProfile(profile: profile); final analysis = (map['analysis'] as String?) ?? 'fake'; diff --git a/app/lib/fake/server/fake_storage_server.dart b/app/lib/fake/server/fake_storage_server.dart index b30aa519cf..a332e79df6 100644 --- a/app/lib/fake/server/fake_storage_server.dart +++ b/app/lib/fake/server/fake_storage_server.dart @@ -59,8 +59,10 @@ class FakeStorageServer { return Response.notFound('404 Not Found'); } final contentType = lookupMimeType(objectName); - return Response.ok([], - headers: {if (contentType != null) 'Content-Type': contentType}); + return Response.ok( + [], + headers: {if (contentType != null) 'Content-Type': contentType}, + ); } else if (request.method == 'GET') { _logger.info('Requested: ${request.requestedUri.path}'); final segments = request.requestedUri.pathSegments; @@ -78,10 +80,13 @@ class FakeStorageServer { exists.metadata.contentType ?? lookupMimeType(objectName); final contentEncoding = exists.metadata.contentEncoding; - return Response.ok(bucket.read(objectName), headers: { - if (contentType != null) 'Content-Type': contentType, - if (contentEncoding != null) 'Content-Encoding': contentEncoding, - }); + return Response.ok( + bucket.read(objectName), + headers: { + if (contentType != null) 'Content-Type': contentType, + if (contentEncoding != null) 'Content-Encoding': contentEncoding, + }, + ); } else if (request.method == 'POST') { _logger.info('Uploading: ${request.requestedUri.path}'); final contentHeader = _parse(request.headers['content-type']); diff --git a/app/lib/fake/server/local_server_state.dart b/app/lib/fake/server/local_server_state.dart index fb51307b0e..f8e519c5d2 100644 --- a/app/lib/fake/server/local_server_state.dart +++ b/app/lib/fake/server/local_server_state.dart @@ -19,8 +19,10 @@ class LocalServerState { Future loadIfExists(String path) async { final file = File(path); if (file.existsSync()) { - final lines = - file.openRead().transform(utf8.decoder).transform(LineSplitter()); + final lines = file + .openRead() + .transform(utf8.decoder) + .transform(LineSplitter()); var marker = 'start'; await for (final line in lines) { if (line.startsWith('{"marker":')) { diff --git a/app/lib/fake/tool/init_data_file.dart b/app/lib/fake/tool/init_data_file.dart index 05542a951c..ba67dd15dc 100644 --- a/app/lib/fake/tool/init_data_file.dart +++ b/app/lib/fake/tool/init_data_file.dart @@ -27,8 +27,10 @@ class FakeInitDataFileCommand extends Command { FakeInitDataFileCommand() { argParser - ..addOption('test-profile', - help: 'The file to read the test profile from.') + ..addOption( + 'test-profile', + help: 'The file to read the test profile from.', + ) ..addOption( 'analysis', allowed: ['none', 'fake', 'local', 'worker'], @@ -42,12 +44,14 @@ class FakeInitDataFileCommand extends Command { @override Future run() async { Logger.root.onRecord.listen((r) { - print([ - r.time.toIso8601String(), - r.toString(), - r.error, - r.stackTrace?.toString(), - ].nonNulls.join(' ')); + print( + [ + r.time.toIso8601String(), + r.toString(), + r.error, + r.stackTrace?.toString(), + ].nonNulls.join(' '), + ); }); final analysis = argResults!['analysis'] as String; @@ -67,16 +71,18 @@ class FakeInitDataFileCommand extends Command { final state = LocalServerState(); await withFakeServices( - datastore: state.datastore, - storage: state.storage, - fn: () async { - // ignore: invalid_use_of_visible_for_testing_member - await importProfile( - profile: profile, - source: ImportSource(pubDevArchiveCachePath: archiveCachePath)); + datastore: state.datastore, + storage: state.storage, + fn: () async { + // ignore: invalid_use_of_visible_for_testing_member + await importProfile( + profile: profile, + source: ImportSource(pubDevArchiveCachePath: archiveCachePath), + ); - await processTaskFakeLocalOrWorker(analysis); - }); + await processTaskFakeLocalOrWorker(analysis); + }, + ); await state.save(dataFile); } } diff --git a/app/lib/frontend/dom/dom.dart b/app/lib/frontend/dom/dom.dart index 07e3a1945e..9b05fb6297 100644 --- a/app/lib/frontend/dom/dom.dart +++ b/app/lib/frontend/dom/dom.dart @@ -80,16 +80,15 @@ Node element( Iterable? children, Node? child, String? text, -}) => - dom.element( - tag, - id: id, - classes: classes, - attributes: attributes, - children: children, - child: child, - text: text, - ); +}) => dom.element( + tag, + id: id, + classes: classes, + attributes: attributes, + children: children, + child: child, + text: text, +); /// Creates a DOM Text node using the default [DomContext]. Node text(String value) => dom.text(value); @@ -124,6 +123,7 @@ Node xAgoTimestamp(DateTime timestamp, {String? datePrefix}) { /// Creates a DOM node with markdown content using the default [DomContext]. Node markdown( String text, { + /// When set, the markdown output is pruned more heavily than usual, like: /// - generated hash IDs are removed /// - relative URLs of links and images are removed (and replaced with their text content) @@ -137,12 +137,7 @@ Node markdown( disableHashIds: strictPruning, urlResolverFn: strictPruning ? null - : ( - url, { - String? relativeFrom, - bool? isEmbeddedObject, - }) => - url, + : (url, {String? relativeFrom, bool? isEmbeddedObject}) => url, ), ); } @@ -175,13 +170,12 @@ Node ldJson(Map content) { void write(dynamic value) { if (value is String) { sb.write('"'); - sb.write(value.replaceAllMapped( - _ldJsonEscapedCharactersRegExp, - (m) { + sb.write( + value.replaceAllMapped(_ldJsonEscapedCharactersRegExp, (m) { final code = m[0]!.codeUnitAt(0); return r'\u' + code.toRadixString(16).padLeft(4, '0'); - }, - )); + }), + ); sb.write('"'); } else if (value is List) { sb.write('['); @@ -204,7 +198,8 @@ Node ldJson(Map content) { sb.write(json.encode(value)); } else { throw ArgumentError( - 'Value `$value` could not be translated to JSON, unexpected type: `${value.runtimeType}`.'); + 'Value `$value` could not be translated to JSON, unexpected type: `${value.runtimeType}`.', + ); } } @@ -255,16 +250,15 @@ Node b({ Iterable? children, Node? child, String? text, -}) => - dom.element( - 'b', - id: id, - classes: classes, - attributes: attributes, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'b', + id: id, + classes: classes, + attributes: attributes, + children: children, + child: child, + text: text, +); /// Creates a `
` Element using the default [DomContext]. Node br() => element('br'); @@ -278,19 +272,15 @@ Node button({ Node? child, String? text, String? ariaLabel, -}) => - dom.element( - 'button', - id: id, - classes: classes, - attributes: { - if (ariaLabel != null) 'aria-label': ariaLabel, - ...?attributes, - }, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'button', + id: id, + classes: classes, + attributes: {if (ariaLabel != null) 'aria-label': ariaLabel, ...?attributes}, + children: children, + child: child, + text: text, +); /// Creates a `` Element using the default [DomContext]. Node code({ @@ -300,16 +290,15 @@ Node code({ Iterable? children, Node? child, String? text, -}) => - dom.element( - 'code', - id: id, - classes: classes, - attributes: attributes, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'code', + id: id, + classes: classes, + attributes: attributes, + children: children, + child: child, + text: text, +); /// Creates a `
` Element using the default [DomContext]. Node details({ @@ -345,16 +334,15 @@ Node div({ Iterable? children, Node? child, String? text, -}) => - dom.element( - 'div', - id: id, - classes: classes, - attributes: attributes, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'div', + id: id, + classes: classes, + attributes: attributes, + children: children, + child: child, + text: text, +); /// Creates an `
` Element using the default [DomContext]. Node form({ @@ -390,16 +378,15 @@ Node h1({ Iterable? children, Node? child, String? text, -}) => - dom.element( - 'h1', - id: id, - classes: classes, - attributes: attributes, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'h1', + id: id, + classes: classes, + attributes: attributes, + children: children, + child: child, + text: text, +); /// Creates a `

` Element using the default [DomContext]. Node h2({ @@ -409,16 +396,15 @@ Node h2({ Iterable? children, Node? child, String? text, -}) => - dom.element( - 'h2', - id: id, - classes: classes, - attributes: attributes, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'h2', + id: id, + classes: classes, + attributes: attributes, + children: children, + child: child, + text: text, +); /// Creates a `

` Element using the default [DomContext]. Node h3({ @@ -428,16 +414,15 @@ Node h3({ Iterable? children, Node? child, String? text, -}) => - dom.element( - 'h3', - id: id, - classes: classes, - attributes: attributes, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'h3', + id: id, + classes: classes, + attributes: attributes, + children: children, + child: child, + text: text, +); /// Creates an `` Element using the default [DomContext]. Node i({ @@ -491,8 +476,8 @@ class Image { required this.src, required this.width, required this.height, - }) : alt = '', - role = 'presentation'; + }) : alt = '', + role = 'presentation'; } /// Creates an `` Element using the default [DomContext]. @@ -567,16 +552,15 @@ Node label({ Iterable? children, Node? child, String? text, -}) => - dom.element( - 'label', - id: id, - classes: classes, - attributes: attributes, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'label', + id: id, + classes: classes, + attributes: attributes, + children: children, + child: child, + text: text, +); /// Creates a `
  • ` Element using the default [DomContext]. Node li({ @@ -586,16 +570,15 @@ Node li({ Iterable? children, Node? child, String? text, -}) => - dom.element( - 'li', - id: id, - classes: classes, - attributes: attributes, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'li', + id: id, + classes: classes, + attributes: attributes, + children: children, + child: child, + text: text, +); /// Creates a `` Element using the default [DomContext]. Node link({ @@ -610,23 +593,22 @@ Node link({ String? title, String? href, String? as, -}) => - dom.element( - 'link', - id: id, - classes: classes, - attributes: { - if (rel != null) 'rel': rel, - if (type != null) 'type': type, - if (title != null) 'title': title, - if (href != null) 'href': href, - if (as != null) 'as': as, - ...?attributes, - }, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'link', + id: id, + classes: classes, + attributes: { + if (rel != null) 'rel': rel, + if (type != null) 'type': type, + if (title != null) 'title': title, + if (href != null) 'href': href, + if (as != null) 'as': as, + ...?attributes, + }, + children: children, + child: child, + text: text, +); /// Creates a `` Element using the default [DomContext]. Node meta({ @@ -643,25 +625,24 @@ Node meta({ String? content, String? rel, String? href, -}) => - dom.element( - 'meta', - id: id, - classes: classes, - attributes: { - if (httpEquiv != null) 'http-equiv': httpEquiv, - if (name != null) 'name': name, - if (property != null) 'property': property, - if (charset != null) 'charset': charset, - if (content != null) 'content': content, - if (rel != null) 'rel': rel, - if (href != null) 'href': href, - ...?attributes, - }, - children: children, - child: child, - text: text, - ); +}) => dom.element( + 'meta', + id: id, + classes: classes, + attributes: { + if (httpEquiv != null) 'http-equiv': httpEquiv, + if (name != null) 'name': name, + if (property != null) 'property': property, + if (charset != null) 'charset': charset, + if (content != null) 'content': content, + if (rel != null) 'rel': rel, + if (href != null) 'href': href, + ...?attributes, + }, + children: children, + child: child, + text: text, +); /// Creates a `