Skip to content

Commit

Permalink
Report page with subject parameter + its form processing.
Browse files Browse the repository at this point in the history
  • Loading branch information
isoos committed Apr 24, 2024
1 parent 6b143bd commit ff8bb69
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 26 deletions.
90 changes: 70 additions & 20 deletions app/lib/admin/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,12 @@ class ModerationCase extends db.ExpandoModel<String> {
@db.StringProperty(required: true)
late String kind;

/// The kind of the entity this notification or appeal concerns. On of:
/// `package`, `package-version` or `publisher`.
@db.StringProperty()
String? subjectKind;

/// The fully qualified name of the entity this notification or appeal concerns.
/// - `package:<package>`
/// - `package-version:<package>/<version>`
/// - `publisher:<publisherId>`
@db.StringProperty()
String? subjectFqn;

/// The local name of the entity (without the type qualifier) this notification or appeal concerns.
/// - `package`: the package name
/// - `package-version`: the `<package>/<version>`
/// - `publisher`: the publisher ID
String? subjectLocalName;
String? subject;

/// The `caseId` of the appeal (or null).
@db.StringProperty()
Expand Down Expand Up @@ -90,17 +79,10 @@ class ModerationCase extends db.ExpandoModel<String> {
required this.detectedBy,
required this.kind,
required this.status,
this.subjectKind,
this.subjectLocalName,
this.subject,
}) {
id = caseId;
opened = clock.now().toUtc();
if (subjectKind != null &&
subjectKind!.isNotEmpty &&
subjectLocalName != null &&
subjectLocalName!.isNotEmpty) {
subjectFqn = '$subjectKind:$subjectLocalName';
}
}
}

Expand All @@ -115,3 +97,71 @@ abstract class ModerationKind {
abstract class ModerationStatus {
static const pending = 'pending';
}

class ModerationSubject {
final String kind;
final String localName;
final String? package;
final String? version;
final String? publisherId;

ModerationSubject._({
required this.kind,
required this.localName,
this.package,
this.version,
this.publisherId,
});

static ModerationSubject? tryParse(String value) {
final parts = value.split(':');
if (parts.length != 2) {
return null;
}
if (parts.any((p) => p.isEmpty || p.trim() != p)) {
return null;
}
final kind = parts.first;
switch (kind) {
case ModerationSubjectKind.package:
final package = parts[1];
return ModerationSubject._(
kind: kind,
localName: package,
package: package,
);
case ModerationSubjectKind.packageVersion:
final localName = parts[1];
final pvParts = localName.split('/');
if (pvParts.length != 2) {
return null;
}
final package = pvParts.first;
final version = pvParts[1];
return ModerationSubject._(
kind: kind,
localName: localName,
package: package,
version: version,
);
case ModerationSubjectKind.publisher:
final publisherId = parts[1];
return ModerationSubject._(
kind: kind,
localName: publisherId,
publisherId: publisherId,
);
default:
return null;
}
}

late final fqn = '$kind:$localName';
}

class ModerationSubjectKind {
static const unspecified = 'unspecified';
static const package = 'package';
static const packageVersion = 'package-version';
static const publisher = 'publisher';
}
47 changes: 47 additions & 0 deletions app/lib/frontend/handlers/report.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import '../../account/backend.dart';
import '../../admin/models.dart';
import '../../frontend/email_sender.dart';
import '../../frontend/handlers/cache_control.dart';
import '../../package/backend.dart';
import '../../publisher/backend.dart';
import '../../shared/datastore.dart';
import '../../shared/email.dart';
import '../../shared/exceptions.dart';
Expand All @@ -26,14 +28,50 @@ Future<shelf.Response> reportPageHandler(shelf.Request request) async {
return notFoundHandler(request);
}

final subjectParam = request.requestedUri.queryParameters['subject'];
ModerationSubject? subject;
if (subjectParam != null) {
subject = ModerationSubject.tryParse(subjectParam);
if (subject == null) {
throw InvalidInputException('Invalid "subject" parameter.');
}
await _verifySubject(subject);
}

return htmlResponse(
renderReportPage(
sessionData: requestContext.sessionData,
subject: subject,
),
headers: CacheControl.explicitlyPrivate.headers,
);
}

Future<void> _verifySubject(ModerationSubject? subject) async {
final package = subject?.package;
final version = subject?.version;
final publisherId = subject?.publisherId;
if (package != null) {
final p = await packageBackend.lookupPackage(package);
if (p == null) {
throw NotFoundException('Package "$package" does not exist.');
}
if (version != null) {
final pv = await packageBackend.lookupPackageVersion(package, version);
if (pv == null) {
throw NotFoundException(
'Package version "$package/$version" does not exist.');
}
}
}
if (publisherId != null) {
final p = await publisherBackend.getPublisher(publisherId);
if (p == null) {
throw NotFoundException('Publisher "$publisherId" does not exist.');
}
}
}

