// ==UserScript== // @name Pi-hole Client Name Manager // @namespace https://git.ponywave.de/Akamaru/Userscripts // @version 1.1 // @description Ersetzt Client-Namen in Pi-hole Admin Dashboard und bietet eine Management-Oberfläche // @author Akamaru // @match http://pi.hole/admin/* // @match http://*/* // @icon https://www.google.com/s2/favicons?domain=pi-hole.net&sz=32 // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @run-at document-idle // @updateURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/pihole-client-name-manager.user.js // @downloadURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/pihole-client-name-manager.user.js // ==/UserScript== (function() { 'use strict'; // Konfiguration const CONFIG = { defaultWhitelistedUrls: [ 'http://pi.hole/admin/' ], storageKeys: { clientNames: 'pihole_custom_client_names', whitelistUrls: 'pihole_whitelist_urls' }, apiEndpoints: { clients: '/api/clients' } }; // Lade Whitelist-URLs function loadWhitelist() { const stored = GM_getValue(CONFIG.storageKeys.whitelistUrls, '[]'); try { const parsed = JSON.parse(stored); return [...CONFIG.defaultWhitelistedUrls, ...parsed]; } catch (e) { return [...CONFIG.defaultWhitelistedUrls]; } } // Speichere Whitelist-URLs (ohne Default-URLs) function saveWhitelist(urls) { // Filtere Default-URLs raus const customUrls = urls.filter(url => !CONFIG.defaultWhitelistedUrls.includes(url)); GM_setValue(CONFIG.storageKeys.whitelistUrls, JSON.stringify(customUrls)); } // Prüfe ob aktuelle URL in Whitelist ist function isWhitelisted() { const currentOrigin = window.location.origin; const currentPath = window.location.pathname; const fullUrl = currentOrigin + currentPath; const whitelist = loadWhitelist(); // Prüfe ob eine URL aus der Whitelist matcht return whitelist.some(url => { // Entferne trailing /* falls vorhanden const cleanUrl = url.replace(/\/\*$/, ''); return fullUrl.startsWith(cleanUrl); }); } // Füge aktuelle URL zur Whitelist hinzu function addCurrentUrlToWhitelist() { const currentUrl = window.location.origin + window.location.pathname; const whitelist = loadWhitelist(); if (!whitelist.includes(currentUrl)) { whitelist.push(currentUrl); saveWhitelist(whitelist); alert(`URL zur Whitelist hinzugefügt:\n${currentUrl}\n\nBitte Seite neu laden.`); } else { alert('Diese URL ist bereits in der Whitelist.'); } } // Verwalte Whitelist über Dialog function manageWhitelist() { const whitelist = loadWhitelist(); const customUrls = whitelist.filter(url => !CONFIG.defaultWhitelistedUrls.includes(url)); let message = 'Pi-hole Client Manager - Whitelist\n\n'; message += 'Standard-URLs (nicht löschbar):\n'; CONFIG.defaultWhitelistedUrls.forEach((url, i) => { message += ` ${i + 1}. ${url}\n`; }); if (customUrls.length > 0) { message += '\nBenutzerdefinierte URLs:\n'; customUrls.forEach((url, i) => { message += ` ${i + 1}. ${url}\n`; }); message += '\n\nURL-Nummer eingeben zum Löschen, oder "0" zum Abbrechen:'; const input = prompt(message); if (input && input !== '0') { const index = parseInt(input) - 1; if (index >= 0 && index < customUrls.length) { customUrls.splice(index, 1); saveWhitelist([...CONFIG.defaultWhitelistedUrls, ...customUrls]); alert('URL entfernt! Bitte Seite neu laden.'); } else { alert('Ungültige Nummer.'); } } } else { message += '\nKeine benutzerdefinierten URLs vorhanden.\n\n'; message += 'Verwende "URL zur Whitelist hinzufügen" im Tampermonkey-Menü.'; alert(message); } } // Prüfe ob Seite Pi-hole Admin ist (Fallback) function isPiHoleAdmin() { return document.querySelector('.sidebar-menu') !== null && document.querySelector('#client-frequency') !== null; } // Speichere Custom-Namen function saveCustomNames(names) { GM_setValue(CONFIG.storageKeys.clientNames, JSON.stringify(names)); } // Lade Custom-Namen function loadCustomNames() { try { const data = GM_getValue(CONFIG.storageKeys.clientNames, '{}'); return JSON.parse(data); } catch (e) { console.error('Fehler beim Laden der Custom-Namen:', e); return {}; } } // Speichere Client-Mapping (MAC/IP -> Hostname Zuordnung) let clientMapping = {}; function updateClientMapping(clients) { // Erstelle Mapping: Hostname -> MAC/IP und IP -> MAC clientMapping = {}; clients.forEach(client => { const identifier = client.ip; // MAC oder IP const hostname = client.name; // Mapping: Hostname -> Identifier (mit und ohne .fritz.box) if (hostname) { const lowerHostname = hostname.toLowerCase(); clientMapping[lowerHostname] = identifier; // Auch ohne Domain-Suffix speichern const shortName = hostname.split('.')[0].toLowerCase(); if (shortName !== lowerHostname) { clientMapping[shortName] = identifier; } } // Wenn es eine IP-Adresse im Namen gibt, auch diese mappen const ipMatch = hostname.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/); if (ipMatch) { clientMapping[ipMatch[0]] = identifier; } }); } function findIdentifierForClient(hostnameOrIp) { // Versuche direkt zu matchen if (clientMapping[hostnameOrIp]) { return clientMapping[hostnameOrIp]; } // Versuche case-insensitive const lower = hostnameOrIp.toLowerCase(); if (clientMapping[lower]) { return clientMapping[lower]; } // Versuche ohne Domain-Suffix const shortName = hostnameOrIp.split('.')[0].toLowerCase(); if (clientMapping[shortName]) { return clientMapping[shortName]; } return hostnameOrIp; // Fallback } // Hole Clients von API function fetchClientsFromAPI() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: window.location.origin + CONFIG.apiEndpoints.clients, onload: function(response) { try { if (response.status === 200) { const data = JSON.parse(response.responseText); // Prüfe verschiedene mögliche Datenstrukturen let clientList = []; if (data.clients && Array.isArray(data.clients)) { clientList = data.clients; } else if (Array.isArray(data)) { clientList = data; } else if (data.data && Array.isArray(data.data)) { clientList = data.data; } // Normalisiere Client-Objekte clientList = clientList.map(client => { // Pi-hole API v6 nutzt 'client' für MAC/IP, 'name' für Hostname const identifier = client.ip || client.client || client.address || client.id; const displayName = client.name || client.hostname || client.comment || identifier || 'Unknown'; return { ip: String(identifier || 'unknown'), name: String(displayName), comment: client.comment || '' }; }).filter(client => client.ip !== 'unknown'); if (clientList.length > 0) { resolve(clientList); } else { resolve(getClientsFromPage()); } } else if (response.status === 401) { resolve(getClientsFromPage()); } else { resolve(getClientsFromPage()); } } catch (e) { console.error('Pi-hole Client Manager: Fehler beim Parsen der API-Antwort:', e); resolve(getClientsFromPage()); } }, onerror: function(error) { console.error('Pi-hole Client Manager: API Request fehlgeschlagen:', error); resolve(getClientsFromPage()); } }); }); } // Fallback: Hole Clients von der Seite function getClientsFromPage() { const clients = []; const tables = [ document.querySelector('#client-frequency table'), document.querySelector('#client-frequency-blocked table') ]; tables.forEach(table => { if (!table) return; const rows = table.querySelectorAll('tbody tr'); rows.forEach(row => { const link = row.querySelector('td:first-child a'); if (link) { const href = link.getAttribute('href'); const ipMatch = href.match(/client_ip=([^&]+)/); if (ipMatch) { const ip = ipMatch[1]; const name = link.textContent.trim(); // Verhindere Duplikate if (!clients.find(c => c.ip === ip)) { clients.push({ ip: ip, name: name }); } } } }); }); return clients; } // Ersetze Client-Namen in Tabellen function replaceClientNames() { const customNames = loadCustomNames(); const tables = [ document.querySelector('#client-frequency table'), document.querySelector('#client-frequency-blocked table') ]; let replacedCount = 0; tables.forEach(table => { if (!table) return; const rows = table.querySelectorAll('tbody tr'); rows.forEach(row => { const link = row.querySelector('td:first-child a'); if (link) { const href = link.getAttribute('href'); const ipMatch = href.match(/client_ip=([^&]+)/); if (ipMatch) { const ip = ipMatch[1]; // Prüfe ob Link bereits bearbeitet wurde let originalName; if (link.hasAttribute('data-original-name')) { // Link wurde bereits ersetzt, hole Original-Namen aus data-Attribut originalName = link.getAttribute('data-original-name'); } else { // Erster Durchlauf, hole Original-Namen aus Text originalName = link.textContent.trim(); // Speichere Original-Namen für spätere Durchläufe link.setAttribute('data-original-name', originalName); } // Versuche den Identifier (MAC) für diese IP/Hostname zu finden const identifierFromIp = findIdentifierForClient(ip); const identifierFromName = findIdentifierForClient(originalName); // Priorität: MAC-Adresse (enthält :) über IP-Adresse let identifier; if (identifierFromName && identifierFromName.includes(':')) { identifier = identifierFromName; // MAC-Adresse vom Hostname } else if (identifierFromIp && identifierFromIp.includes(':')) { identifier = identifierFromIp; // MAC-Adresse von IP } else { identifier = identifierFromName || identifierFromIp; // Fallback } // Prüfe ob ein Custom-Name existiert (mit IP, Hostname oder MAC) let customName = customNames[identifier] || customNames[originalName] || customNames[ip]; if (customName) { // Setze Custom-Namen nur wenn er sich unterscheidet if (link.textContent !== customName) { link.textContent = customName; replacedCount++; } // Füge Tooltip mit Original-Namen hinzu link.title = 'Original: ' + originalName + '\nIP: ' + ip; link.style.cursor = 'help'; } else { // Kein Custom-Name mehr vorhanden, stelle Original wieder her if (link.textContent !== originalName) { link.textContent = originalName; } // Entferne Tooltip link.removeAttribute('title'); link.style.cursor = ''; } } } }); }); } // Erstelle Management-Modal function createManagementModal() { // Modal HTML const modalHtml = `
${safeIp}