Skip to content

Commit dad5c82

Browse files
chiragchhatralamadassdevJhumanJ
authored
Apply Mentions everywhere (#595)
* variables and mentions * fix lint * add missing changes * fix tests * update quilly, fix bugs * fix lint * apply fixes * apply fixes * Fix MentionParser * Apply Mentions everywhere * Fix MentionParserTest * Small refactoring * Fixing quill import issues * Polished email integration, added customer sender mail * Add missing changes * improve migration command --------- Co-authored-by: Frank <csskfaves@gmail.com> Co-authored-by: Julien Nahum <julien@nahum.net>
1 parent 2fdf2a4 commit dad5c82

File tree

50 files changed

+1901
-872
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1901
-872
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Forms\Form;
6+
use App\Models\Integration\FormIntegration;
7+
use Illuminate\Console\Command;
8+
9+
class EmailNotificationMigration extends Command
10+
{
11+
/**
12+
* The name and signature of the console command.
13+
*
14+
* @var string
15+
*/
16+
protected $signature = 'forms:email-notification-migration';
17+
18+
/**
19+
* The console command description.
20+
*
21+
* @var string
22+
*/
23+
protected $description = 'One Time Only -- Migrate Email & Submission Notifications to new Email Integration';
24+
25+
/**
26+
* Execute the console command.
27+
*
28+
* @return int
29+
*/
30+
public function handle()
31+
{
32+
if (app()->environment('production')) {
33+
if (!$this->confirm('Are you sure you want to run this migration in production?')) {
34+
$this->info('Migration aborted.');
35+
return 0;
36+
}
37+
}
38+
$query = FormIntegration::whereIn('integration_id', ['email', 'submission_confirmation'])
39+
->whereHas('form');
40+
$totalCount = $query->count();
41+
$progressBar = $this->output->createProgressBar($totalCount);
42+
$progressBar->start();
43+
44+
$query->with('form')->chunk(100, function ($integrations) use ($progressBar) {
45+
foreach ($integrations as $integration) {
46+
try {
47+
$this->updateIntegration($integration);
48+
} catch (\Exception $e) {
49+
$this->error('Error updating integration ' . $integration->id . '. Error: ' . $e->getMessage());
50+
ray($e);
51+
}
52+
$progressBar->advance();
53+
}
54+
});
55+
56+
$progressBar->finish();
57+
$this->newLine();
58+
59+
$this->line('Migration Done');
60+
}
61+
62+
public function updateIntegration(FormIntegration $integration)
63+
{
64+
if (!$integration->form) {
65+
return;
66+
}
67+
$existingData = $integration->data;
68+
if ($integration->integration_id === 'email') {
69+
$integration->data = [
70+
'send_to' => $existingData->notification_emails ?? null,
71+
'sender_name' => 'OpnForm',
72+
'subject' => 'New form submission',
73+
'email_content' => 'Hello there 👋 <br>New form submission received.',
74+
'include_submission_data' => true,
75+
'include_hidden_fields_submission_data' => false,
76+
'reply_to' => $existingData->notification_reply_to ?? null
77+
];
78+
} elseif ($integration->integration_id === 'submission_confirmation') {
79+
$integration->integration_id = 'email';
80+
$integration->data = [
81+
'send_to' => $this->getMentionHtml($integration->form),
82+
'sender_name' => $existingData->notification_sender,
83+
'subject' => $existingData->notification_subject,
84+
'email_content' => $existingData->notification_body,
85+
'include_submission_data' => $existingData->notifications_include_submission,
86+
'include_hidden_fields_submission_data' => false,
87+
'reply_to' => $existingData->confirmation_reply_to ?? null
88+
];
89+
}
90+
return $integration->save();
91+
}
92+
93+
private function getMentionHtml(Form $form)
94+
{
95+
$emailField = $this->getRespondentEmail($form);
96+
return $emailField ? '<span mention-field-id="' . $emailField['id'] . '" mention-field-name="' . $emailField['name'] . '" mention-fallback="" contenteditable="false" mention="true">' . $emailField['name'] . '</span>' : '';
97+
}
98+
99+
private function getRespondentEmail(Form $form)
100+
{
101+
$emailFields = collect($form->properties)->filter(function ($field) {
102+
$hidden = $field['hidden'] ?? false;
103+
return !$hidden && $field['type'] == 'email';
104+
});
105+
106+
return $emailFields->count() > 0 ? $emailFields->first() : null;
107+
}
108+
}

api/app/Http/Controllers/Forms/PublicFormController.php

+20-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Jobs\Form\StoreFormSubmissionJob;
1010
use App\Models\Forms\Form;
1111
use App\Models\Forms\FormSubmission;
12+
use App\Open\MentionParser;
1213
use App\Service\Forms\FormCleaner;
1314
use App\Service\WorkspaceHelper;
1415
use Illuminate\Http\Request;
@@ -105,13 +106,28 @@ public function answer(AnswerFormRequest $request)
105106
return $this->success(array_merge([
106107
'message' => 'Form submission saved.',
107108
'submission_id' => $submissionId,
108-
'is_first_submission' => $isFirstSubmission
109-
], $request->form->is_pro && $request->form->redirect_url ? [
109+
'is_first_submission' => $isFirstSubmission,
110+
], $this->getRedirectData($request->form, $submissionData)));
111+
}
112+
113+
private function getRedirectData($form, $submissionData)
114+
{
115+
$formattedData = collect($submissionData)->map(function ($value, $key) {
116+
return ['id' => $key, 'value' => $value];
117+
})->values()->all();
118+
119+
$redirectUrl = ($form->redirect_url) ? (new MentionParser($form->redirect_url, $formattedData))->parse() : null;
120+
121+
if ($redirectUrl && !filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
122+
$redirectUrl = null;
123+
}
124+
125+
return $form->is_pro && $redirectUrl ? [
110126
'redirect' => true,
111-
'redirect_url' => $request->form->redirect_url,
127+
'redirect_url' => $redirectUrl,
112128
] : [
113129
'redirect' => false,
114-
]));
130+
];
115131
}
116132

