diff --git a/api/index.php b/api/index.php
new file mode 100644
index 0000000..863a8fb
--- /dev/null
+++ b/api/index.php
@@ -0,0 +1,40 @@
+ false,
+ 'error' => 'Unknown endpoint',
+ 'path' => $path,
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+ break;
+}
diff --git a/api/target/.htaccess b/api/target/.htaccess
new file mode 100644
index 0000000..d364e60
--- /dev/null
+++ b/api/target/.htaccess
@@ -0,0 +1,9 @@
+# api/target/.htaccess
+
+ Require all denied
+
+
+
+ Order allow,deny
+ Deny from all
+
diff --git a/api/target/quickcheck.php b/api/target/quickcheck.php
new file mode 100644
index 0000000..323ca93
--- /dev/null
+++ b/api/target/quickcheck.php
@@ -0,0 +1,314 @@
+ false,
+ 'error' => 'Missing required parameter: serial',
+ ];
+ }
+
+ // Mini-VID-Datenbank (kannst du später aus Datei/DB laden)
+ $VID_DB = [
+ '0781' => 'SanDisk Corp.',
+ '0951' => 'Kingston Technology',
+ '054C' => 'Sony Corp.',
+ '1B1C' => 'Corsair',
+ '13FE' => 'Phison Electronics',
+ '8564' => 'Transcend Information, Inc.',
+ '090C' => 'Silicon Motion, Inc.',
+ '174C' => 'ASMedia Technology',
+ '0BC2' => 'Seagate',
+ // ... nach Bedarf ergänzen
+ ];
+
+ $vendorInfo = quickcheck_lookup_vendor($vid, $VID_DB);
+ $serialAnalysis = quickcheck_analyze_serial($serial);
+ $consistencyInfo = quickcheck_evaluate_consistency($vendorInfo['vendor'] ?? null, $manufacturer, $serialAnalysis);
+
+ // Gesamtrating
+ $rating = 'ok';
+ if ($serialAnalysis['category'] === 'invalid') {
+ $rating = 'invalid';
+ } elseif ($serialAnalysis['category'] === 'very_suspicious' || $consistencyInfo['manufacturer_match'] === 'mismatch') {
+ $rating = 'suspicious';
+ } elseif ($serialAnalysis['category'] === 'suspicious') {
+ $rating = 'needs_review';
+ }
+
+ return [
+ 'success' => true,
+ 'rating' => $rating,
+ 'input' => [
+ 'vid' => $vid,
+ 'pid' => $pid,
+ 'manufacturer' => $manufacturer,
+ 'serial' => $serial,
+ ],
+ 'vendor_detected' => $vendorInfo,
+ 'serial_analysis' => $serialAnalysis,
+ 'consistency' => $consistencyInfo,
+ 'messages' => [
+ 'de' => [
+ 'Hinweis: Diese Prüfung kann keine Echtheit garantieren, sondern bewertet nur Plausibilität und Auffälligkeiten.',
+ ],
+ ],
+ ];
+}
+
+/**
+ * Eingaben lesen: JSON-Body > POST > GET
+ */
+function quickcheck_get_input(): array
+{
+ $contentType = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '';
+ $contentType = strtolower(trim(explode(';', $contentType)[0]));
+
+ if ($contentType === 'application/json') {
+ $raw = file_get_contents('php://input');
+ if ($raw !== false && $raw !== '') {
+ $data = json_decode($raw, true);
+ if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
+ return $data;
+ }
+ }
+ }
+
+ if (!empty($_POST)) {
+ return $_POST;
+ }
+ if (!empty($_GET)) {
+ return $_GET;
+ }
+
+ return [];
+}
+
+/**
+ * VID normalisieren + Lookup
+ */
+function quickcheck_normalize_vid(string $vid): ?string
+{
+ $vid = strtoupper(trim($vid));
+ $vid = preg_replace('/^0X/i', '', $vid);
+ if ($vid === '' || !preg_match('/^[0-9A-F]{1,4}$/', $vid)) {
+ return null;
+ }
+ return str_pad($vid, 4, '0', STR_PAD_LEFT);
+}
+
+function quickcheck_lookup_vendor(?string $vid, array $VID_DB): array
+{
+ if (!$vid) {
+ return [
+ 'vid' => null,
+ 'found' => false,
+ 'vendor' => null,
+ 'confidence' => 0,
+ ];
+ }
+
+ $norm = quickcheck_normalize_vid($vid);
+ if (!$norm) {
+ return [
+ 'vid' => $vid,
+ 'found' => false,
+ 'vendor' => null,
+ 'confidence' => 0,
+ 'issue' => 'Invalid VID format',
+ ];
+ }
+
+ if (isset($VID_DB[$norm])) {
+ return [
+ 'vid' => $norm,
+ 'found' => true,
+ 'vendor' => $VID_DB[$norm],
+ 'confidence' => 1.0,
+ ];
+ }
+
+ return [
+ 'vid' => $norm,
+ 'found' => false,
+ 'vendor' => null,
+ 'confidence' => 0.2,
+ ];
+}
+
+/**
+ * Seriennummer grob analysieren (Plausibilität)
+ */
+function quickcheck_analyze_serial(string $serial): array
+{
+ $serial = trim($serial);
+ $length = strlen($serial);
+ $issues = [];
+ $score = 100;
+
+ if ($length === 0) {
+ return [
+ 'serial' => $serial,
+ 'length' => 0,
+ 'issues' => ['empty'],
+ 'score' => 0,
+ 'category' => 'invalid',
+ ];
+ }
+
+ if ($length < 4) {
+ $issues[] = 'too_short_critical';
+ $score -= 60;
+ } elseif ($length < 8) {
+ $issues[] = 'short_suspicious';
+ $score -= 30;
+ }
+
+ if ($length > 32) {
+ $issues[] = 'very_long';
+ $score -= 10;
+ }
+
+ if (quickcheck_is_all_same_char($serial)) {
+ $issues[] = 'all_same_char';
+ $score -= 50;
+ }
+
+ $simplePatterns = [
+ '000000', '00000000', '000000000000',
+ '111111', '123456', '12345678', 'ABCDEF', 'AABBCC'
+ ];
+ if (in_array(strtoupper($serial), $simplePatterns, true)) {
+ $issues[] = 'simple_pattern';
+ $score -= 40;
+ }
+
+ if (quickcheck_is_simple_sequence($serial)) {
+ $issues[] = 'sequence_pattern';
+ $score -= 25;
+ }
+
+ $isNumeric = preg_match('/^[0-9]+$/', $serial) === 1;
+ $isAlpha = preg_match('/^[A-Za-z]+$/', $serial) === 1;
+ $isHex = preg_match('/^[0-9A-Fa-f]+$/', $serial) === 1;
+ $isAlnum = preg_match('/^[A-Za-z0-9]+$/', $serial) === 1;
+
+ if ($isNumeric) {
+ $issues[] = 'numeric_only';
+ $score -= 10;
+ } elseif ($isAlpha) {
+ $issues[] = 'letters_only';
+ $score -= 10;
+ }
+
+ if ($isHex && !$isNumeric && $length >= 8 && $length <= 24) {
+ $issues[] = 'hex_pattern_plausible';
+ $score += 5;
+ }
+
+ if ($isAlnum && !$isNumeric && !$isAlpha && $length >= 8) {
+ $issues[] = 'alnum_mixed_plausible';
+ $score += 5;
+ }
+
+ if ($score > 100) $score = 100;
+ if ($score < 0) $score = 0;
+
+ if ($length === 0) {
+ $category = 'invalid';
+ } elseif ($score >= 80) {
+ $category = 'plausible';
+ } elseif ($score >= 50) {
+ $category = 'suspicious';
+ } else {
+ $category = 'very_suspicious';
+ }
+
+ return [
+ 'serial' => $serial,
+ 'length' => $length,
+ 'issues' => array_values(array_unique($issues)),
+ 'score' => $score,
+ 'category' => $category,
+ ];
+}
+
+function quickcheck_is_all_same_char(string $s): bool
+{
+ return strlen($s) > 1 && preg_match('/^(.)\1+$/', $s) === 1;
+}
+
+function quickcheck_is_simple_sequence(string $s): bool
+{
+ $seqs = [
+ '123456', '1234567', '12345678', '234567', '345678',
+ 'ABCDEFG', 'ABCDEF', 'ABCDE'
+ ];
+ $ls = strtoupper($s);
+ foreach ($seqs as $pattern) {
+ if (strpos($ls, $ls) !== false && strpos($ls, $pattern) !== false) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Herstellerangabe vs. Vendor-Name bewerten
+ */
+function quickcheck_evaluate_consistency(?string $vendorName, string $userManufacturer, array $serialAnalysis): array
+{
+ $vendorName = $vendorName ? trim($vendorName) : '';
+ $userManufacturer = trim($userManufacturer);
+
+ $matchStatus = 'unknown';
+ $notes = [];
+
+ if ($vendorName !== '' && $userManufacturer !== '') {
+ $v = mb_strtolower($vendorName);
+ $u = mb_strtolower($userManufacturer);
+
+ if (strpos($v, $u) !== false || strpos($u, $v) !== false) {
+ $matchStatus = 'match';
+ $notes[] = 'Herstellerangabe passt zur ermittelten Vendor-ID.';
+ } else {
+ $matchStatus = 'mismatch';
+ $notes[] = 'Herstellerangabe weicht von der Vendor-ID ab.';
+ }
+ } elseif ($vendorName !== '') {
+ $notes[] = 'Hersteller über Vendor-ID bekannt, aber keine Herstellerangabe des Nutzers.';
+ } elseif ($userManufacturer !== '') {
+ $notes[] = 'Hersteller vom Nutzer angegeben, aber keine oder unbekannte Vendor-ID.';
+ } else {
+ $notes[] = 'Keine Herstellerinformationen vorhanden.';
+ }
+
+ $ratingHint = 'neutral';
+ if ($serialAnalysis['category'] === 'very_suspicious' || $matchStatus === 'mismatch') {
+ $ratingHint = 'suspicious';
+ } elseif ($serialAnalysis['category'] === 'plausible' && $matchStatus === 'match') {
+ $ratingHint = 'good';
+ }
+
+ return [
+ 'manufacturer_match' => $matchStatus,
+ 'notes' => $notes,
+ 'consistency_hint' => $ratingHint,
+ ];
+}
diff --git a/config/prod/config.php b/config/prod/config.php
index 6f4490b..27341ff 100644
--- a/config/prod/config.php
+++ b/config/prod/config.php
@@ -21,3 +21,11 @@ if (!defined('APP_URL_FAKECHECK')) {
define('MATOMO_URL', 'https://matomo.my-statistics.info/');
define('MATOMO_ENABLED', true);
define('MATOMO_SITE_ID', 7);
+$env = 'prod';
+$baseUrl = 'https://usbcheck.it';
+$apiBaseUrl = 'https://api.usbcheck.it';
+
+// Diese Werte später ins Template schieben:
+$GLOBALS['usb_env'] = $env;
+$GLOBALS['usb_base_url'] = $baseUrl;
+$GLOBALS['usb_api_base'] = $apiBaseUrl;
\ No newline at end of file
diff --git a/config/staging/config.php b/config/staging/config.php
index 72c3106..336a83b 100644
--- a/config/staging/config.php
+++ b/config/staging/config.php
@@ -21,3 +21,11 @@ if (!defined('APP_URL_FAKECHECK')) {
define('MATOMO_URL', 'https://matomo.my-statistics.info/');
define('MATOMO_ENABLED', false);
define('MATOMO_SITE_ID', 8);
+$env = 'staging';
+$baseUrl = 'https://statging.usbcheck.it';
+$apiBaseUrl = 'https://api.staging.usbcheck.it';
+
+// Diese Werte später ins Template schieben:
+$GLOBALS['usb_env'] = $env;
+$GLOBALS['usb_base_url'] = $baseUrl;
+$GLOBALS['usb_api_base'] = $apiBaseUrl;
\ No newline at end of file
diff --git a/partials/landing/fakecheck/serialcheck.php b/partials/landing/fakecheck/serialcheck.php
new file mode 100644
index 0000000..a68c5c6
--- /dev/null
+++ b/partials/landing/fakecheck/serialcheck.php
@@ -0,0 +1,92 @@
+
+
+
+ Seriennummer prüfen (Quickcheck)
+
+
+ Gib die Seriennummer deines USB-Sticks ein und wir prüfen die Plausibilität
+ sowie die Konsistenz deiner Herstellerangabe zu Vendor-ID (falls vorhanden).
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/partials/structure/layout_start.php b/partials/structure/layout_start.php
index dc629cd..7b67f1c 100644
--- a/partials/structure/layout_start.php
+++ b/partials/structure/layout_start.php
@@ -41,6 +41,13 @@ $host = $_SERVER['HTTP_HOST'] ?? '';
+
+
+
{
const baseUrl = window.fakecheckBaseUrl || "";
const locale = window.fakecheckLocale || "en";
+ // Neu: API-Basis-URL (prod/staging) – über Config oder Fallback anhand Host
+ const apiBaseUrl = window.fakecheckApiBaseUrl || detectApiBaseUrl();
+
+ function detectApiBaseUrl() {
+ const host = window.location.hostname || "";
+ // einfache Heuristik: staging-Domain → staging-API
+ if (host === "staging.usbcheck.it" || host.endsWith(".staging.usbcheck.it")) {
+ return "https://api.staging.usbcheck.it";
+ }
+ // default: production
+ return "https://api.usbcheck.it";
+ }
+
const root = document.getElementById("fc-root");
if (!root) {
// Auf anderen Seiten eingebunden? Dann einfach nichts tun.
@@ -682,6 +695,168 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
+ // --- Seriennummer-Quickcheck (API-gestützt) -----------------------------
+
+ function initSerialCheckWidget() {
+ const rootSc = document.getElementById("serialcheck-root");
+ if (!rootSc) return; // Serialcheck-Partial nicht eingebunden → nichts tun
+
+ const form = rootSc.querySelector("#serialcheck-form");
+ const errorBox = rootSc.querySelector("#serialcheck-error");
+ const resultBox = rootSc.querySelector("#serialcheck-result");
+
+ const manufacturerInput = rootSc.querySelector("#sc-manufacturer");
+ const vidInput = rootSc.querySelector("#sc-vid");
+ const pidInput = rootSc.querySelector("#sc-pid");
+ const serialInput = rootSc.querySelector("#sc-serial");
+
+ function showScError(msg) {
+ if (!errorBox) return;
+ errorBox.textContent = msg || "Es ist ein Fehler aufgetreten.";
+ errorBox.classList.remove("hidden");
+ if (resultBox) resultBox.classList.add("hidden");
+ }
+
+ function clearScError() {
+ if (!errorBox) return;
+ errorBox.classList.add("hidden");
+ errorBox.textContent = "";
+ }
+
+ function renderScResult(data) {
+ if (!resultBox) return;
+ clearScError();
+ resultBox.classList.remove("hidden");
+
+ const rating = data.rating || "unknown";
+ let ratingLabel = "";
+ let ratingDesc = "";
+
+ if (rating === "ok") {
+ ratingLabel = "Plausibel";
+ ratingDesc = "Keine deutlichen Auffälligkeiten erkannt.";
+ } else if (rating === "needs_review") {
+ ratingLabel = "Überprüfen empfohlen";
+ ratingDesc = "Leichte Auffälligkeiten. In Kombination mit einem technischen Test ergibt sich ein klareres Bild.";
+ } else if (rating === "suspicious") {
+ ratingLabel = "Auffällig / Verdächtig";
+ ratingDesc = "Deutliche Auffälligkeiten erkannt. Ein Kapazitäts-/Geschwindigkeitstest ist dringend empfohlen.";
+ } else if (rating === "invalid") {
+ ratingLabel = "Ungültig";
+ ratingDesc = "Die Seriennummer konnte nicht sinnvoll bewertet werden.";
+ } else {
+ ratingLabel = "Unklar";
+ ratingDesc = "Bewertung nicht eindeutig möglich.";
+ }
+
+ const input = data.input || {};
+ const vendorInfo = data.vendor_detected || {};
+ const serialInfo = data.serial_analysis || {};
+ const consistency = data.consistency || {};
+ const issues = serialInfo.issues || [];
+ const notes = consistency.notes || [];
+
+ const issuesList = issues.length
+ ? '
' +
+ issues.map(i => '' + i + ' ').join("") +
+ " "
+ : 'Keine besonderen Auffälligkeiten. ';
+
+ const vendorLine = vendorInfo.found
+ ? (vendorInfo.vendor + " (VID " + vendorInfo.vid + ")")
+ : (vendorInfo.vid ? ("Unbekannter Hersteller für VID " + vendorInfo.vid) : "Keine Vendor-ID angegeben");
+
+ const notesList = notes.length
+ ? '' +
+ notes.map(n => "" + n + " ").join("") +
+ " "
+ : "";
+
+ resultBox.innerHTML = `
+
+
+ Bewertung: ${ratingLabel}
+
+
${ratingDesc}
+
+
+
+
Eingabedaten
+
+
Hersteller (Angabe): ${input.manufacturer || 'keine Angabe '}
+
VID / PID: ${(input.vid || "–") + " / " + (input.pid || "–")}
+
Vendor aus VID: ${vendorLine}
+
+
+
+
+
Seriennummer-Analyse
+
+
Seriennummer: ${serialInfo.serial || ""}
+
Länge: ${serialInfo.length || 0} Zeichen
+
Kategorie: ${serialInfo.category || "-"}
+
Score: ${typeof serialInfo.score === "number" ? serialInfo.score : "-"} / 100
+
Auffälligkeiten: ${issuesList}
+
+
+
+
+
Hersteller-Konsistenz
+ ${notesList}
+
+ Diese Einschätzung basiert auf Heuristiken und kann keine Echtheit garantieren.
+
+
+ `;
+ }
+
+ if (!form) return;
+
+ form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ clearScError();
+ if (resultBox) resultBox.classList.add("hidden");
+
+ const payload = {
+ manufacturer: manufacturerInput ? manufacturerInput.value.trim() : "",
+ vid: vidInput ? vidInput.value.trim() : "",
+ pid: pidInput ? pidInput.value.trim() : "",
+ serial: serialInput ? serialInput.value.trim() : ""
+ };
+
+ if (!payload.serial) {
+ showScError("Bitte gib eine Seriennummer ein.");
+ return;
+ }
+
+ fetch(apiBaseUrl.replace(/\/+$/, "") + "/quickcheck", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload)
+ })
+ .then(res => {
+ if (!res.ok) {
+ throw new Error("Server returned status " + res.status);
+ }
+ return res.json();
+ })
+ .then(data => {
+ if (!data || !data.success) {
+ throw new Error((data && data.error) || "Unerwartete Antwort vom Server.");
+ }
+ renderScResult(data);
+ })
+ .catch(err => {
+ showScError("Fehler bei der Prüfung: " + err.message);
+ });
+ });
+ }
+
// --- Initialzustand -----------------------------------------------------
setStatus("Bereit. Wähle zuerst deinen USB-Stick aus.");
@@ -689,4 +864,7 @@ document.addEventListener("DOMContentLoaded", () => {
setOverallStatus("ok", "Noch kein Test durchgeführt.");
logLine("USB-Browser-Test (fakecheck) geladen. Warte auf Verzeichnisauswahl und Modus.");
updateStartButtonState();
+
+ // Serialcheck nur initialisieren, wenn das Partial vorhanden ist
+ initSerialCheckWidget();
});