42e311c05a
Also extends test bootstrap autoloader to resolve classes in includes/sources/ and admin/ subdirectories.
137 lines
4.9 KiB
PHP
137 lines
4.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Cf7stg;
|
|
|
|
final class CF7Source implements Interface_Spam_Source
|
|
{
|
|
public function register(): void
|
|
{
|
|
add_filter('wpcf7_spam', [$this, 'on_spam'], 9999, 2);
|
|
add_action('wpcf7_submission', [$this, 'on_submission'], 20, 1);
|
|
}
|
|
|
|
public function on_spam($spam, $submission): bool
|
|
{
|
|
if ($spam === true && is_object($submission)) {
|
|
$this->enqueue_from_submission($submission, 'Honeypot or filter');
|
|
}
|
|
return (bool)$spam;
|
|
}
|
|
|
|
public function on_submission($submission): void
|
|
{
|
|
if (!is_object($submission) || !method_exists($submission, 'get_status')) return;
|
|
if ($submission->get_status() !== 'spam') return;
|
|
$this->enqueue_from_submission($submission, 'Статус spam');
|
|
}
|
|
|
|
private function enqueue_from_submission($submission, string $fallback_reason): void
|
|
{
|
|
$fields = method_exists($submission, 'get_posted_data') ? (array)$submission->get_posted_data() : [];
|
|
$form = method_exists($submission, 'get_contact_form') ? $submission->get_contact_form() : null;
|
|
$form_title = $form && method_exists($form, 'title') ? $form->title() : '';
|
|
$form_id = $form && method_exists($form, 'id') ? (int)$form->id() : null;
|
|
|
|
$reason = $this->detect_reason($submission, $fallback_reason);
|
|
$classification = Classifier::classify($fields, ['reason' => $reason]);
|
|
$label = $classification['label'];
|
|
$decision = self::decide_drop_action($label);
|
|
|
|
if ($decision['action'] === 'skip') {
|
|
Settings::increment_drop_counter($decision['counter_mode']);
|
|
return;
|
|
}
|
|
|
|
if (!self::should_enqueue($label, $fields, $submission)) {
|
|
return;
|
|
}
|
|
|
|
$post_id = FlamingoHelper::find_post_id_for_submission($submission);
|
|
if ($post_id !== null && Queue::exists_for_flamingo($post_id, false)) {
|
|
return;
|
|
}
|
|
|
|
$ip = $this->remote_addr();
|
|
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string)$_SERVER['HTTP_USER_AGENT'] : '';
|
|
|
|
$payload = MessageFormatter::build([
|
|
'fields' => $fields,
|
|
'classification' => $classification,
|
|
'site_title' => Settings::get_site_title(),
|
|
'form_title' => $form_title,
|
|
'submitted_at' => wp_date('d.m.Y H:i'),
|
|
'reason' => $reason,
|
|
'ip' => $ip,
|
|
'user_agent' => $ua,
|
|
'is_retro' => false,
|
|
'dry_run_drop' => $decision['action'] === 'enqueue_marked',
|
|
]);
|
|
|
|
if ($decision['action'] === 'enqueue_marked') {
|
|
Settings::increment_drop_counter($decision['counter_mode']);
|
|
}
|
|
|
|
Queue::enqueue([
|
|
'flamingo_post_id' => $post_id,
|
|
'form_id' => $form_id,
|
|
'source_key' => 'cf7',
|
|
'payload_json' => wp_json_encode($payload),
|
|
'label' => $label,
|
|
'is_retro' => 0,
|
|
]);
|
|
}
|
|
|
|
private function detect_reason($submission, string $fallback): string
|
|
{
|
|
$data = method_exists($submission, 'get_posted_data') ? (array)$submission->get_posted_data() : [];
|
|
// Honeypot plugin by Nocean adds a non-empty field usually prefixed by 'wpcf7_hp' or a custom name.
|
|
foreach ($data as $k => $v) {
|
|
if (is_string($k) && (stripos($k, 'honeypot') !== false || stripos($k, 'wpcf7_hp') !== false) && !empty($v)) {
|
|
return 'Honeypot';
|
|
}
|
|
}
|
|
if (method_exists($submission, 'get_spam_log') && $submission->get_spam_log()) {
|
|
return 'Akismet';
|
|
}
|
|
return $fallback;
|
|
}
|
|
|
|
private function remote_addr(): string
|
|
{
|
|
foreach (['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $key) {
|
|
if (!empty($_SERVER[$key])) {
|
|
$first = explode(',', (string)$_SERVER[$key])[0];
|
|
return trim($first);
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Decides what to do with a classified submission based on its label and the
|
|
* dry-run setting. Pure function — no side effects.
|
|
*
|
|
* @return array{action:'enqueue'|'enqueue_marked'|'skip',counter_mode:?string}
|
|
*/
|
|
public static function decide_drop_action(string $label): array
|
|
{
|
|
if ($label !== 'drop') {
|
|
return ['action' => 'enqueue', 'counter_mode' => null];
|
|
}
|
|
if (Settings::is_dry_run_drop()) {
|
|
return ['action' => 'enqueue_marked', 'counter_mode' => 'dry_run'];
|
|
}
|
|
return ['action' => 'skip', 'counter_mode' => 'real'];
|
|
}
|
|
|
|
/**
|
|
* Filterable gate — allows external code to block non-drop submissions
|
|
* via filter `cf7stg_should_enqueue`.
|
|
*/
|
|
public static function should_enqueue(string $label, array $fields, $submission): bool
|
|
{
|
|
return (bool)apply_filters('cf7stg_should_enqueue', true, $label, $fields, $submission);
|
|
}
|
|
}
|