Medowar il y a 1 mois
commit
27580af2f4

+ 2 - 0
click/data/.gitkeep

@@ -0,0 +1,2 @@
+# Keep this directory in git. tracks.jsonl is created by track.php at runtime.
+# Ensure this directory is writable by the web server.

+ 4 - 0
click/data/.htaccess

@@ -0,0 +1,4 @@
+# Deny direct access to log files (Apache). PHP can still read/write them.
+<IfModule mod_authz_core.c>
+    Require all denied
+</IfModule>

+ 5 - 0
click/data/keys.json

@@ -0,0 +1,5 @@
+[
+    "ad2094ea951dcada",
+    "74b6c9675714b1c0",
+    "58f81518328b5e42"
+]

+ 32 - 0
click/data/tracks.jsonl

@@ -0,0 +1,32 @@
+{"timestamp":"2026-02-03T15:33:22+01:00","target_url":"https://www.medowar.de","ip":"213.95.133.22","ip_forwarded":"213.95.133.22","ip_real":null,"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0","referrer":null,"request_uri":"/click/track.php?to=https%3A%2F%2Fwww.medowar.de","request_method":"GET","query_string":"to=https%3A%2F%2Fwww.medowar.de","accept_language":"en,en-US;q=0.9,de-DE;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}
+{"timestamp":"2026-02-03T15:38:59+01:00","target_url":"https://www.medowar.de","ip":"54.163.50.194","ip_forwarded":"54.163.50.194","ip_real":null,"user_agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?to=https%3A%2F%2Fwww.medowar.de","request_method":"GET","query_string":"to=https%3A%2F%2Fwww.medowar.de","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:39:09+01:00","target_url":"https://www.medowar.de","ip":"100.48.156.57","ip_forwarded":"100.48.156.57","ip_real":null,"user_agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?to=https:%2F%2Fwww.medowar.de","request_method":"GET","query_string":"to=https:%2F%2Fwww.medowar.de","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:39:09+01:00","target_url":"https://www.medowar.de","ip":"4.182.160.74","ip_forwarded":"4.182.160.74","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?to=https%3A%2F%2Fwww.medowar.de","request_method":"GET","query_string":"to=https%3A%2F%2Fwww.medowar.de","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:39:55+01:00","target_url":"https://www.medowar.de","ip":"38.202.3.30","ip_forwarded":"38.202.3.30","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?to=https:%2F%2Fwww.medowar.de","request_method":"GET","query_string":"to=https:%2F%2Fwww.medowar.de","accept_language":"en-US,en;q=0.9,en;q=0.9;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:39:56+01:00","target_url":"https://www.medowar.de","ip":"108.59.15.68","ip_forwarded":"108.59.15.68","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?to=https%3A%2F%2Fwww.medowar.de","request_method":"GET","query_string":"to=https%3A%2F%2Fwww.medowar.de","accept_language":"en-US,en;q=0.9,en;q=0.9;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:40:36+01:00","target_url":"https://www.medowar.de","ip":"100.48.156.57","ip_forwarded":"100.48.156.57","ip_real":null,"user_agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?to=https%3A%2F%2Fwww.medowar.de","request_method":"GET","query_string":"to=https%3A%2F%2Fwww.medowar.de","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:41:12+01:00","target_url":"https://www.medowar.de","ip":"173.208.94.232","ip_forwarded":"173.208.94.232","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?to=https%3A%2F%2Fwww.medowar.de","request_method":"GET","query_string":"to=https%3A%2F%2Fwww.medowar.de","accept_language":"en-US,en;q=0.9,en;q=0.9;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:47:34+01:00","key":"ad2094ea951dcada","target_url":null,"ip":"3.215.85.1","ip_forwarded":"3.215.85.1","ip_real":null,"user_agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=ad2094ea951dcada","request_method":"GET","query_string":"key=ad2094ea951dcada","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:48:16+01:00","key":"ad2094ea951dcada","target_url":null,"ip":"38.202.3.6","ip_forwarded":"38.202.3.6","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=ad2094ea951dcada","request_method":"GET","query_string":"key=ad2094ea951dcada","accept_language":"en-US,en;q=0.9,en;q=0.9;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:52:38+01:00","key":"ad2094ea951dcada","target_url":null,"ip":"4.182.160.74","ip_forwarded":"4.182.160.74","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.162 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=ad2094ea951dcada","request_method":"GET","query_string":"key=ad2094ea951dcada","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:53:14+01:00","key":"ad2094ea951dcada","target_url":null,"ip":"4.182.160.76","ip_forwarded":"4.182.160.76","ip_real":null,"user_agent":"Mozilla/5.0 (Linux; Android 9; itel A27) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.143 Mobile Safari/537.36","referrer":"https://tool.medowar.de/click/index.php","request_uri":"/click/track.php?key=ad2094ea951dcada","request_method":"GET","query_string":"key=ad2094ea951dcada","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:53:14+01:00","key":null,"target_url":null,"ip":"4.182.160.76","ip_forwarded":"4.182.160.76","ip_real":null,"user_agent":"Mozilla/5.0 (Linux; Android 9; itel A27) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.143 Mobile Safari/537.36","referrer":"https://tool.medowar.de/click/index.php","request_uri":"/click/track.php","request_method":"GET","query_string":"","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:54:02+01:00","key":null,"target_url":null,"ip":"213.95.133.22","ip_forwarded":"213.95.133.22","ip_real":null,"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0","referrer":"https://tool.medowar.de/click/","request_uri":"/click/track.php","request_method":"GET","query_string":"","accept_language":"en,en-US;q=0.9,de-DE;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}
+{"timestamp":"2026-02-03T15:55:47+01:00","key":"ad2094ea951dcada","target_url":null,"ip":"100.50.251.173","ip_forwarded":"100.50.251.173","ip_real":null,"user_agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=ad2094ea951dcada","request_method":"GET","query_string":"key=ad2094ea951dcada","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T15:56:36+01:00","key":"ad2094ea951dcada","target_url":null,"ip":"23.111.254.30","ip_forwarded":"23.111.254.30","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=ad2094ea951dcada","request_method":"GET","query_string":"key=ad2094ea951dcada","accept_language":"en-US,en;q=0.9,en;q=0.9;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-03T22:02:31+01:00","key":"74b6c9675714b1c0","target_url":null,"ip":"149.233.2.43","ip_forwarded":"149.233.2.43","ip_real":null,"user_agent":"Vert.x-WebClient/4.5.7","referrer":null,"request_uri":"/click/track.php?key=74b6c9675714b1c0","request_method":"GET","query_string":"key=74b6c9675714b1c0","accept_language":null,"accept":null}
+{"timestamp":"2026-02-23T14:39:06+01:00","key":"58f81518328b5e42","target_url":null,"ip":"213.95.133.22","ip_forwarded":"213.95.133.22","ip_real":null,"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e42","request_method":"GET","query_string":"key=58f81518328b5e42","accept_language":"en,en-US;q=0.9,de-DE;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}
+{"timestamp":"2026-02-23T14:41:42+01:00","key":"58f81518328b5e42","target_url":null,"ip":"13.216.91.49","ip_forwarded":"13.216.91.49","ip_real":null,"user_agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e42","request_method":"GET","query_string":"key=58f81518328b5e42","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-23T14:41:45+01:00","key":"58f81518328b5e42","target_url":null,"ip":"4.182.160.2","ip_forwarded":"4.182.160.2","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.163 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e42","request_method":"GET","query_string":"key=58f81518328b5e42","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-23T14:42:47+01:00","key":"58f81518328b5e42","target_url":null,"ip":"37.221.112.75","ip_forwarded":"37.221.112.75","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e42","request_method":"GET","query_string":"key=58f81518328b5e42","accept_language":"en-US,en;q=0.9,en;q=0.9;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-23T14:43:13+01:00","key":null,"target_url":null,"ip":"4.182.160.69","ip_forwarded":"4.182.160.69","ip_real":null,"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36","referrer":"https://tool.medowar.de/click/index.php","request_uri":"/click/track.php","request_method":"GET","query_string":"","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-23T14:43:44+01:00","key":"58f81518328b5e42","target_url":null,"ip":"98.95.44.135","ip_forwarded":"98.95.44.135","ip_real":null,"user_agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e42","request_method":"GET","query_string":"key=58f81518328b5e42","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-23T14:45:19+01:00","key":"58f81518328b5e42","target_url":null,"ip":"38.132.121.109","ip_forwarded":"38.132.121.109","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e42","request_method":"GET","query_string":"key=58f81518328b5e42","accept_language":"en-US,en;q=0.9,en;q=0.9;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-23T14:57:21+01:00","key":"58f81518328b5e43","target_url":null,"ip":"213.95.133.22","ip_forwarded":"213.95.133.22","ip_real":null,"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e43","request_method":"GET","query_string":"key=58f81518328b5e43","accept_language":"en,en-US;q=0.9,de-DE;q=0.8","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}
+{"timestamp":"2026-02-23T15:03:36+01:00","key":"58f81518328b5e44","target_url":null,"ip":"4.182.160.69","ip_forwarded":"4.182.160.69","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.163 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e44","request_method":"GET","query_string":"key=58f81518328b5e44","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-23T15:04:34+01:00","key":null,"target_url":null,"ip":"4.182.160.69","ip_forwarded":"4.182.160.69","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.175 Safari/537.36","referrer":"https://tool.medowar.de/click/index.php","request_uri":"/click/track.php","request_method":"GET","query_string":"","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-23T15:17:44+01:00","key":"58f81518328b5e45","target_url":null,"ip":"4.182.160.69","ip_forwarded":"4.182.160.69","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.175 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e45","request_method":"GET","query_string":"key=58f81518328b5e45","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-02-23T15:19:12+01:00","key":null,"target_url":null,"ip":"4.182.160.2","ip_forwarded":"4.182.160.2","ip_real":null,"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36","referrer":"https://tool.medowar.de/click/index.php","request_uri":"/click/track.php","request_method":"GET","query_string":"","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-03-25T17:47:29+01:00","key":null,"target_url":null,"ip":"45.148.10.62","ip_forwarded":"45.148.10.62","ip_real":null,"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36","referrer":null,"request_uri":"/click/track.php","request_method":"GET","query_string":"","accept_language":null,"accept":null}
+{"timestamp":"2026-04-20T09:15:39+02:00","key":"58f81518328b5e42","target_url":null,"ip":"162.224.132.192","ip_forwarded":"162.224.132.192","ip_real":null,"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=58f81518328b5e42","request_method":"GET","query_string":"key=58f81518328b5e42","accept_language":"en-US,en;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}
+{"timestamp":"2026-04-20T09:38:27+02:00","key":"browser-use","target_url":null,"ip":"95.90.243.122","ip_forwarded":"95.90.243.122","ip_real":null,"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36","referrer":null,"request_uri":"/click/track.php?key=browser-use","request_method":"GET","query_string":"key=browser-use","accept_language":"de-DE,de;q=0.9","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}

+ 147 - 0
click/index.php

@@ -0,0 +1,147 @@
+<?php
+/**
+ * Admin page: generate tracking links and view all captured clicks.
+ */
+$dataDir = __DIR__ . '/data';
+$logFile = $dataDir . '/tracks.jsonl';
+$keysFile = $dataDir . '/keys.json';
+
+$baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
+    . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost')
+    . rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? '/'), '/') . '/';
+
+$trackingLink = $baseUrl . 'track.php';
+
+$trackedKeys = [];
+if (is_file($keysFile) && is_readable($keysFile)) {
+    $raw = file_get_contents($keysFile);
+    $decoded = json_decode($raw, true);
+    if (is_array($decoded)) {
+        $trackedKeys = $decoded;
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_key') {
+    $existing = array_flip($trackedKeys);
+    do {
+        $newKey = bin2hex(random_bytes(8));
+    } while (isset($existing[$newKey]));
+    $trackedKeys[] = $newKey;
+    if (!is_dir($dataDir)) {
+        mkdir($dataDir, 0755, true);
+    }
+    file_put_contents($keysFile, json_encode($trackedKeys, JSON_PRETTY_PRINT), LOCK_EX);
+}
+
+$maxTracks = 100;
+$tracks = [];
+if (is_file($logFile) && is_readable($logFile)) {
+    $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+    if ($lines !== false) {
+        if (count($lines) > $maxTracks) {
+            $lines = array_slice($lines, -$maxTracks);
+            file_put_contents($logFile, implode("\n", $lines) . "\n", LOCK_EX);
+        }
+        $reversed = array_reverse($lines);
+        foreach ($reversed as $line) {
+            $decoded = json_decode($line, true);
+            if (is_array($decoded)) {
+                $tracks[] = $decoded;
+            }
+        }
+    }
+}
+
+?>
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Click Tracker – Admin</title>
+    <style>
+        body { font-family: system-ui, sans-serif; margin: 1rem 2rem; max-width: 1200px; }
+        h1 { margin-top: 0; }
+        .form-group { margin-bottom: 1rem; }
+        .form-group label { display: block; margin-bottom: 0.25rem; font-weight: 500; }
+        .form-group input[type="url"] { width: 100%; max-width: 480px; padding: 0.5rem; box-sizing: border-box; }
+        .generated { margin: 1rem 0; padding: 0.75rem; background: #e8f5e9; border-radius: 4px; word-break: break-all; }
+        .generated a { color: #2e7d32; }
+        .error { color: #c62828; margin: 1rem 0; }
+        table { border-collapse: collapse; width: 100%; margin-top: 1.5rem; font-size: 0.9rem; }
+        th, td { border: 1px solid #ccc; padding: 0.5rem 0.75rem; text-align: left; vertical-align: top; }
+        th { background: #f5f5f5; font-weight: 600; }
+        tr:nth-child(even) { background: #fafafa; }
+        .col-time { white-space: nowrap; }
+        .col-ua, .col-referrer, .col-target { max-width: 280px; word-break: break-all; }
+        .empty { color: #666; font-style: italic; margin-top: 1rem; }
+    </style>
+</head>
+<body>
+    <h1>Click Tracker – Admin</h1>
+
+    <section>
+        <h2>Tracking link</h2>
+        <p class="generated">Use this link to capture clicks (redirects here after): <a href="<?php echo htmlspecialchars($trackingLink); ?>"><?php echo htmlspecialchars($trackingLink); ?></a></p>
+    </section>
+
+    <section>
+        <h2>Tracked links (by key)</h2>
+        <p>Create a unique key to get a link that identifies which link was clicked. Clicks are logged with the key.</p>
+        <form method="post" action="">
+            <input type="hidden" name="action" value="create_key">
+            <button type="submit">Create unique key</button>
+        </form>
+        <?php if (!empty($trackedKeys)): ?>
+            <?php $linkTemplate = $baseUrl . 'track.php?key=KEY'; ?>
+            <ul style="margin-top: 1rem; list-style: none; padding: 0;">
+                <?php foreach (array_reverse($trackedKeys) as $k): ?>
+                    <li style="margin-bottom: 0.5rem; padding: 0.5rem; background: #f5f5f5; border-radius: 4px;">
+                        <code style="font-size: 0.85em;"><?php echo htmlspecialchars($k); ?></code>
+                        <span style="margin-left: 0.5rem; word-break: break-all; color: #555;"><?php echo htmlspecialchars($linkTemplate); ?></span>
+                    </li>
+                <?php endforeach; ?>
+            </ul>
+            <p class="empty" style="margin-top: 0.5rem;">Replace <code>KEY</code> in the URL with the key above to construct your link.</p>
+        <?php else: ?>
+            <p class="empty">No tracked keys yet. Click the button above to create one.</p>
+        <?php endif; ?>
+    </section>
+
+    <section>
+        <h2>Past tracks</h2>
+        <?php if (empty($tracks)): ?>
+            <p class="empty">No tracks yet.</p>
+        <?php else: ?>
+            <table>
+                <thead>
+                    <tr>
+                        <th class="col-time">Time</th>
+                        <th>Key</th>
+                        <th>IP</th>
+                        <th class="col-ua">User-Agent</th>
+                        <th class="col-referrer">Referrer</th>
+                        <th class="col-target">Target URL</th>
+                        <th>Method</th>
+                        <th>Request URI</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <?php foreach ($tracks as $t): ?>
+                    <tr>
+                        <td class="col-time"><?php echo htmlspecialchars($t['timestamp'] ?? '-'); ?></td>
+                        <td><?php echo htmlspecialchars($t['key'] ?? '-'); ?></td>
+                        <td><?php echo htmlspecialchars($t['ip'] ?? '-'); ?><?php if (!empty($t['ip_forwarded'])) echo ' <small>(X-Forwarded: ' . htmlspecialchars($t['ip_forwarded']) . ')</small>'; ?></td>
+                        <td class="col-ua"><?php echo htmlspecialchars($t['user_agent'] ?? '-'); ?></td>
+                        <td class="col-referrer"><?php echo htmlspecialchars($t['referrer'] ?? '-'); ?></td>
+                        <td class="col-target"><?php echo htmlspecialchars($t['target_url'] ?? '-'); ?></td>
+                        <td><?php echo htmlspecialchars($t['request_method'] ?? '-'); ?></td>
+                        <td><?php echo htmlspecialchars($t['request_uri'] ?? '-'); ?></td>
+                    </tr>
+                    <?php endforeach; ?>
+                </tbody>
+            </table>
+        <?php endif; ?>
+    </section>
+</body>
+</html>

+ 50 - 0
click/track.php

@@ -0,0 +1,50 @@
+<?php
+/**
+ * Click tracking capture endpoint.
+ * Logs request details to data/tracks.jsonl and redirects to the main page.
+ * Usage: track.php (no parameters required)
+ */
+
+$dataDir = __DIR__ . '/data';
+$logFile = $dataDir . '/tracks.jsonl';
+
+$mainPageUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
+    . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost')
+    . rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? '/'), '/') . '/index.php';
+
+$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
+$forwarded = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : '';
+$realIp = isset($_SERVER['HTTP_X_REAL_IP']) ? $_SERVER['HTTP_X_REAL_IP'] : '';
+
+$key = isset($_GET['key']) && $_GET['key'] !== '' ? trim($_GET['key']) : null;
+
+if ($key === 'KEY') {
+    header('Location: ' . $mainPageUrl);
+    exit;
+}
+
+$payload = [
+    'timestamp' => date('c'),
+    'key' => $key,
+    'target_url' => null,
+    'ip' => $ip,
+    'ip_forwarded' => $forwarded !== '' ? $forwarded : null,
+    'ip_real' => $realIp !== '' ? $realIp : null,
+    'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null,
+    'referrer' => isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] !== '' ? $_SERVER['HTTP_REFERER'] : null,
+    'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null,
+    'request_method' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null,
+    'query_string' => isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : null,
+    'accept_language' => isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null,
+    'accept' => isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : null,
+];
+
+if (!is_dir($dataDir)) {
+    mkdir($dataDir, 0755, true);
+}
+
+$line = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
+file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
+
+header('Location: ' . $mainPageUrl);
+exit;

+ 0 - 0
fw-hook/data/einsaetze.json


+ 111 - 0
fw-hook/webhook_receiver.php

@@ -0,0 +1,111 @@
+<?php
+/**
+ * Webhook receiver: accepts POST with JSON body and appends to data/einsaetze.json.
+ * Payload is stored under webhook_events[] with a received timestamp; einsaetze is unchanged.
+ */
+
+declare(strict_types=1);
+
+header('Content-Type: application/json; charset=utf-8');
+
+const DEFAULT_DATA_PATH = 'data/einsaetze.json';
+const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
+
+$dataPath = getenv('EINSATZ_JSON_PATH') ?: DEFAULT_DATA_PATH;
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+    http_response_code(405);
+    echo json_encode(['error' => 'Method Not Allowed']);
+    exit;
+}
+
+$raw = file_get_contents('php://input');
+if ($raw === false || $raw === '') {
+    http_response_code(400);
+    echo json_encode(['error' => 'Invalid or empty JSON']);
+    exit;
+}
+
+if (strlen($raw) > MAX_BODY_SIZE) {
+    http_response_code(400);
+    echo json_encode(['error' => 'Request body too large']);
+    exit;
+}
+
+$payload = json_decode($raw, true);
+if (json_last_error() !== JSON_ERROR_NONE) {
+    http_response_code(400);
+    echo json_encode(['error' => 'Invalid or empty JSON']);
+    exit;
+}
+
+$received = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s.uP');
+
+$dir = dirname($dataPath);
+if (!is_dir($dir)) {
+    if (!@mkdir($dir, 0755, true)) {
+        http_response_code(500);
+        echo json_encode(['error' => 'Could not create data directory']);
+        exit;
+    }
+}
+
+$fp = fopen($dataPath, 'c+');
+if ($fp === false) {
+    http_response_code(500);
+    echo json_encode(['error' => 'Could not open data file']);
+    exit;
+}
+
+if (!flock($fp, LOCK_EX)) {
+    fclose($fp);
+    http_response_code(500);
+    echo json_encode(['error' => 'Could not lock data file']);
+    exit;
+}
+
+$content = stream_get_contents($fp);
+$data = [];
+if ($content !== false && $content !== '') {
+    $data = json_decode($content, true);
+    if (!is_array($data)) {
+        $data = [];
+    }
+}
+
+if (!isset($data['einsaetze'])) {
+    $data['einsaetze'] = [];
+}
+if (!isset($data['webhook_events'])) {
+    $data['webhook_events'] = [];
+}
+
+$data['webhook_events'][] = [
+    'received' => $received,
+    'data' => $payload,
+];
+$data['updated'] = $received;
+
+$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+if ($json === false) {
+    flock($fp, LOCK_UN);
+    fclose($fp);
+    http_response_code(500);
+    echo json_encode(['error' => 'Could not encode JSON']);
+    exit;
+}
+
+ftruncate($fp, 0);
+rewind($fp);
+$written = fwrite($fp, $json);
+flock($fp, LOCK_UN);
+fclose($fp);
+
+if ($written === false || $written !== strlen($json)) {
+    http_response_code(500);
+    echo json_encode(['error' => 'Could not write data file']);
+    exit;
+}
+
+http_response_code(200);
+echo json_encode(['ok' => true, 'received' => $received]);

+ 138 - 0
yubikey-nfc-id-calculator.html

@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>YubiKey NFC-ID Calculator</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            max-width: 600px;
+            margin: 50px auto;
+            padding: 20px;
+        }
+        .container {
+            border: 1px solid #ccc;
+            padding: 20px;
+            border-radius: 5px;
+        }
+        label {
+            display: block;
+            margin-bottom: 10px;
+            font-weight: bold;
+        }
+        input {
+            width: 100%;
+            padding: 8px;
+            margin-bottom: 15px;
+            box-sizing: border-box;
+            font-size: 14px;
+        }
+        button {
+            padding: 10px 20px;
+            background-color: #007bff;
+            color: white;
+            border: none;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 14px;
+        }
+        button:hover {
+            background-color: #0056b3;
+        }
+        .output {
+            margin-top: 20px;
+            padding: 15px;
+            background-color: #f5f5f5;
+            border-radius: 4px;
+            font-family: monospace;
+        }
+        .output-label {
+            font-weight: bold;
+            margin-bottom: 5px;
+        }
+        .output-value {
+            font-size: 16px;
+            word-break: break-all;
+        }
+        .error {
+            color: red;
+            margin-top: 10px;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <h1>YubiKey NFC-ID Calculator</h1>
+        <p>Enter a YubiKey serial number (decimal) to calculate its NFC-ID (v5.3.0+ format).</p>
+        
+        <label for="serialNumber">Serial Number (Decimal):</label>
+        <input type="number" id="serialNumber" placeholder="e.g., 23852733" min="0" step="1">
+        
+        <button onclick="calculateNFCID()">Calculate NFC-ID</button>
+        
+        <div id="output"></div>
+        <div id="error" class="error"></div>
+    </div>
+
+    <script>
+        function calculateNFCID() {
+            const serialInput = document.getElementById('serialNumber').value;
+            const errorDiv = document.getElementById('error');
+            const outputDiv = document.getElementById('output');
+            
+            // Clear previous output
+            errorDiv.textContent = '';
+            outputDiv.innerHTML = '';
+            
+            // Validate input
+            if (!serialInput || serialInput.trim() === '') {
+                errorDiv.textContent = 'Please enter a serial number.';
+                return;
+            }
+            
+            const serialNumber = parseInt(serialInput, 10);
+            
+            if (isNaN(serialNumber) || serialNumber < 0) {
+                errorDiv.textContent = 'Please enter a valid positive number.';
+                return;
+            }
+            
+            // Convert to hex and pad to 8 characters (4 bytes)
+            let hexString = serialNumber.toString(16).toLowerCase();
+            hexString = hexString.padStart(8, '0');
+            
+            // Extract the 4 bytes
+            // serial_0 is most significant (leftmost), serial_3 is least significant (rightmost)
+            const serial_0 = hexString.substring(0, 2);
+            const serial_1 = hexString.substring(2, 4);
+            const serial_2 = hexString.substring(4, 6);
+            const serial_3 = hexString.substring(6, 8);
+            
+            // Calculate NFC-ID according to formula:
+            // 0x88 0x27 serial_3 serial_2 serial_1 serial_0 serial_2 serial_3
+            // Based on user example, they want: 27 serial_3 serial_2 serial_1 serial_0 serial_2 serial_3
+            const nfcId = '27' + serial_3 + serial_2 + serial_1 + serial_0 + serial_2 + serial_3;
+            
+            // Display results
+            outputDiv.innerHTML = `
+                <div class="output">
+                    <div class="output-label">Serial Number (Hex):</div>
+                    <div class="output-value">${hexString}</div>
+                </div>
+                <div class="output">
+                    <div class="output-label">NFC-ID:</div>
+                    <div class="output-value">${nfcId}</div>
+                </div>
+            `;
+        }
+        
+        // Allow Enter key to trigger calculation
+        document.getElementById('serialNumber').addEventListener('keypress', function(e) {
+            if (e.key === 'Enter') {
+                calculateNFCID();
+            }
+        });
+    </script>
+</body>
+</html>