Skip to content

Commit

Permalink
Setting + using Package.isModerated flag. (#7547)
Browse files Browse the repository at this point in the history
  • Loading branch information
isoos authored Mar 12, 2024
1 parent e299cf6 commit 4058eff
Show file tree
Hide file tree
Showing 10 changed files with 414 additions and 7 deletions.
2 changes: 2 additions & 0 deletions app/lib/admin/actions/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:pub_dev/admin/actions/create_publisher.dart';
import 'package:pub_dev/admin/actions/delete_publisher.dart';

import 'package:pub_dev/admin/actions/merge_moderated_package_into_existing.dart';
import 'package:pub_dev/admin/actions/moderate_package.dart';
import 'package:pub_dev/admin/actions/publisher_block.dart';
import 'package:pub_dev/admin/actions/publisher_members_list.dart';
import 'package:pub_dev/admin/actions/remove_package_from_publisher.dart';
Expand Down Expand Up @@ -71,6 +72,7 @@ final class AdminAction {
createPublisher,
deletePublisher,
mergeModeratedPackageIntoExisting,
moderatePackage,
publisherBlock,
publisherMembersList,
removePackageFromPublisher,
Expand Down
77 changes: 77 additions & 0 deletions app/lib/admin/actions/moderate_package.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:pub_dev/admin/actions/actions.dart';
import 'package:pub_dev/package/backend.dart';
import 'package:pub_dev/package/models.dart';
import 'package:pub_dev/shared/datastore.dart';
import 'package:pub_dev/tool/maintenance/update_public_bucket.dart';

final moderatePackage = AdminAction(
name: 'moderate-package',
summary: 'Set the moderated flag on a package (making it not visible).',
description: '''
Set the moderated flag on a package (updating the flag and the timestamp).
''',
options: {
'package': 'The package name to be moderated',
'state':
'Set moderated state true / false. Returns current state if omitted.',
},
invoke: (options) async {
final package = options['package'];
InvalidInputException.check(
package != null && package.isNotEmpty,
'package must be given',
);

final state = options['state'];
bool? valueToSet;
switch (state) {
case 'true':
valueToSet = true;
break;
case 'false':
valueToSet = false;
break;
}

final p = await packageBackend.lookupPackage(package!);
if (p == null) {
throw NotFoundException.resource(package);
}

Package? p2;
if (valueToSet != null) {
p2 = await withRetryTransaction(dbService, (tx) async {
final pkg = await tx.lookupValue<Package>(p.key);
pkg.updateIsModerated(isModerated: valueToSet!);
tx.insert(pkg);
return pkg;
});

// retract public archive files
await updatePublicArchiveBucket(
package: package,
ageCheckThreshold: Duration.zero,
deleteIfOlder: Duration.zero,
);

await purgePackageCache(package);
}

return {
'package': p.name,
'before': {
'isModerated': p.isModerated,
'moderatedAt': p.moderatedAt?.toIso8601String(),
},
if (p2 != null)
'after': {
'isModerated': p2.isModerated,
'moderatedAt': p2.moderatedAt?.toIso8601String(),
},
};
},
);
4 changes: 4 additions & 0 deletions app/lib/frontend/handlers/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ Future<shelf.Response> _handlePackagePage({
if (cachedPage == null) {
final package = await packageBackend.lookupPackage(packageName);
if (package == null || !package.isVisible) {
if (package?.isModerated ?? false) {
final content = renderModeratedPackagePage(packageName);
return htmlResponse(content, status: 404);
}
if (await packageBackend.isPackageModerated(packageName)) {
final content = renderModeratedPackagePage(packageName);
return htmlResponse(content, status: 404);
Expand Down
2 changes: 1 addition & 1 deletion app/lib/frontend/templates/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,6 @@ List<Tab> buildPackageTabs({

/// Renders the package page when the package has been moderated.
String renderModeratedPackagePage(String packageName) {
final message = 'The package `$packageName` has been removed.';
final message = 'The package `$packageName` has been moderated.';
return renderErrorPage(default404NotFound, message);
}
2 changes: 1 addition & 1 deletion app/lib/package/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ class PackageBackend {
/// Returns false if the user is not an admin.
/// Returns false if the package is not visible e.g. blocked.
Future<bool> isPackageAdmin(Package p, String userId) async {
if (p.isBlocked) {
if (p.isNotVisible) {
return false;
}
if (p.publisherId == null) {
Expand Down
11 changes: 10 additions & 1 deletion app/lib/package/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,13 @@ class Package extends db.ExpandoModel<String> {

// Convenience Fields:

bool get isVisible => !isBlocked;
bool get isVisible => !isBlocked && !(isModerated ?? false);
bool get isNotVisible => !isVisible;

bool get isIncludedInRobots {
final now = clock.now();
return isVisible &&
!(isModerated ?? false) &&
!isDiscontinued &&
!isUnlisted &&
now.difference(created!) > robotsVisibilityMinAge &&
Expand Down Expand Up @@ -414,6 +415,14 @@ class Package extends db.ExpandoModel<String> {
blocked = isBlocked ? clock.now().toUtc() : null;
updated = clock.now().toUtc();
}

void updateIsModerated({
required bool isModerated,
}) {
this.isModerated = isModerated;
moderatedAt = isModerated ? clock.now().toUtc() : null;
updated = clock.now().toUtc();
}
}

/// Describes the various categories of latest releases.
Expand Down
1 change: 1 addition & 0 deletions app/lib/search/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ class SearchBackend {
Stream<PackageDocument> loadMinimumPackageIndex() async* {
final query = _db.query<Package>();
await for (final p in query.run()) {
if (p.isNotVisible) continue;
final releases = await packageBackend.latestReleases(p);
yield PackageDocument(
package: p.name!,
Expand Down
24 changes: 20 additions & 4 deletions app/lib/tool/maintenance/update_public_bucket.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import 'package:gcloud/storage.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:pub_dev/package/backend.dart';
import 'package:pub_dev/package/models.dart';
import 'package:pub_dev/shared/configuration.dart';
Expand Down Expand Up @@ -35,28 +34,42 @@ class PublicBucketUpdateStat {
/// Return the number of objects that were updated.
Future<PublicBucketUpdateStat> updatePublicArchiveBucket({
String? package,
@visibleForTesting Duration ageCheckThreshold = const Duration(days: 1),
@visibleForTesting Duration deleteIfOlder = const Duration(days: 7),
Duration ageCheckThreshold = const Duration(days: 1),
Duration deleteIfOlder = const Duration(days: 7),
}) async {
_logger.info('Scanning PackageVersions for public bucket updates...');

var updatedCount = 0;
var toBeDeletedCount = 0;
final deleteObjects = <String>[];
final deleteObjects = <String>{};
final canonicalBucket =
storageService.bucket(activeConfiguration.canonicalPackagesBucketName!);
final publicBucket =
storageService.bucket(activeConfiguration.publicPackagesBucketName!);

final objectNamesInPublicBucket = <String>{};

Package? lastPackage;
final pvStream = package == null
? dbService.query<PackageVersion>().run()
: packageBackend.streamVersionsOfPackage(package);
await for (final pv in pvStream) {
if (lastPackage?.name != pv.package) {
lastPackage = await packageBackend.lookupPackage(pv.package);
}
final isModerated =
(lastPackage!.isModerated ?? false) || (pv.isModerated ?? false);

final objectName = tarballObjectName(pv.package, pv.version!);
final publicInfo = await publicBucket.tryInfo(objectName);

if (isModerated) {
if (publicInfo != null) {
deleteObjects.add(objectName);
}
continue;
}

if (publicInfo == null) {
_logger.warning('Updating missing object in public bucket: $objectName');
try {
Expand Down Expand Up @@ -89,6 +102,9 @@ Future<PublicBucketUpdateStat> updatePublicArchiveBucket({
if (objectNamesInPublicBucket.contains(entry.name)) {
continue;
}
if (deleteObjects.contains(entry.name)) {
continue;
}

final publicInfo = await publicBucket.tryInfo(entry.name);
if (publicInfo == null) {
Expand Down
14 changes: 14 additions & 0 deletions app/test/frontend/handlers/_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
library pub_dartlang_org.frontend.handlers_test;

import 'dart:async';
import 'dart:convert';

import 'package:_pub_shared/validation/html/html_validation.dart';
import 'package:gcloud/service_scope.dart' as ss;
Expand Down Expand Up @@ -134,6 +135,19 @@ Future<String> expectHtmlResponse(
return content;
}

Future<Map<String, dynamic>> expectJsonMapResponse(
shelf.Response response, {
int status = 200,
Object? matcher,
}) async {
expect(response.statusCode, status);
expect(response.headers['content-type'], 'application/json; charset="utf-8"');
final content = await response.readAsString();
final map = json.decode(content) as Map<String, dynamic>;
expect(map, matcher ?? isNotNull);
return map;
}

Future<String> expectAtomXmlResponse(shelf.Response response,
{int status = 200}) async {
expect(response.statusCode, status);
Expand Down
Loading

0 comments on commit 4058eff

Please sign in to comment.