/// Handles POST /api/report
Future<String> processReportPageHandler(
shelf.Request request, ReportForm form) async {
Expand All @@ -56,6 +94,13 @@ Future<String> processReportPageHandler(
InvalidInputException.checkNull(form.email, 'email');
}

ModerationSubject? subject;
if (form.subject != null) {
subject = ModerationSubject.tryParse(form.subject!);
InvalidInputException.check(subject != null, 'Invalid subject.');
await _verifySubject(subject);
}

InvalidInputException.checkStringLength(
form.message,
'message',
Expand All @@ -73,12 +118,14 @@ Future<String> processReportPageHandler(
detectedBy: ModerationDetectedBy.externalNotification,
kind: ModerationKind.notification,
status: ModerationStatus.pending,
subject: subject?.fqn,
);
tx.insert(mc);
});

final bodyText = <String>[
'New report recieved on ${now.toIso8601String()}: $caseId',
if (subject != null) 'Subject: ${subject.fqn}',
'Message:\n${form.message}',
].join('\n\n');

Expand Down
16 changes: 16 additions & 0 deletions app/lib/frontend/templates/report.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@
// BSD-style license that can be found in the LICENSE file.

import '../../account/models.dart';
import '../../admin/models.dart';
import '../dom/dom.dart' as d;
import '../dom/material.dart' as material;
import 'layout.dart';

const _subjectKindLabels = {
ModerationSubjectKind.package: 'Package: ',
ModerationSubjectKind.packageVersion: 'Package version: ',
ModerationSubjectKind.publisher: 'Publisher: ',
};

/// Renders the create publisher page.
String renderReportPage({
SessionData? sessionData,
ModerationSubject? subject,
}) {
return renderLayoutPage(
PageType.standalone,
Expand All @@ -31,6 +39,14 @@ String renderReportPage({
label: 'Email',
),
]),
if (subject != null)
d.fragment([
d.input(type: 'hidden', name: 'subject', value: subject.fqn),
d.p(children: [
d.text(_subjectKindLabels[subject.kind] ?? ''),
d.code(text: subject.localName),
]),
]),
d.p(text: 'Please describe the issue you want to report:'),
material.textArea(
id: 'message',
Expand Down
7 changes: 3 additions & 4 deletions app/lib/search/search_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:_pub_shared/search/tags.dart';
import 'package:clock/clock.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:pub_dev/admin/actions/actions.dart';
import 'package:pub_dev/shared/utils.dart';

part 'search_service.g.dart';

Expand Down Expand Up @@ -148,8 +149,6 @@ class ApiDocPage {
Map<String, dynamic> toJson() => _$ApiDocPageToJson(this);
}

String? _stringToNull(String? v) => (v == null || v.isEmpty) ? null : v;

class ServiceSearchQuery {
final String? query;
final ParsedQueryText parsedQuery;
Expand All @@ -176,7 +175,7 @@ class ServiceSearchQuery {
this.limit,
}) : parsedQuery = ParsedQueryText.parse(query),
tagsPredicate = tagsPredicate ?? TagsPredicate(),
publisherId = _stringToNull(publisherId);
publisherId = publisherId?.trimToNull();

factory ServiceSearchQuery.parse({
String? query,
Expand All @@ -188,7 +187,7 @@ class ServiceSearchQuery {
int offset = 0,
int? limit = 10,
}) {
final q = _stringToNull(query?.trim());
final q = query?.trimToNull();
return ServiceSearchQuery._(
query: q,
tagsPredicate: tagsPredicate,
Expand Down
7 changes: 7 additions & 0 deletions app/lib/shared/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,10 @@ bool fixedTimeIntListEquals(List<int> a, List<int> b) {
}
return result == 0;
}

extension StringExt on String {
String? trimToNull() {
final v = trim();
return v.isEmpty ? null : v;
}
}
44 changes: 44 additions & 0 deletions app/test/admin/models_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2022, 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/models.dart';
import 'package:test/test.dart';

void main() {
group('ModerationSubject', () {
test('Invalid values', () {
expect(ModerationSubject.tryParse(''), null);
expect(ModerationSubject.tryParse('x:x'), null);
expect(ModerationSubject.tryParse('package'), null);
expect(ModerationSubject.tryParse('package:'), null);
});

test('package', () {
final ms = ModerationSubject.tryParse('package:x');
expect(ms!.kind, ModerationSubjectKind.package);
expect(ms.localName, 'x');
expect(ms.package, 'x');
expect(ms.version, isNull);
expect(ms.publisherId, isNull);
});

test('package version', () {
final ms = ModerationSubject.tryParse('package-version:x/1.0.0');
expect(ms!.kind, ModerationSubjectKind.packageVersion);
expect(ms.localName, 'x/1.0.0');
expect(ms.package, 'x');
expect(ms.version, '1.0.0');
expect(ms.publisherId, isNull);
});

test('publisher', () {
final ms = ModerationSubject.tryParse('publisher:example.com');
expect(ms!.kind, ModerationSubjectKind.publisher);
expect(ms.localName, 'example.com');
expect(ms.package, isNull);
expect(ms.version, isNull);
expect(ms.publisherId, 'example.com');
});
});
}
Loading

0 comments on commit ff8bb69

Please sign in to comment.