117133
public function fetchSubmission(Request $request, string $slug, string $submissionId)

api/app/Http/Requests/UserFormRequest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public function rules()
5353
're_fillable' => 'boolean',
5454
're_fill_button_text' => 'string|min:1|max:50',
5555
'submitted_text' => 'string|max:2000',
56-
'redirect_url' => 'nullable|active_url|max:255',
56+
'redirect_url' => 'nullable|max:255',
5757
'database_fields_update' => 'nullable|array',
5858
'max_submissions_count' => 'integer|nullable|min:1',
5959
'max_submissions_reached_text' => 'string|nullable',

api/app/Integrations/Handlers/DiscordIntegration.php

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Integrations\Handlers;
44

5+
use App\Open\MentionParser;
56
use App\Service\Forms\FormSubmissionFormatter;
67
use Illuminate\Support\Arr;
78
use Vinkla\Hashids\Facades\Hashids;
@@ -32,6 +33,9 @@ protected function shouldRun(): bool
3233

3334
protected function getWebhookData(): array
3435
{
36+
$formatter = (new FormSubmissionFormatter($this->form, $this->submissionData))->outputStringsOnly();
37+
$formattedData = $formatter->getFieldsWithValue();
38+
3539
$settings = (array) $this->integrationData ?? [];
3640
$externalLinks = [];
3741
if (Arr::get($settings, 'link_open_form', true)) {
@@ -50,8 +54,7 @@ protected function getWebhookData(): array
5054
$blocks = [];
5155
if (Arr::get($settings, 'include_submission_data', true)) {
5256
$submissionString = '';
53-
$formatter = (new FormSubmissionFormatter($this->form, $this->submissionData))->outputStringsOnly();
54-
foreach ($formatter->getFieldsWithValue() as $field) {
57+
foreach ($formattedData as $field) {
5558
$tmpVal = is_array($field['value']) ? implode(',', $field['value']) : $field['value'];
5659
$submissionString .= '**' . ucfirst($field['name']) . '**: ' . $tmpVal . "\n";
5760
}
@@ -80,8 +83,9 @@ protected function getWebhookData(): array
8083
];
8184
}
8285

86+
$message = Arr::get($settings, 'message', 'New form submission');
8387
return [
84-
'content' => 'New submission for your form **' . $this->form->title . '**',
88+
'content' => (new MentionParser($message, $formattedData))->parse(),
8589
'tts' => false,
8690
'username' => config('app.name'),
8791
'avatar_url' => asset('img/logo.png'),

api/app/Integrations/Handlers/EmailIntegration.php

+44-9
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,51 @@
22

33
namespace App\Integrations\Handlers;
44

5-
use App\Rules\OneEmailPerLine;
5+
use App\Notifications\Forms\FormEmailNotification;
66
use Illuminate\Support\Facades\Log;
77
use Illuminate\Support\Facades\Notification;
8-
use App\Notifications\Forms\FormSubmissionNotification;
8+
use App\Open\MentionParser;
9+
use App\Service\Forms\FormSubmissionFormatter;
910

1011
class EmailIntegration extends AbstractEmailIntegrationHandler
1112
{
13+
public const RISKY_USERS_LIMIT = 120;
14+
1215
public static function getValidationRules(): array
1316
{
1417
return [
15-
'notification_emails' => ['required', new OneEmailPerLine()],
16-
'notification_reply_to' => 'email|nullable',
18+
'send_to' => 'required',
19+
'sender_name' => 'required',
20+
'sender_email' => 'email|nullable',
21+
'subject' => 'required',
22+
'email_content' => 'required',
23+
'include_submission_data' => 'boolean',
24+
'include_hidden_fields_submission_data' => ['nullable', 'boolean'],
25+
'reply_to' => 'nullable',
1726
];
1827
}
1928

2029
protected function shouldRun(): bool
2130
{
22-
return $this->integrationData->notification_emails && parent::shouldRun();
31+
return $this->integrationData->send_to && parent::shouldRun() && !$this->riskLimitReached();
32+
}
33+
34+
// To avoid phishing abuse we limit this feature for risky users
35+
private function riskLimitReached(): bool
36+
{
37+
// This is a per-workspace limit for risky workspaces
38+
if ($this->form->workspace->is_risky) {
39+
if ($this->form->workspace->submissions_count >= self::RISKY_USERS_LIMIT) {
40+
Log::error('!!!DANGER!!! Dangerous user detected! Attempting many email sending.', [
41+
'form_id' => $this->form->id,
42+
'workspace_id' => $this->form->workspace->id,
43+
]);
44+
45+
return true;
46+
}
47+
}
48+
49+
return false;
2350
}
2451

2552
public function handle(): void
@@ -28,19 +55,27 @@ public function handle(): void
2855
return;
2956
}
3057

31-
$subscribers = collect(preg_split("/\r\n|\n|\r/", $this->integrationData->notification_emails))
58+
if ($this->form->is_pro) { // For Send to field Mentions are Pro feature
59+
$formatter = (new FormSubmissionFormatter($this->form, $this->submissionData))->outputStringsOnly();
60+
$parser = new MentionParser($this->integrationData->send_to, $formatter->getFieldsWithValue());
61+
$sendTo = $parser->parse();
62+
} else {
63+
$sendTo = $this->integrationData->send_to;
64+
}
65+
66+
$recipients = collect(preg_split("/\r\n|\n|\r/", $sendTo))
3267
->filter(function ($email) {
3368
return filter_var($email, FILTER_VALIDATE_EMAIL);
3469
});
3570
Log::debug('Sending email notification', [
36-
'recipients' => $subscribers->toArray(),
71+
'recipients' => $recipients->toArray(),
3772
'form_id' => $this->form->id,
3873
'form_slug' => $this->form->slug,
3974
'mailer' => $this->mailer
4075
]);
41-
$subscribers->each(function ($subscriber) {
76+
$recipients->each(function ($subscriber) {
4277
Notification::route('mail', $subscriber)->notify(
43-
new FormSubmissionNotification($this->event, $this->integrationData, $this->mailer)
78+
new FormEmailNotification($this->event, $this->integrationData, $this->mailer)
4479
);
4580
});
4681
}

api/app/Integrations/Handlers/SlackIntegration.php

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Integrations\Handlers;
44

5+
use App\Open\MentionParser;
56
use App\Service\Forms\FormSubmissionFormatter;
67
use Illuminate\Support\Arr;
78
use Vinkla\Hashids\Facades\Hashids;
@@ -32,6 +33,9 @@ protected function shouldRun(): bool
3233

3334
protected function getWebhookData(): array
3435
{
36+
$formatter = (new FormSubmissionFormatter($this->form, $this->submissionData))->outputStringsOnly();
37+
$formattedData = $formatter->getFieldsWithValue();
38+
3539
$settings = (array) $this->integrationData ?? [];
3640
$externalLinks = [];
3741
if (Arr::get($settings, 'link_open_form', true)) {
@@ -46,20 +50,20 @@ protected function getWebhookData(): array
4650
$externalLinks[] = '*<' . $this->form->share_url . '?submission_id=' . $submissionId . '|✍️ ' . $this->form->editable_submissions_button_text . '>*';
4751
}
4852

53+
$message = Arr::get($settings, 'message', 'New form submission');
4954
$blocks = [
5055
[
5156
'type' => 'section',
5257
'text' => [
5358
'type' => 'mrkdwn',
54-
'text' => 'New submission for your form *' . $this->form->title . '*',
59+
'text' => (new MentionParser($message, $formattedData))->parse(),
5560
],
5661
],
5762
];
5863

5964
if (Arr::get($settings, 'include_submission_data', true)) {
6065
$submissionString = '';
61-
$formatter = (new FormSubmissionFormatter($this->form, $this->submissionData))->outputStringsOnly();
62-
foreach ($formatter->getFieldsWithValue() as $field) {
66+
foreach ($formattedData as $field) {
6367
$tmpVal = is_array($field['value']) ? implode(',', $field['value']) : $field['value'];
6468
$submissionString .= '>*' . ucfirst($field['name']) . '*: ' . $tmpVal . " \n";
6569
}

0 commit comments

Comments
 (0)