2026-09-21 15:26:59 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace Plugin\fp_altcha_spamschutz\src\Handler;
|
|
|
|
|
|
|
|
|
|
use JTL\phpQuery\phpQueryObject;
|
|
|
|
|
use JTL\Plugin\PluginInterface;
|
|
|
|
|
use JTL\Smarty\JTLSmarty;
|
|
|
|
|
use Plugin\fp_altcha_spamschutz\src\Service\AltchaService;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-21 20:26:47 +02:00
|
|
|
* Fügt das ALTCHA-Widget (Container + Skript) in Registrierungs- und Newsletter-Formular ein.
|
|
|
|
|
* Wird über HOOK_SMARTY_OUTPUTFILTER aufgerufen, also nach dem Rendern der Seite, auf dem
|
|
|
|
|
* fertigen HTML-Dokument (phpQuery, jQuery-ähnliche PHP-DOM-API).
|
2026-09-21 15:26:59 +02:00
|
|
|
*/
|
|
|
|
|
class TemplateHandler
|
|
|
|
|
{
|
|
|
|
|
private PluginInterface $plugin;
|
|
|
|
|
private AltchaService $altchaService;
|
|
|
|
|
|
|
|
|
|
public function __construct(PluginInterface $plugin, AltchaService $altchaService)
|
|
|
|
|
{
|
|
|
|
|
$this->plugin = $plugin;
|
|
|
|
|
$this->altchaService = $altchaService;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-21 20:26:47 +02:00
|
|
|
* @param array<string, mixed> $args Enthält 'smarty' (JTLSmarty) und 'document' (phpQueryObject).
|
2026-09-21 15:26:59 +02:00
|
|
|
*/
|
|
|
|
|
public function generalTemplateIntegration(array $args): void
|
|
|
|
|
{
|
2026-09-21 20:26:47 +02:00
|
|
|
// WICHTIG: Dieser Hook läuft bei JEDEM Seitenaufruf im gesamten Shop. Ein Fehler hier
|
2026-09-21 15:26:59 +02:00
|
|
|
// darf niemals die Anzeige irgendeiner Shop-Seite verhindern -- deshalb komplett
|
|
|
|
|
// defensiv mit try/catch umschlossen. Im Zweifel wird einfach kein Widget angezeigt,
|
|
|
|
|
// statt die Seite kaputt zu machen.
|
|
|
|
|
try {
|
|
|
|
|
$this->doTemplateIntegration($args);
|
|
|
|
|
} catch (\Throwable $e) {
|
2026-09-21 18:58:45 +02:00
|
|
|
$this->logDebug($args, 'Fehler bei Template-Integration: ' . $e->getMessage());
|
2026-09-21 15:26:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string, mixed> $args
|
|
|
|
|
*/
|
|
|
|
|
private function doTemplateIntegration(array $args): void
|
|
|
|
|
{
|
|
|
|
|
/** @var phpQueryObject $document */
|
|
|
|
|
$document = $args['document'];
|
|
|
|
|
|
2026-09-21 18:58:45 +02:00
|
|
|
$registerEnabled = $this->altchaService->isEnabledForRegistration();
|
|
|
|
|
$newsletterEnabled = $this->altchaService->isEnabledForNewsletter();
|
|
|
|
|
|
2026-09-21 15:26:59 +02:00
|
|
|
$injected = false;
|
|
|
|
|
|
2026-09-21 18:58:45 +02:00
|
|
|
$registerFormCount = 0;
|
|
|
|
|
if ($registerEnabled) {
|
2026-09-21 15:26:59 +02:00
|
|
|
$registerForm = $document->find('form.register-form');
|
2026-09-21 18:58:45 +02:00
|
|
|
$registerFormCount = \count($registerForm);
|
|
|
|
|
if ($registerFormCount > 0) {
|
2026-09-21 15:26:59 +02:00
|
|
|
$this->injectWidget($document, $registerForm);
|
|
|
|
|
$injected = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 18:58:45 +02:00
|
|
|
$newsletterFormCount = 0;
|
|
|
|
|
if ($newsletterEnabled) {
|
2026-09-21 20:26:47 +02:00
|
|
|
// JTLs phpQuery-Implementierung kennt kein closest(), daher über parents() aufwärts
|
|
|
|
|
// suchen und das nächstgelegene Formular (erster Treffer) nehmen.
|
2026-09-21 19:14:56 +02:00
|
|
|
$newsletterForm = $document->find('input[name="abonnieren"]')->parents('form')->eq(0);
|
2026-09-21 18:58:45 +02:00
|
|
|
$newsletterFormCount = \count($newsletterForm);
|
|
|
|
|
if ($newsletterFormCount > 0) {
|
2026-09-21 15:26:59 +02:00
|
|
|
$this->injectWidget($document, $newsletterForm);
|
|
|
|
|
$injected = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 19:25:53 +02:00
|
|
|
$contactEnabled = $this->altchaService->isEnabledForContact();
|
|
|
|
|
$contactFormCount = 0;
|
|
|
|
|
if ($contactEnabled) {
|
|
|
|
|
$contactForm = $document->find('form.contact-form');
|
|
|
|
|
$contactFormCount = \count($contactForm);
|
|
|
|
|
if ($contactFormCount > 0) {
|
|
|
|
|
$this->injectWidget($document, $contactForm);
|
|
|
|
|
$injected = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 18:58:45 +02:00
|
|
|
$this->logDebug($args, \sprintf(
|
2026-09-21 20:26:47 +02:00
|
|
|
'Hook ausgeführt: register_enabled=%d register_forms_gefunden=%d '
|
2026-09-21 19:25:53 +02:00
|
|
|
. 'newsletter_enabled=%d newsletter_forms_gefunden=%d '
|
|
|
|
|
. 'contact_enabled=%d contact_forms_gefunden=%d widget_eingefuegt=%d',
|
2026-09-21 18:58:45 +02:00
|
|
|
$registerEnabled ? 1 : 0,
|
|
|
|
|
$registerFormCount,
|
|
|
|
|
$newsletterEnabled ? 1 : 0,
|
|
|
|
|
$newsletterFormCount,
|
2026-09-21 19:25:53 +02:00
|
|
|
$contactEnabled ? 1 : 0,
|
|
|
|
|
$contactFormCount,
|
2026-09-21 18:58:45 +02:00
|
|
|
$injected ? 1 : 0
|
|
|
|
|
));
|
|
|
|
|
|
2026-09-21 15:26:59 +02:00
|
|
|
if ($injected) {
|
|
|
|
|
$this->includeScript($document);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 18:58:45 +02:00
|
|
|
/**
|
|
|
|
|
* Schreibt eine Diagnosemeldung, sofern in den Plugin-Einstellungen "Debug-Logging aktivieren"
|
|
|
|
|
* eingeschaltet ist. Die Meldung geht sowohl in das PHP-Errorlog des Servers als auch -- damit
|
|
|
|
|
* sie auch ohne Serverzugriff einsehbar ist -- als HTML-Kommentar in die ausgelieferte Seite
|
2026-09-21 20:26:47 +02:00
|
|
|
* (für Besucher unsichtbar, im Seitenquelltext aber direkt sichtbar).
|
2026-09-21 18:58:45 +02:00
|
|
|
*
|
|
|
|
|
* @param array<string, mixed> $args
|
|
|
|
|
*/
|
|
|
|
|
private function logDebug(array $args, string $message): void
|
|
|
|
|
{
|
|
|
|
|
if (!$this->altchaService->isDebug()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error_log('[fp_altcha_spamschutz] ' . $message);
|
|
|
|
|
|
|
|
|
|
$document = $args['document'] ?? null;
|
|
|
|
|
if ($document instanceof phpQueryObject) {
|
|
|
|
|
$safeMessage = htmlspecialchars($message, \ENT_QUOTES, 'UTF-8');
|
|
|
|
|
$document->find('body')->append('<!-- fp_altcha_debug: ' . $safeMessage . ' -->');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 15:26:59 +02:00
|
|
|
private function injectWidget(phpQueryObject $document, phpQueryObject $form): void
|
|
|
|
|
{
|
2026-09-21 20:26:47 +02:00
|
|
|
// Nicht doppelt einfügen (z. B. wenn die Seite mehrfach gefiltert wird).
|
2026-09-21 15:26:59 +02:00
|
|
|
if (\count($form->find('.fp-altcha')) > 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$challenge = $this->altchaService->createChallengeArray();
|
|
|
|
|
$challengeJson = htmlspecialchars(
|
|
|
|
|
(string) json_encode($challenge, \JSON_UNESCAPED_SLASHES),
|
|
|
|
|
\ENT_QUOTES,
|
|
|
|
|
'UTF-8'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$isGerman = empty($_SESSION['cISOSprache']) || $_SESSION['cISOSprache'] === 'ger';
|
|
|
|
|
$labelPreparing = $isGerman ? 'Sicherheitsprüfung wird vorbereitet …' : 'Preparing security check …';
|
|
|
|
|
$labelPreparing = htmlspecialchars($labelPreparing, \ENT_QUOTES, 'UTF-8');
|
|
|
|
|
|
2026-09-22 10:52:02 +02:00
|
|
|
$labelVerified = $isGerman ? 'Ja, ich bin ein Mensch' : 'Security check<br>passed';
|
2026-09-21 20:26:47 +02:00
|
|
|
$labelVerified = htmlspecialchars($labelVerified, \ENT_QUOTES, 'UTF-8');
|
|
|
|
|
|
|
|
|
|
$labelFailed = $isGerman
|
|
|
|
|
? 'Sicherheitsprüfung fehlgeschlagen. Bitte Seite neu laden.'
|
|
|
|
|
: 'Security check failed. Please reload the page.';
|
|
|
|
|
$labelFailed = htmlspecialchars($labelFailed, \ENT_QUOTES, 'UTF-8');
|
|
|
|
|
|
|
|
|
|
$labelWait = $isGerman
|
|
|
|
|
? 'Bitte einen Moment warten, die Sicherheitsprüfung läuft noch.'
|
|
|
|
|
: 'Please wait a moment, the security check is still running.';
|
|
|
|
|
$labelWait = htmlspecialchars($labelWait, \ENT_QUOTES, 'UTF-8');
|
|
|
|
|
|
2026-09-22 10:52:02 +02:00
|
|
|
$html = '<div class="fp-altcha form-group mt-2" data-fp-altcha="' . $challengeJson . '"'
|
2026-09-21 20:26:47 +02:00
|
|
|
. ' data-label-verified="' . $labelVerified . '"'
|
|
|
|
|
. ' data-label-failed="' . $labelFailed . '"'
|
|
|
|
|
. ' data-label-wait="' . $labelWait . '">'
|
2026-09-21 15:26:59 +02:00
|
|
|
. '<span class="fp-altcha-status text-muted small"><i class="fa fa-shield" aria-hidden="true"></i> '
|
|
|
|
|
. $labelPreparing . '</span>'
|
|
|
|
|
. '<input type="hidden" name="altcha" class="fp-altcha-input" value="">'
|
|
|
|
|
. '</div>';
|
|
|
|
|
|
|
|
|
|
$submitButton = $form->find('button[type="submit"], input[type="submit"]');
|
2026-09-22 10:52:02 +02:00
|
|
|
|
|
|
|
|
// Bei schmalen Bootstrap-"input-group"-Formularen (z. B. das Newsletter-Feld im Footer:
|
|
|
|
|
// ein einzeiliges Eingabefeld mit direkt angehängtem Absende-Button) wuerde das Widget,
|
|
|
|
|
// direkt vor den Button gesetzt, mit in die schmale, einzeilige Button-Leiste gequetscht.
|
|
|
|
|
// In diesem Fall wird das Widget stattdessen als eigene Zeile UNTER der gesamten
|
|
|
|
|
// input-group platziert.
|
|
|
|
|
$inputGroup = null;
|
2026-09-21 15:26:59 +02:00
|
|
|
if (\count($submitButton) > 0) {
|
2026-09-22 10:52:02 +02:00
|
|
|
$candidateGroup = $submitButton->eq(0)->parents('.input-group')->eq(0);
|
|
|
|
|
if (\count($candidateGroup) > 0) {
|
|
|
|
|
$inputGroup = $candidateGroup;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($inputGroup !== null) {
|
|
|
|
|
$inputGroup->after($html);
|
|
|
|
|
} elseif (\count($submitButton) > 0) {
|
2026-09-21 19:07:23 +02:00
|
|
|
$submitButton->eq(0)->before($html);
|
2026-09-21 15:26:59 +02:00
|
|
|
} else {
|
|
|
|
|
$form->append($html);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function includeScript(phpQueryObject $document): void
|
|
|
|
|
{
|
|
|
|
|
if (\count($document->find('script[data-fp-altcha-script]')) > 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 20:26:47 +02:00
|
|
|
// Bewusst analog zum bewährten Muster anderer, quelloffener JTL5-Plugins direkt über
|
2026-09-21 15:26:59 +02:00
|
|
|
// die globale URL_SHOP-Konstante aufgebaut, statt eine Plugin-Pfad-API zu erraten.
|
|
|
|
|
$assetUrl = \URL_SHOP . '/plugins/fp_altcha_spamschutz/assets/fp-altcha.js';
|
|
|
|
|
$assetUrl = htmlspecialchars($assetUrl, \ENT_QUOTES, 'UTF-8');
|
|
|
|
|
|
|
|
|
|
$document->find('body')->append(
|
|
|
|
|
'<script data-fp-altcha-script defer src="' . $assetUrl . '"></script>'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|