mahnung/ajax/regex_preview.php
Eduard Wisch 56954d68f3
Some checks failed
Deploy mahnung / deploy (push) Failing after 4s
Konfigurierbare Tracking-Patterns mit Live-Vorschau [deploy]
Datenmodell:
- Neue Tabelle llx_mahnung_trackingpattern (provider, label, regex,
  url_template, priority, active). Auto-Anlage + Default-Seed im setup.php
  und in modMahnung::init() — idempotent.

Default-Patterns (priority hoeher = spezifischer, zuerst gepruef):
- DHL Paket 20-stellig (90), DPAG Einschreiben RR...DE (85), UPS 1Z... (80),
- DHL 11-stellig Online-Frankierung (30), Hermes 14-stellig (25), DPD (20).

Setup-Seite admin/tracking_patterns.php:
- CRUD (Anlegen/Bearbeiten/Aktivieren-Deaktivieren/Loeschen)
- Live-Vorschau: Regex + Beispieltext + URL-Template werden waehrend
  des Tippens (debounce 300ms) via ajax/regex_preview.php ausgewertet.
  UI zeigt: Regex-Syntax-Status, gefundene Sendungsnummer, vollstaendige
  Tracking-URL (anklickbar).
- Validierung: Delimiter / # ~ Whitelist, https://-Pflicht, {nr}-Platzhalter,
  max 255 Zeichen Regex.

AJAX-Endpoint ajax/regex_preview.php:
- ReDoS-Schutz: max 10 KB Sample, pcre.backtrack_limit=100k.
- POST-only (mit Setup-Recht), JSON-Response.

card.php:
- tracking-URL kommt jetzt aus MahnungTrackingPattern::urlFor() (DB-Lookup
  nach Provider) statt hardcoded Mahnung::trackingUrl() — letztere bleibt
  als Fallback.

Setup-Seite: neuer Button "Tracking-Muster (Regex)" oben rechts.

Lang-Keys: 23 neue (de_DE + en_US) fuer Pattern-CRUD + Live-Vorschau.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:04:42 +02:00

89 lines
2.7 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* GPL v3 (siehe COPYING).
*/
/**
* \file htdocs/custom/mahnung/ajax/regex_preview.php
* \ingroup mahnung
* \brief AJAX-Endpoint: Live-Vorschau fuer Tracking-Regex auf der Setup-Seite.
*
* POST: regex (string), sample (string, max 10 KB), url_template (string)
* Response (JSON): { valid: bool, match: string|null, preview_url: string|null, error: string|null }
*/
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
require_once $_SERVER['DOCUMENT_ROOT'].'/main.inc.php';
global $user, $langs;
$langs->load('mahnung@mahnung');
header('Content-Type: application/json; charset=utf-8');
if (!$user->admin && !$user->hasRight('mahnung', 'setup')) {
echo json_encode(array('valid' => false, 'error' => 'access denied'));
exit;
}
// ReDoS-Schutz: Eingaben begrenzen + Zeitlimit
$regex = (string) GETPOST('regex', 'nohtml');
$sample = (string) GETPOST('sample', 'nohtml');
$urlTemplate = (string) GETPOST('url_template', 'alphanohtml');
if (strlen($regex) > 255) {
echo json_encode(array('valid' => false, 'error' => 'regex too long (max 255)'));
exit;
}
if (strlen($sample) > 10240) {
$sample = substr($sample, 0, 10240);
}
// Whitelist: Delimiter / # ~
if ($regex === '' || !in_array($regex[0], array('/', '#', '~'), true)) {
echo json_encode(array('valid' => false, 'error' => 'allowed delimiters: / # ~'));
exit;
}
// ReDoS-Schutz via PCRE-Backtrack-Limit (klein halten)
$prevBacktrack = ini_get('pcre.backtrack_limit');
$prevRecursion = ini_get('pcre.recursion_limit');
@ini_set('pcre.backtrack_limit', '100000');
@ini_set('pcre.recursion_limit', '10000');
$matches = array();
$ret = @preg_match($regex, '', $matches); // erst leerer String — testet Syntax
if ($ret === false) {
@ini_set('pcre.backtrack_limit', (string) $prevBacktrack);
@ini_set('pcre.recursion_limit', (string) $prevRecursion);
$err = preg_last_error_msg();
echo json_encode(array('valid' => false, 'error' => 'invalid regex'.($err && $err !== 'No error' ? ': '.$err : '')));
exit;
}
// Echtes Sample matchen
$match = null;
if ($sample !== '') {
$ret = @preg_match($regex, $sample, $matches);
if ($ret === 1) {
$match = !empty($matches[1]) ? $matches[1] : $matches[0];
}
}
@ini_set('pcre.backtrack_limit', (string) $prevBacktrack);
@ini_set('pcre.recursion_limit', (string) $prevRecursion);
$previewUrl = null;
if ($match !== null && strpos($urlTemplate, 'https://') === 0 && strpos($urlTemplate, '{nr}') !== false) {
$previewUrl = str_replace('{nr}', rawurlencode($match), $urlTemplate);
}
echo json_encode(array(
'valid' => true,
'match' => $match,
'preview_url' => $previewUrl,
'error' => null,
));
exit;