digitaladapt/vigilant-form

VigilantForm用于表单评分和处理。

v0.1 2020-02-25 17:14 UTC

README

用于表单评分和处理。

工作正在进行中,功能完善且在生产中积极使用,但需要清理和更多文档。

那么VigilantForm是什么?

VigilantForm是我防止垃圾表单提交进入CRM的尝试。

因此,而不是直接将表单提交放入CRM,我将它们推送到VigilantForm。

VigilantForm根据您选择的评分逻辑对提交进行评分;一些例子包括

  • 检查是否通过了蜜罐测试
  • 检查表单提交的速度
  • 检查电子邮件是否有效(语法和DNS检查)
  • 检查电话号码是否合理
  • 检查是否填写了必填字段
  • 检查IP地址的来源(通过ipstack.com)
  • 寻找不良输入,如名称字段中的"http://"
  • 等等

评分完成后,表单提交将获得等级,您可以根据等级采取不同的自定义操作。

例如,我将高质量表单提交推送到CRM,但需要审查的表单提交将发送到Discord,带有批准/拒绝的链接;同时垃圾表单提交将被记录到文件中以供定期审查,垃圾邮件表单提交则静默删除。

查找IP地址的地理位置、计算评分和执行自定义处理的过程是异步于表单提交的。因此,存储表单提交的API应该始终快速返回,额外的处理可以几乎立即开始。

那么如何设置它?

创建项目,配置数据库和访问密钥,迁移数据库,队列工作进程,设置apache/nginx,并自定义流程和评分文件以符合需求。

composer create-project digitaladapt/vigilant-form <DESTINATION_FOLDER>

.env文件中,将DB_*值更新为您想要使用的数据库。

您还需要设置CLIENT_IDCLIENT_SECRET为长唯一字符串,您将在将表单提交存储到系统时需要它们。

如果您想查找表单提交的IP地址的地理位置,请使用ipstack.com设置(免费或付费)访问密钥,并将其放入IPSTACK_key。否则您无法根据大洲/国家/地区进行评分。

通过运行迁移命令创建数据库表

php artisan migrate

