Skip to content

Commit 6fb4de9

Browse files
authored
Add handling of content reports (#949)
* add config vars for sending complaint mails * add ComplaintNotification * clearer naming * add ComplaintController * add route * add ExternalComplaintNotification * templating * switch order of notifications * add WIP tests * restrict email validation to rfc filter * fix email validation rule condition * add multi mail address test case * refactored class naming * WIP persist notifications in database * add ComplaintRecord class * extend test cases * adjust field names to UI code * fix testRecordOnMailFail status code assert * adjust parameter names in test case * reduce size limit of text fields to 1000 * fix field names in test * remove form contents from externail mail notification
1 parent d5475b2 commit 6fb4de9

File tree

9 files changed

+567
-0
lines changed

9 files changed

+567
-0
lines changed

app/ComplaintRecord.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Support\Carbon;
8+
9+
/**
10+
* App\ComplaintRecord.
11+
*
12+
* @property int $id
13+
* @property string|null $name
14+
* @property string|null $mail_address
15+
* @property string $reason
16+
* @property string $offending_urls
17+
* @property \Illuminate\Support\Carbon|null $dispatched_at
18+
* @property \Illuminate\Support\Carbon|null $created_at
19+
* @property \Illuminate\Support\Carbon|null $updated_at
20+
* @mixin \Eloquent
21+
*/
22+
class ComplaintRecord extends Model
23+
{
24+
use HasFactory;
25+
26+
protected $fillable = [
27+
'name',
28+
'mail_address',
29+
'reason',
30+
'offending_urls',
31+
];
32+
33+
public function markAsDispatched()
34+
{
35+
$this->dispatched_at = Carbon::now();
36+
}
37+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Notifications\ComplaintNotificationExternal;
6+
use App\Notifications\ComplaintNotification;
7+
use App\Rules\ReCaptchaValidation;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Support\Facades\Validator;
10+
use Illuminate\Support\Facades\Notification;
11+
use Illuminate\Validation\Rule;
12+
use App\ComplaintRecord;
13+
use Illuminate\Support\Facades\Log;
14+
15+
class ComplaintController extends Controller
16+
{
17+
/**
18+
* @var \App\Rules\ReCaptchaValidation
19+
*/
20+
protected $recaptchaValidation;
21+
22+
public function __construct(ReCaptchaValidation $recaptchaValidation) {
23+
$this->recaptchaValidation = $recaptchaValidation;
24+
}
25+
26+
/**
27+
* Handle a complaint report page request for the application.
28+
*
29+
* @param \Illuminate\Http\Request $request
30+
*/
31+
public function sendMessage(Request $request): \Illuminate\Http\JsonResponse
32+
{
33+
$validator = $this->validator($request->all());
34+
35+
if ($validator->fails()) {
36+
$failed = $validator->failed();
37+
38+
if (isset($failed['recaptcha'])) {
39+
abort(401);
40+
} else {
41+
abort(400);
42+
}
43+
}
44+
45+
$validated = $validator->safe();
46+
47+
$complaintRecord = new ComplaintRecord;
48+
$complaintRecord->name = $validated['name'];
49+
$complaintRecord->mail_address = $validated['email'];
50+
$complaintRecord->reason = $validated['message'];
51+
$complaintRecord->offending_urls = $validated['url'];
52+
$complaintRecord->save();
53+
54+
if (! empty($complaintRecord->mail_address)) {
55+
Notification::route('mail', [
56+
$complaintRecord->mail_address,
57+
])->notify(
58+
new ComplaintNotificationExternal(
59+
$complaintRecord->offending_urls,
60+
$complaintRecord->reason,
61+
$complaintRecord->name,
62+
$complaintRecord->mail_address,
63+
)
64+
);
65+
}
66+
67+
Notification::route('mail', [
68+
config('app.complaint-mail-recipient'),
69+
])->notify(
70+
new ComplaintNotification(
71+
$complaintRecord->offending_urls,
72+
$complaintRecord->reason,
73+
$complaintRecord->name,
74+
$complaintRecord->mail_address,
75+
)
76+
);
77+
78+
$complaintRecord->markAsDispatched();
79+
$complaintRecord->save();
80+
81+
return response()->json('Success', 200);
82+
}
83+
84+
/**
85+
* Get a validator for an incoming complaint report page request.
86+
*/
87+
protected function validator(array $data): \Illuminate\Validation\Validator
88+
{
89+
$data['name'] = $data['name'] ?? '';
90+
$data['email'] = $data['email'] ?? '';
91+
92+
$validation = [
93+
'recaptcha' => ['required', 'string', 'bail', $this->recaptchaValidation],
94+
'name' => ['nullable', 'string', 'max:300'],
95+
'message' => ['required', 'string', 'max:1000'],
96+
'url' => ['required', 'string', 'max:1000'],
97+
98+
'email' => [
99+
'nullable',
100+
'max:300',
101+
Rule::when(
102+
!empty($data['email']),
103+
['email:rfc']
104+
),
105+
],
106+
];
107+
108+
return Validator::make($data, $validation);
109+
}
110+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace App\Notifications;
4+
5+
use Illuminate\Notifications\Messages\DatabaseMessage;
6+
use Illuminate\Notifications\Messages\MailMessage;
7+
use Illuminate\Notifications\Notification;
8+
use Illuminate\Support\Facades\Lang;
9+
10+
/**
11+
* A notification to be sent when the legal complaint form is being used.
12+
*/
13+
class ComplaintNotification extends Notification
14+
{
15+
public $offendingUrls;
16+
public $reason;
17+
public $name;
18+
public $mailAddress;
19+
20+
/**
21+
* Create a notification instance.
22+
*
23+
* @param string $offendingUrls
24+
* @param string $reason
25+
* @param string $name
26+
* @param string $mailAddress
27+
* @return void
28+
*/
29+
public function __construct($offendingUrls, $reason, $name=null, $mailAddress=null)
30+
{
31+
$this->offendingUrls = $offendingUrls;
32+
$this->reason = $reason;
33+
$this->name = $name;
34+
$this->mailAddress = $mailAddress;
35+
}
36+
37+
/**
38+
* Get the notification's channels.
39+
*
40+
* @param mixed $notifiable
41+
* @return array|string
42+
*/
43+
public function via($notifiable)
44+
{
45+
return ['database', 'mail'];
46+
}
47+
48+
/**
49+
* Build the mail representation of the notification.
50+
*
51+
* @param mixed $notifiable
52+
* @return \Illuminate\Notifications\Messages\MailMessage
53+
*/
54+
public function toMail($notifiable)
55+
{
56+
$name = $this->name;
57+
$mailAddress = $this->mailAddress;
58+
59+
if (empty($name)) {
60+
$name = 'None';
61+
}
62+
63+
if (empty($mailAddress)) {
64+
$mailAddress = 'None';
65+
}
66+
67+
$mailFrom = config('app.complaint-mail-sender');
68+
$mailSubject = config('app.name') . ': Report of Illegal Content';
69+
70+
return (new MailMessage)
71+
->from($mailFrom)
72+
->subject($mailSubject)
73+
->line(Lang::get('A message via the wikibase.cloud form for reporting illegal content has been submitted.'))
74+
->line(Lang::get('Reporter name: ') . $name)
75+
->line(Lang::get('Reporter email address: ') . $mailAddress)
76+
->line(Lang::get('Reason why the information in question is illegal content:'))
77+
->line($this->reason)
78+
->line(Lang::get('URL(s) for the content in question:'))
79+
->line($this->offendingUrls)
80+
->line('---');
81+
}
82+
83+
public function toDatabase($notifiable) {
84+
$mail = $this->toMail($notifiable);
85+
86+
return (new DatabaseMessage($mail->toArray()));
87+
}
88+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Notifications;
4+
5+
use App\Notifications\ComplaintNotification;
6+
use Illuminate\Notifications\Messages\MailMessage;
7+
use Illuminate\Support\Facades\Lang;
8+
9+
/**
10+
* A notification to be sent when the legal complaint form is being used.
11+
*/
12+
class ComplaintNotificationExternal extends ComplaintNotification
13+
{
14+
/**
15+
* Build the mail representation of the notification.
16+
*
17+
* @param mixed $notifiable
18+
* @return \Illuminate\Notifications\Messages\MailMessage
19+
*/
20+
public function toMail($notifiable)
21+
{
22+
$name = $this->name;
23+
24+
if (empty($name)) {
25+
$name = 'None';
26+
}
27+
28+
$mailFrom = config('app.complaint-mail-sender');
29+
$mailSubject = config('app.name') . ': Report of Illegal Content';
30+
31+
return (new MailMessage)
32+
->from($mailFrom)
33+
->subject($mailSubject)
34+
->line(Lang::get('Your message via the wikibase.cloud form for reporting illegal content has been submitted.'))
35+
->line('---');
36+
}
37+
}

config/app.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
'contact-mail-recipient' => env('WBSTACK_CONTACT_MAIL_RECIPIENT', 'wikibase-cloud-owner@lists.wikimedia.org'),
1111
'contact-mail-sender' => env('WBSTACK_CONTACT_MAIL_SENDER', 'contact-<subject>@wikibase.cloud'),
1212

13+
'complaint-mail-recipient' => env('WBSTACK_COMPLAINT_MAIL_RECIPIENT', 'dsa-meldung@wikimedia.de'),
14+
'complaint-mail-sender' => env('WBSTACK_COMPLAINT_MAIL_SENDER', 'dsa@wikibase.cloud'),
15+
1316
/*
1417
|--------------------------------------------------------------------------
1518
| Application Name
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Database\Factories;
4+
5+
use App\ComplaintRecord;
6+
use Illuminate\Database\Eloquent\Factories\Factory;
7+
8+
/**
9+
* @template TModel of \App\ComplaintRecord
10+
*
11+
* @extends \Illuminate\Database\Eloquent\Factories\Factory<TModel>
12+
*/
13+
class ComplaintRecordFactory extends Factory
14+
{
15+
/**
16+
* The name of the factory's corresponding model.
17+
*
18+
* @var class-string<TModel>
19+
*/
20+
protected $model = ComplaintRecord::class;
21+
22+
/**
23+
* Define the model's default state.
24+
*
25+
* @return array<string, mixed>
26+
*/
27+
public function definition(): array
28+
{
29+
return [
30+
'dispatched' => null,
31+
];
32+
}
33+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::create('complaint_records', function (Blueprint $table) {
15+
$table->id();
16+
$table->timestamps();
17+
$table->timestamp('dispatched_at')->nullable();
18+
$table->string('name')->nullable();
19+
$table->string('mail_address')->nullable();
20+
$table->text('reason');
21+
$table->text('offending_urls');
22+
});
23+
}
24+
25+
/**
26+
* Reverse the migrations.
27+
*/
28+
public function down(): void
29+
{
30+
Schema::dropIfExists('complaint_records');
31+
}
32+
};

routes/api.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
$router->post('user/forgotPassword', ['uses' => 'Auth\ForgotPasswordController@sendResetLinkEmail']);
1919
$router->post('user/resetPassword', ['uses' => 'Auth\ResetPasswordController@reset']);
2020
$router->post('contact/sendMessage', ['uses' => 'ContactController@sendMessage']);
21+
$router->post('complaint/sendMessage', ['uses' => 'ComplaintController@sendMessage']);
2122

2223
$router->post('auth/login', ['uses' => 'Auth\LoginController@postLogin'])->name('login');
2324
// Authed

0 commit comments

Comments
 (0)