query('SELECT * FROM network'); while ($results !== false && $res = $results->fetchArray(SQLITE3_ASSOC)) { $id = intval($res['id']); // Get IP addresses and host names for this device $res['ip'] = array(); $res['name'] = array(); $network_addresses = $db->query("SELECT ip,name FROM network_addresses WHERE network_id = {$id} ORDER BY lastSeen DESC"); while ($network_addresses !== false && $network_address = $network_addresses->fetchArray(SQLITE3_ASSOC)) { array_push($res['ip'], $network_address['ip']); if ($network_address['name'] !== null) { array_push($res['name'], utf8_encode($network_address['name'])); } else { array_push($res['name'], ''); } } $network_addresses->finalize(); // UTF-8 encode vendor $res['macVendor'] = utf8_encode($res['macVendor']); array_push($network, $res); } $results->finalize(); $data = array_merge($data, array('network' => $network)); } if (isset($_GET['getAllQueries']) && $auth) { $allQueries = array(); if ($_GET['getAllQueries'] !== 'empty') { $from = intval($_GET['from']); $until = intval($_GET['until']); // Use table "query_storage" // - replace domain ID with domain // - replace client ID with client name // - replace forward ID with forward destination $dbquery = 'SELECT timestamp, type,'; $dbquery .= " CASE typeof(domain) WHEN 'integer' THEN (SELECT domain FROM domain_by_id d WHERE d.id = q.domain) ELSE domain END domain,"; $dbquery .= " CASE typeof(client) WHEN 'integer' THEN ("; $dbquery .= " SELECT CASE TRIM(name) WHEN '' THEN c.ip ELSE c.name END name FROM client_by_id c WHERE c.id = q.client"; $dbquery .= ' ) ELSE client END client,'; $dbquery .= " CASE typeof(forward) WHEN 'integer' THEN (SELECT forward FROM forward_by_id f WHERE f.id = q.forward) ELSE forward END forward,"; $dbquery .= ' status, reply_type, reply_time, dnssec'; $dbquery .= ' FROM query_storage q'; $dbquery .= ' WHERE timestamp >= :from AND timestamp <= :until '; if (isset($_GET['status'])) { // if some query status should be excluded $excludedStatus = $_GET['status']; if (preg_match('/^[0-9]+(?:,[0-9]+)*$/', $excludedStatus) === 1) { // Append selector to DB query. The used regex ensures // that only numbers, separated by commas are accepted // to avoid code injection and other malicious things // We accept only valid lists like "1,2,3" // We reject ",2,3", "1,2," and similar arguments $dbquery .= 'AND status NOT IN ('.$excludedStatus.') '; } else { exit('Error. Selector status specified using an invalid format.'); } } $dbquery .= 'ORDER BY timestamp ASC'; $stmt = $db->prepare($dbquery); $stmt->bindValue(':from', intval($from), SQLITE3_INTEGER); $stmt->bindValue(':until', intval($until), SQLITE3_INTEGER); $results = $stmt->execute(); // Start the JSON string echo '{"data":['; if (!is_bool($results)) { $first = true; while ($row = $results->fetchArray(SQLITE3_ASSOC)) { // Insert a comma before the next record (except on the first one) if (!$first) { echo ','; } else { $first = false; } // Format, encode, transform each field (if necessary). $time = $row['timestamp']; $query_type = getQueryTypeStr($row['type']); // Convert query type ID to name $domain = utf8_encode(str_replace('~', ' ', $row['domain'])); $client = $row['client']; $status = $row['status']; $destination = utf8_encode($row['forward']); $reply_type = $row['reply_type']; $reply_time = $row['reply_time']; $dnssec = $row['dnssec']; // Insert into array and output it in JSON format echo json_encode(array($time, $query_type, $domain, $client, $status, $destination, $reply_type, $reply_time, $dnssec)); } } // Finish the JSON string echo ']}'; // exit at the end exit; } // only used if getAllQueries==empty $result = array('data' => $allQueries); $data = array_merge($data, $result); } if (isset($_GET['topClients']) && $auth) { // $from = intval($_GET["from"]); $limit = ''; if (isset($_GET['from'], $_GET['until'])) { $limit = 'WHERE timestamp >= :from AND timestamp <= :until'; } elseif (isset($_GET['from']) && !isset($_GET['until'])) { $limit = 'WHERE timestamp >= :from'; } elseif (!isset($_GET['from']) && isset($_GET['until'])) { $limit = 'WHERE timestamp <= :until'; } $dbquery = "SELECT CASE typeof(client) WHEN 'integer' THEN ("; $dbquery .= " SELECT CASE TRIM(name) WHEN '' THEN c.ip ELSE c.name END name FROM client_by_id c WHERE c.id = q.client)"; $dbquery .= ' ELSE client END client, count(client) FROM query_storage q '.$limit.' GROUP BY client ORDER BY count(client) DESC LIMIT 20'; $stmt = $db->prepare($dbquery); $stmt->bindValue(':from', intval($_GET['from']), SQLITE3_INTEGER); $stmt->bindValue(':until', intval($_GET['until']), SQLITE3_INTEGER); $results = $stmt->execute(); $clientnums = array(); if (!is_bool($results)) { while ($row = $results->fetchArray()) { // $row[0] is the client IP if (array_key_exists($row[0], $clientnums)) { // Entry already exists, add to it (might appear multiple times due to mixed capitalization in the database) $clientnums[$row[0]] += intval($row[1]); } else { // Entry does not yet exist $clientnums[$row[0]] = intval($row[1]); } } } // Sort by number of hits arsort($clientnums); // Extract only the first ten entries $clientnums = array_slice($clientnums, 0, 10); $result = array('top_sources' => $clientnums); $data = array_merge($data, $result); } if (isset($_GET['topDomains']) && $auth) { $limit = ''; if (isset($_GET['from'], $_GET['until'])) { $limit = ' AND timestamp >= :from AND timestamp <= :until'; } elseif (isset($_GET['from']) && !isset($_GET['until'])) { $limit = ' AND timestamp >= :from'; } elseif (!isset($_GET['from']) && isset($_GET['until'])) { $limit = ' AND timestamp <= :until'; } // Select top permitted domains only $stmt = $db->prepare('SELECT domain,count(domain) FROM queries WHERE status IN (2,3,12,13,14,17)'.$limit.' GROUP by domain order by count(domain) desc limit 20'); $stmt->bindValue(':from', intval($_GET['from']), SQLITE3_INTEGER); $stmt->bindValue(':until', intval($_GET['until']), SQLITE3_INTEGER); $results = $stmt->execute(); $domains = array(); if (!is_bool($results)) { while ($row = $results->fetchArray()) { // Convert domain to lower case UTF-8 $c = utf8_encode(strtolower($row[0])); if (array_key_exists($c, $domains)) { // Entry already exists, add to it (might appear multiple times due to mixed capitalization in the database) $domains[$c] += intval($row[1]); } else { // Entry does not yet exist $domains[$c] = intval($row[1]); } } } // Sort by number of hits arsort($domains); // Extract only the first ten entries $domains = array_slice($domains, 0, 10); $result = array('top_domains' => $domains); $data = array_merge($data, $result); } if (isset($_GET['topAds']) && $auth) { $limit = ''; if (isset($_GET['from'], $_GET['until'])) { $limit = ' AND timestamp >= :from AND timestamp <= :until'; } elseif (isset($_GET['from']) && !isset($_GET['until'])) { $limit = ' AND timestamp >= :from'; } elseif (!isset($_GET['from']) && isset($_GET['until'])) { $limit = ' AND timestamp <= :until'; } $stmt = $db->prepare('SELECT domain,count(domain) FROM queries WHERE status IN (1,4,5,6,7,8,9,10,11)'.$limit.' GROUP by domain order by count(domain) desc limit 10'); $stmt->bindValue(':from', intval($_GET['from']), SQLITE3_INTEGER); $stmt->bindValue(':until', intval($_GET['until']), SQLITE3_INTEGER); $results = $stmt->execute(); $addomains = array(); if (!is_bool($results)) { while ($row = $results->fetchArray()) { $addomains[utf8_encode($row[0])] = intval($row[1]); } } $result = array('top_ads' => $addomains); $data = array_merge($data, $result); } if (isset($_GET['getMinTimestamp']) && $auth) { $results = $db->query('SELECT MIN(timestamp) FROM queries'); if (!is_bool($results)) { $result = array('mintimestamp' => $results->fetchArray()[0]); } else { $result = array(); } $data = array_merge($data, $result); } if (isset($_GET['getMaxTimestamp']) && $auth) { $results = $db->query('SELECT MAX(timestamp) FROM queries'); if (!is_bool($results)) { $result = array('maxtimestamp' => $results->fetchArray()[0]); } else { $result = array(); } $data = array_merge($data, $result); } if (isset($_GET['getQueriesCount']) && $auth) { $results = $db->query('SELECT COUNT(timestamp) FROM queries'); if (!is_bool($results)) { $result = array('count' => $results->fetchArray()[0]); } else { $result = array(); } $data = array_merge($data, $result); } if (isset($_GET['getDBfilesize']) && $auth) { $filesize = filesize('/etc/pihole/pihole-FTL.db'); $result = array('filesize' => $filesize); $data = array_merge($data, $result); } if (isset($_GET['getGraphData']) && $auth) { $limit = ''; if (isset($_GET['from'], $_GET['until'])) { $limit = 'timestamp >= :from AND timestamp <= :until'; } elseif (isset($_GET['from']) && !isset($_GET['until'])) { $limit = 'timestamp >= :from'; } elseif (!isset($_GET['from']) && isset($_GET['until'])) { $limit = 'timestamp <= :until'; } $interval = 600; if (isset($_GET['interval'])) { $q = intval($_GET['interval']); if ($q >= 10) { $interval = $q; } } // Round $from and $until to match the requested $interval $from = intval((intval($_GET['from']) / $interval) * $interval); $until = intval((intval($_GET['until']) / $interval) * $interval); // Count domains and blocked queries using the same intervals $sqlcommand = " SELECT (timestamp / :interval) * :interval AS interval, SUM(CASE WHEN status !=0 THEN 1 ELSE 0 END) AS domains, SUM(CASE WHEN status IN (1,4,5,6,7,8,9,10,11,15,16) THEN 1 ELSE 0 END) AS blocked FROM queries WHERE $limit GROUP BY interval ORDER BY interval"; $stmt = $db->prepare($sqlcommand); $stmt->bindValue(':from', $from, SQLITE3_INTEGER); $stmt->bindValue(':until', $until, SQLITE3_INTEGER); $stmt->bindValue(':interval', $interval, SQLITE3_INTEGER); $results = $stmt->execute(); // Parse the DB result into graph data, filling in missing interval sections with zero function parseDBData($results, $interval, $from, $until) { $domains = array(); $blocked = array(); $first_db_timestamp = -1; if (!is_bool($results)) { // Read in the data while ($row = $results->fetchArray()) { $domains[$row['interval']] = intval($row['domains']); $blocked[$row['interval']] = intval($row['blocked']); if ($first_db_timestamp === -1) { $first_db_timestamp = intval($row[0]); } } } // It is unpredictable what the first timestamp returned by the database will be. // This depends on live data. The bar graph can handle "gaps", but the Area graph can't. // Hence, we filling the "missing" timeslots with 0 to avoid wrong graphic render. // (https://github.com/pi-hole/AdminLTE/pull/2374#issuecomment-1261865428) $aligned_from = $from + (($first_db_timestamp - $from) % $interval); // Fill gaps in returned data for ($i = $aligned_from; $i < $until; $i += $interval) { if (!array_key_exists($i, $domains)) { $domains[$i] = 0; $blocked[$i] = 0; } } return array('domains_over_time' => $domains, 'ads_over_time' => $blocked); } $over_time = parseDBData($results, $interval, $from, $until); $data = array_merge($data, $over_time); } if (isset($_GET['status']) && $auth) { $extra = ';'; if (isset($_GET['ignore']) && $_GET['ignore'] === 'DNSMASQ_WARN') { $extra = "WHERE type != 'DNSMASQ_WARN';"; } $results = $db->query('SELECT COUNT(*) FROM message '.$extra); if (!is_bool($results)) { $result = array('message_count' => $results->fetchArray()[0]); } else { $result = array(); } $data = array_merge($data, $result); } if (isset($_GET['messages']) && $auth) { $extra = ';'; if (isset($_GET['ignore']) && $_GET['ignore'] === 'DNSMASQ_WARN') { $extra = "WHERE type != 'DNSMASQ_WARN';"; } $messages = array(); $results = $db->query('SELECT * FROM message '.$extra); while ($results !== false && $res = $results->fetchArray(SQLITE3_ASSOC)) { // Convert string to to UTF-8 encoding to ensure php-json can handle it. // Furthermore, convert special characters to HTML entities to prevent XSS attacks. foreach ($res as $key => $value) { if (is_string($value)) { $res[$key] = htmlspecialchars(utf8_encode($value)); } } array_push($messages, $res); } $data = array_merge($data, array('messages' => $messages)); } if (isset($_GET['jsonForceObject'])) { echo json_encode($data, JSON_FORCE_OBJECT); } else { echo json_encode($data); }