您还需要设置队列工作进程:[https://laravel.net.cn/docs/6.x/queues](https://laravel.net.cn/docs/6.x/queues) 建议您使用redis,并将QUEUE_CONNECTION设置为"redis",但默认数据库也可以正常工作。虽然强烈建议想出一种确保队列:work进程始终运行的方法,但简单的cron可以是一个很好的快速测试方法,然后再设置更健壮的系统

* * * * * php /path/to/project/artisan queue:work --stop-when-empty

设置您的Web服务器,根目录是项目中的public文件夹。如果您使用nginx,请添加一个index.php的location回退,如下所示

location / {
    try_files $uri $uri/ /index.php?$request_uri;
}

如果您在Web浏览器中访问项目,您应该期望得到一个405方法不允许的响应。根路由只允许POST请求,用于存储表单提交。

希望将表单提交推送到您的 vigilant-forms 实例的网站可以使用 digitaladapt/vigilant-form-kit 库。 https://packagist.org.cn/packages/digitaladapt/vigilant-form-kit 该库可以帮助您收集和提交表单以及元数据。在 app/Http/Requests/SubmissionStoreRequest.php 中可以找到 vigilant-form 提交的 API 要求。

注意事项

  • 每次您修改任何评分或处理 php 文件时,都应该重新启动您的队列 php artisan queue:restart,否则您的更改将不会完全生效。
  • 在数据库中,IP 地址记录可以在多个表单提交中重复使用,以避免重复查找相同的信息;因此,一旦设置,ip 字段是不可变的。
  • 您应该监控作业失败 php artisan queue:failed,并且强烈建议您设置 LOG_SLACK_WEBHOOK_URL 以将重要日志发送到 Slack/Discord。
  • 正常表单字段限制为 255 个字符;但存在一个特殊的消息属性来存储更长的内容,任何名为 "comments"、"comment" 或 "message" 的表单字段都将归入消息属性;表单提交仅限于一个长消息属性。
  • 账户及其字段是一种将多个表单提交与单个人/组织关联起来的方式,并在更高层面上存储提交信息,但这是一种适合处理文件的业务逻辑,具体取决于您的需求。

处理方式

默认情况下,在 process 文件夹中,为每个可能的结果都有一个 php 文件。这些文件默认不执行任何操作,但可以利用它们在给定的表单提交获得特定评分时执行所需的任何操作。鉴于它是一个 Laravel 框架项目中的 php 文件,您可以编写所需的任何代码,但已包含一些实用工具。

例如,您可以通过 Discord webhook 和电子邮件发送提交的详细信息:在使用 Mail Facade 之前,请确保更新您的 .env 以发送电子邮件。

use App\Mail\SubmissionProcessed;
use App\Utilities\Discord;
use Illuminate\Support\Facades\Mail;

/* use global to access the submission */
global $submission;

/* use global to access reprocess (set to true if submissions grade was overridden by user action) */
global $reprocess;

/* use global to access details (array which scoring rules applied to the submission, may be empty) */
global $details;

$color     = 'ff0000'; /* red */
$webhook   = 'https://discordapp.com/api/webhooks/<YOUR_WEBHOOK_URL>';
$toEmails  = [
    '<YOUR_EMAIL_ADDRESS>',
];

/* notification information */
$title       = $submission->type->websiteTitle();
$description = $submission->message;
$fields      = $submission->fields()->scoring()->pluck('input', 'field')->toArray();
$meta        = [
    'timestamp'   => $submission->created_at->format('D, M jS, Y \a\t g:ia'),
    'ip_address ' => $submission->ipAddress->ip,
    'ip_location' => "{$submission->ipAddress->country} > {$submission->ipAddress->region}",
    'uuid'        => $submission->uuid,
] + $submission->origins()->whereIn('field', [
    'utm_source', 'utm_term', 'utm_campaign', 'utm_adgroup', 'utm_medium',
])->pluck('input', 'field')->toArray();

/* send notification via chat */
$discord = new Discord($webhook, $color);
$discord->title       = $title;
$discord->description = $description;
$discord->fields      = $fields;
$discord->details     = $details;
$discord->meta        = $meta;
$discord->send(); /* sent in real-time */

/* also queue up the email, but only if not reprocessing */
if (!$reprocess) {
    $email = new SubmissionProcessed();
    $email->subject     = 'New Submission Arrived';
    $email->title       = $title;
    $email->description = $description;
    $email->fields      = $fields;
    $email->details     = $details;
    $email->meta        = $meta;
    Mail::to($toEmails)->queue($email); /* queued as separate job */
}

return true;

评分方式

分数越高,内容越可能是垃圾邮件/垃圾信息。分数被分解为 5 个等级

  • 完美:分数 0 到 9
  • 质量:分数 10 到 99
  • 审核:分数 100 到 999
  • 垃圾:分数 1,000 到 9,999
  • 忽略:分数 10,000 到 1,000,000

scoring.php 文件决定了如何确定每个提交的分数。该文件返回一个规则数组,允许您指定要检查哪些字段或属性,要执行哪种类型的检查,以及如果提交匹配则添加多少分数。如果需要,还可以限制最大分数,规则也可能具有负分数,以提高优质内容的分数。

每个规则必须包含:"name" 您想给此规则命名,"score" 确定每次违规给出的分数,"fields" 或 "property"(要审查的数据),

  • "fields" 是字段名称的数组,或 true 表示所有字段(这还包括 "message" 属性)。
  • "property" 是点 "." 表示法中所需属性的字符串 "ipAddress.country"、"duration"、"honeypot" 等。 "check",必须是以下之一
  • "regexp" ("values" 字符串,不允许 "%",要在 php/sql 之间使正则表达式工作,必须有一个保留的分隔符),
  • "not_regexp" ("values" 字符串,不允许 "%",要在 php/sql 之间使正则表达式工作,必须有一个保留的分隔符),
  • "regexp_count_over" ("values" 包含正则表达式字符串和计数的数组,如果正则表达式出现超过计数值次,则触发规则),
  • "contains" ("values" 字符串数组),
  • "ends_with" ("values" 字符串数组),
  • "is_bool" ("values" true/false),
  • "is_empty" ("values" 不必要求,仅当字段在表单中提供但未填写时计数),
  • "email" ("values" 不必要求,检查电子邮件地址并执行简单的 DNS MX 检查),
  • "missing" ("values" 字符串数组),
  • "less_than" ("values" 字符串,仅适用于 "duration" 属性),
  • "length_under" ("values" 字符数)。
  • “长度超过” ("values" 字符数), “values” (除非 "check" 是 "is_empty" 或 "email") 是必需的,并且应该与指定的检查相匹配。

可选地可以指定一个 "limit" (整数),如果规则匹配,表单提交的最大得分就是给定的限制。

注意:所有进行的检查都是不区分大小写的(包括正则表达式),另外请注意,所有检查都适用于 utf-8 数据(长度是字符数而不是字节数),最后:对于所有字段检查,如果表单中没有指定字段,则提交无法通过该字段的规则(永远);这意味着只有一些表单要求字段,只有包含该字段的表单的提交才会应用该规则。

示例:两个表单,它们都包含 "email",但只有一个包含 "phone",一个给予 "缺失" "phone" 得分的规则不会给没有 "phone" 字段表单的任何提交得分。

示例:scoring.php

return [
    [
        // if either field contains either string, the submission will be graded as "ignore"
        'name'   => 'name/org has url',
        'score'  => 10000,
        'fields' => ['full_name', 'company'],
        'check'  => 'contains',
        'values' => ['http://', 'https://'],
    ],
    [
        // if the email field is not a valid email, the submission will be graded as "junk"
        'name'   => 'email is invalid',
        'score'  => 1000,
        'fields' => ['email'],
        'check'  => 'email',
    ],
    [
        // if the phone field fails the regular expression, the submission will be graded as "review"
        // this regexp assumes phone is unformatted and is a 10 digit North American phone number.
        // see: https://en.wikipedia.org/wiki/North_American_Numbering_Plan
        'name'   => 'phone is invalid',
        'score'  => 100,
        'fields' => ['phone'],
        'check'  => 'not_regexp',
        'values' => '^[2-9][0-9]{2}[2-9][0-9]{6}$',
    ],
    [
        // if any field is empty, the submission will be graded as "quality"
        'name'   => 'a field was left empty',
        'score'  => 10,
        'fields' => true, /* all fields (including property "message") */
        'check'  => 'is_empty',
    ],

    // if a submission passes all of these rules it will be graded as "perfect"

    // it is also possible to improve the score with rules that improve the score
    [
        // if the ip geo-location country is USA, the submission may be upgraded from "quality" back to "perfect"
        'name'     => '[positive] ip_address country is USA',
        'score'    => -10,
        'property' => 'ipAddress.country',
        'check'    => 'contains',
        'values'   => ['United States'],
    ],

    // it is also possible to limit the maximum score
    [
        // if form submission seems to have originated from paid marketing, the submission may be upgraded
        // from "review"/"quality" to "perfec" and disallow grades of "ignore" and "junk".
        'name'     => '[positive] has utm_source',
        'score'    => -100,
        'property' => 'hasUtmSource',
        'check'    => 'is_bool',
        'values'   => true,
        'limit'    => 999,
    ],
];

账户

在您的处理过程中,如果您想存储补充信息,可以在账户级别进行。所有对账户和 account_fields 的使用都由您决定,因为它是特定于业务的逻辑。

例如,我在将高质量的提交推送到我的 CRM 后,获得了一些账户元数据,因此我可以将其包含在 Discord 聊天中。

global $submission; // quality Submission just pushed into CRM
$record = ['id' => '<EXTERNAL_UUID>', /* data from CRM, including "id", among other things */ ];

/* find account, so we can store extra information into our database */
if (!$submission->account) {
	$account = Account::firstOrCreate(['external_key' => $record['id']]);
	$submission->account()->associate($account);
	$submission->save();
} else {
	$account = $submission->account;
}

/* store extra information into our database */
foreach ($record as $key => $value) {
	$account->setField($key, $value);
}

/* mark the submission as externally synced */
$submission->synced = true;
$submission->save();