diff --git a/application/IpNetwork/IpNetwork.php b/application/IpNetwork/IpNetwork.php deleted file mode 100644 index a801c807c..000000000 --- a/application/IpNetwork/IpNetwork.php +++ /dev/null @@ -1,9 +0,0 @@ -loadMe(); @@ -16,15 +16,14 @@ class IpNetworkController extends mfBaseController { } protected function indexAction(): void { - $JSGlobals = ["BASE_URL" => self::getUrl("IpNetwork"), - "DASHBOARD_URL" => self::getUrl("Dashboard"), - "MFAPPNAME" => MFAPPNAME_SLUG, + $JSGlobals = [ + "BASE_URL" => self::getUrl("IpNetwork"), + "API_BASE_URL" => self::getUrl("IpNetwork"), "PAGE_TITLE" => "IPAM", "PATH" => [ ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], ["text" => "IPAM", "href" => self::getUrl("IpNetwork")] ], - "IPNETWORK_API_URL" => self::getUrl("IpNetwork/api"), ]; $this->layout()->set("vueViewName", "IpNetwork"); @@ -32,187 +31,124 @@ class IpNetworkController extends mfBaseController { $this->layout()->setTemplate("VueViews/Vue"); } - protected function apiAction() { - $do = $this->request->do; - - if (!$this->me->isAdmin()) { - $this->redirect("dashboard"); - } - - switch ($do) { - case "get": - $return = $this->get(); - break; - case "getById": - $return = $this->getById(); - break; - case "create": - $return = $this->create(); - break; - case "update": - $return = $this->update(); - break; - case "delete": - $return = $this->delete(); - break; - default: - $return = false; - break; - } - - if (!$return) { - $return = [ - "status" => "error", - "message" => "Invalid request." - ]; - } - - header('Content-Type: application/json'); - die(json_encode($return)); - } - - private function aggregateChildren($network, &$childrenCount) { - $children = IpNetworkModel::getChildren($network->id); - - foreach ($children as $child) { - $childrenCount++; - if ($child->cidr !== 32) { - $this->aggregateChildren($child, $childrenCount); - } - } - } - - private function get(): array { + protected function getAction() { $json = json_decode(file_get_contents('php://input'), true); $filters = $json['filters'] ?? []; - $order = $json['order'] ?? []; + $order = $json['order'] ?? ['key' => 'network_address', 'order' => 'asc']; $page = $json['pagination']['page'] ?? 1; - $perPage = $json['pagination']['per_page'] ?? 10; + $perPage = $json['pagination']['per_page'] ?? 50; + $offset = ($page - 1) * $perPage; - $orderChildren = false; + $networks = IpNetworkModel::getIpNetworks($filters, $perPage, $offset, $order); + $total_rows = IpNetworkModel::countIpNetworks($filters); - if ($order['key'] === 'network_address_str') { - $order['key'] = 'network_address'; - } else if ($order['key'] === 'children') { - $orderChildren = $order['order']; - $order = null; - } - - $networks = IpNetworkModel::getIpNetworks($filters, null, $perPage * $page - $perPage, $order); - $total_rows = IpNetworkModel::countIpNetworks([ - "parent_network_id" => $filters['parent_network_id'] ?? '' - ]); - - $processedNetworks = []; - - foreach ($networks as $network) { - $childrenCount = 0; - $this->aggregateChildren($network, $childrenCount); - - $from = $filters['children']['from'] ?? null; - $to = $filters['children']['to'] ?? null; - - if (($from !== null && $childrenCount < $from) || ($to !== null && $childrenCount > $to)) { - continue; - } - - $network->children = $childrenCount; - $processedNetworks[] = $network; - } - - if ($orderChildren) { - usort($processedNetworks, function ($a, $b) use ($orderChildren) { - if ($orderChildren === 'asc') { - return $a->children <=> $b->children; - } else { - return $b->children <=> $a->children; - } - }); - } - - return [ - "rows" => array_slice($processedNetworks, $perPage * $page - $perPage, $perPage), + self::returnJson([ + "rows" => $networks, "pagination" => [ "page" => $page, - "total_pages" => ceil(count($processedNetworks) / $perPage), - "filtered_available" => count($processedNetworks), + "total_pages" => ceil($total_rows / $perPage), + "filtered_available" => intval($total_rows), "per_page" => $perPage, "total_rows" => intval($total_rows) ] - ]; - + ]); } - private function getById(): array { - $json = json_decode(file_get_contents('php://input'), true); - - $network = IpNetworkModel::getById($json['id']); - - if ($network === null) { - return [ - "status" => "error", - "message" => "Network not found." - ]; + protected function getByIdAction() { + $id = $this->request->id; + if (!$id) { + self::sendError("Network ID is missing."); } - return [ - "status" => "success", - "network" => $network - ]; + $network = IpNetworkModel::getById($id); + + if ($network === null) { + self::sendError("Network not found."); + } + + self::returnJson(["success" => true, "network" => $network]); } + protected function globalSearchAction() { + $query = $this->request->q ?? ''; + if (strlen($query) < 2) { + self::returnJson([]); + return; + } + $suggestions = IpNetworkModel::findSuggestions($query); + self::returnJson($suggestions); + } - private function create(): array { + protected function findNetworkByStringAction() { + $networkString = $this->request->network_string; + if (!$networkString) { + self::sendError("Network string is missing."); + } + + $network = IpNetworkModel::findByNetworkString($networkString); + + if ($network === null) { + self::sendError("Network not found."); + } + + $navigateToId = null; + if ($network->cidr < 31) { + $navigateToId = $network->id; + } else { + $navigateToId = $network->parent_network_id; + } + + self::returnJson(["success" => true, "navigateToId" => $navigateToId]); + } + + protected function createAction() { $json = json_decode(file_get_contents('php://input'), true); try { IpNetworkModel::createIpNetwork($json); - return [ - "status" => "success", - "message" => "IP Network created." - ]; + self::returnJson([ + "success" => true, + "message" => "IP-Netzwerk wurde erstellt." + ]); } catch (Exception $e) { - return [ - "status" => "error", - "message" => $e->getMessage() - ]; + self::sendError($e->getMessage()); } } - private function update(): array { + protected function updateAction() { $json = json_decode(file_get_contents('php://input'), true); + if (empty($json['id'])) { + self::sendError("Netzwerk-ID fehlt."); + } + try { IpNetworkModel::updateIpNetwork($json); - return [ - "status" => "success", - "message" => "IP Network updated." - ]; + self::returnJson([ + "success" => true, + "message" => "IP-Netzwerk wurde aktualisiert." + ]); } catch (Exception $e) { - return [ - "status" => "error", - "message" => $e->getMessage() - ]; + self::sendError($e->getMessage()); } } - private function delete(): array { + protected function deleteAction() { $json = json_decode(file_get_contents('php://input'), true); + if (empty($json['id'])) { + self::sendError("Netzwerk-ID fehlt."); + } + try { IpNetworkModel::deleteIpNetwork($json['id']); - return [ - "status" => "success", - "message" => "IP Network deleted." - ]; + self::returnJson([ + "success" => true, + "message" => "IP-Netzwerk wurde gelöscht." + ]); } catch (Exception $e) { - return [ - "status" => "error", - "message" => $e->getMessage() - ]; + self::sendError($e->getMessage()); } } - - } \ No newline at end of file diff --git a/application/IpNetwork/IpNetworkModel.php b/application/IpNetwork/IpNetworkModel.php index 6f5811861..6987d57bb 100644 --- a/application/IpNetwork/IpNetworkModel.php +++ b/application/IpNetwork/IpNetworkModel.php @@ -1,4 +1,5 @@ link; + + if (!empty($filters['globalSearch'])) { + $searchTerm = $db->real_escape_string($filters['globalSearch']); + $sqlConditions[] = " (CONCAT(INET_NTOA(network_address), '/', cidr) LIKE '%{$searchTerm}%' OR `name` LIKE '%{$searchTerm}%' OR `description` LIKE '%{$searchTerm}%') "; + } + + if (isset($filters['name'])) $sqlConditions[] = Helper::generateFilterCondition($filters['name'], 'name'); + if (isset($filters['description'])) $sqlConditions[] = Helper::generateFilterCondition($filters['description'], 'description'); + if (isset($filters['location'])) $sqlConditions[] = Helper::generateFilterCondition($filters['location'], 'location'); + if (isset($filters['status'])) $sqlConditions[] = Helper::generateFilterCondition($filters['status'], 'status'); + + if (empty($filters['parent_network_id'])) { + $sqlConditions[] = " `parent_network_id` IS NULL "; + } else { + $sqlConditions[] = " `parent_network_id` = " . intval($filters['parent_network_id']) . " "; + } + + return empty($sqlConditions) ? "" : " WHERE " . implode(" AND ", $sqlConditions); + } + public static function getIpNetworks($filters, $limit = null, $offset = 0, $order = null): array { - $db = FronkDB::singleton(); + $db = FronkDB::singleton()->link; + + $orderClause = "ORDER BY `network_address` ASC"; + if ($order && !empty($order['key'])) { + $orderKey = $db->real_escape_string($order['key']); + $orderDir = (isset($order['order']) && strtolower($order['order']) === 'desc') ? 'DESC' : 'ASC'; + if ($orderKey === 'network_address_str') $orderKey = 'network_address'; + $orderClause = "ORDER BY `{$orderKey}` {$orderDir}"; + } + + $limitClause = is_null($limit) ? "" : " LIMIT " . intval($limit) . " OFFSET " . intval($offset); + + $sql = " + SELECT + main.*, + CONCAT(INET_NTOA(main.network_address), '/', main.cidr) AS network_address_str, + (SELECT COUNT(*) FROM `IpNetwork` WHERE `parent_network_id` = main.id) as children + FROM `IpNetwork` main + " . self::getSqlFilter($filters) . " + " . $orderClause . " + " . $limitClause; - $sql = "SELECT *, CONCAT(INET_NTOA(network_address), '/', cidr) AS network_address_str FROM `IpNetwork` WHERE 1 "; - $sql .= isset($filters['network_address_str']) ? " AND CONCAT(INET_NTOA(network_address), '/', cidr) LIKE '%" . $filters['network_address_str'] . "%'" : ""; - $sql .= self::getSqlFilter($filters); - $sql .= $order === null || $order['key'] === null ? " ORDER BY `network_address` ASC" : " ORDER BY `" . $order['key'] . "` " . $order['order']; - $sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset; $result = $db->query($sql); $rows = []; @@ -40,209 +80,140 @@ class IpNetworkModel { return $rows; } - public static function getSqlFilter($filters): string { - $sql = isset($filters['name']) ? Helper::generateFilterCondition($filters['name'], 'name') : ""; - $sql .= isset($filters['description']) ? Helper::generateFilterCondition($filters['description'], 'description') : ""; - $sql .= isset($filters['location']) ? Helper::generateFilterCondition($filters['location'], 'location') : ""; - $sql .= isset($filters['status']) ? Helper::generateFilterCondition($filters['status'], 'status') : ""; - $sql .= empty($filters['parent_network_id']) ? " AND `parent_network_id` IS NULL" : " AND `parent_network_id` = " . $filters['parent_network_id']; - - return $sql; - } - - public static function countIpNetworks($filters) { - $db = FronkDB::singleton(); - $sql = "SELECT COUNT(*) as `total_rows` FROM `IpNetwork` WHERE 1 " . self::getSqlFilter($filters); - $result = $db->query($sql); - return $result->fetch_assoc()['total_rows']; - } - - public static function countChildren($id) { - $db = FronkDB::singleton(); - $sql = "SELECT COUNT(*) as `total_rows` FROM `IpNetwork` WHERE `parent_network_id` = $id"; - $result = $db->query($sql); - return $result->fetch_assoc()['total_rows']; - } - - public static function getChildren($id): array { - $db = FronkDB::singleton(); - $sql = "SELECT * FROM `IpNetwork` WHERE `parent_network_id` = $id"; - $result = $db->query($sql); - $rows = []; - while ($row = $result->fetch_assoc()) { - $rows[] = new IpNetworkModel($row); + public static function findByNetworkString(string $networkString): ?IpNetworkModel { + $db = FronkDB::singleton()->link; + if (!str_contains($networkString, '/')) { + return null; } + // Extract IP and CIDR, allowing for extra text like "(My Network)" + preg_match('/^([0-9\.]+)\/(\d+)/', $networkString, $matches); + if (count($matches) < 3) { + return null; + } + $ip = $db->real_escape_string($matches[1]); + $cidr = (int)$matches[2]; - return $rows; + $sql = "SELECT * FROM `IpNetwork` WHERE `network_address` = INET_ATON('$ip') AND `cidr` = $cidr"; + $result = $db->query($sql); + $row = $result->fetch_assoc(); + return $row ? new IpNetworkModel($row) : null; + } + + public static function countIpNetworks($filters): int { + $db = FronkDB::singleton()->link; + $sql = "SELECT COUNT(*) as `total_rows` FROM `IpNetwork`" . self::getSqlFilter($filters); + $result = $db->query($sql); + return (int)$result->fetch_assoc()['total_rows']; + } + + public static function findSuggestions(string $query, int $limit = 10): array { + $db = FronkDB::singleton()->link; + $query = $db->real_escape_string($query); + $sql = " + SELECT + CONCAT(INET_NTOA(network_address), '/', cidr) as network_address_str, + name + FROM `IpNetwork` + WHERE + CONCAT(INET_NTOA(network_address), '/', cidr) LIKE '%{$query}%' OR + `name` LIKE '%{$query}%' OR + `description` LIKE '%{$query}%' + LIMIT " . $limit; + + $result = $db->query($sql); + $suggestions = []; + while ($row = $result->fetch_assoc()) { + $text = $row['network_address_str']; + if ($row['name']) { + $text .= " ({$row['name']})"; + } + $suggestions[] = [ + 'value' => $row['network_address_str'], + 'text' => $text + ]; + } + return $suggestions; } /** * @throws Exception */ public static function createIpNetwork($data): void { - $db = FronkDB::singleton(); + $db = FronkDB::singleton()->link; $network_address = $data['network_address']; - $cidr = $data['cidr']; - $parent_network_id = $data['parent_network_id'] ?? 'NULL'; - $status = $data['status']; - $name = $data['name']; - $description = $data['description']; - $location = $data['location']; + if (!filter_var($network_address, FILTER_VALIDATE_IP)) { + throw new Exception("Ungültige IP-Adresse angegeben."); + } - // Convert network address to integer - $network_address_int = ip2long($network_address); - - // Define query to check for overlapping networks - $check_sql = " - SELECT `id`, `network_address`, `cidr` - FROM `IpNetwork` - WHERE ( - (INET_ATON('$network_address') & ~((1 << (32 - `cidr`)) - 1)) = `network_address` - OR - (`network_address` & ~((1 << (32 - $cidr)) - 1)) = INET_ATON('$network_address') - )"; - -// die($check_sql); + $cidr = (int)$data['cidr']; + $parent_network_id = !empty($data['parent_network_id']) ? (int)$data['parent_network_id'] : 'NULL'; + $status = $db->real_escape_string($data['status']); + $name = $db->real_escape_string($data['name']); + $description = $db->real_escape_string($data['description']); + $location = $db->real_escape_string($data['location']); + $check_sql = "SELECT id FROM `IpNetwork` WHERE `network_address` = INET_ATON('$network_address') AND `cidr` = $cidr"; $result = $db->query($check_sql); - - $parentFound = false; - $parentFoundId = null; - - while ($row = $result->fetch_assoc()) { - $existing_network_address_int = $row['network_address']; - $existing_cidr = $row['cidr']; - - // Check if the new network is within an existing network - if ($network_address_int >= $existing_network_address_int && - $network_address_int < ($existing_network_address_int + pow(2, (32 - $existing_cidr)))) { - - if ($cidr <= $existing_cidr) { - // The new network is larger or equal, which is invalid - if ($cidr == 32) { - throw new Exception("Address $network_address/32 already exists"); - } else { - throw new Exception("Network $network_address/$cidr conflicts with existing network " . long2ip($existing_network_address_int) . "/$existing_cidr"); - } - } - - // Check if the new network is a correct subnetwork - if ($parent_network_id != 'NULL') { - $result->data_seek(0); - while ($row = $result->fetch_assoc()) { - if ($row['id'] == $parent_network_id) { - $parentFoundId = $row['id']; - $parentFound = true; - break; - } - } - if (!$parentFound) { - throw new Exception("Parent network ID $parent_network_id does not match the actual parent network ID {$row['id']}"); - } - -// New check for conflicts with child networks - $check_child_sql = " - SELECT `id`, `network_address`, `cidr` - FROM `IpNetwork` - WHERE `parent_network_id` = $parent_network_id - AND ( - INET_ATON('$network_address') BETWEEN `network_address` - AND (`network_address` + POW(2, (32 - `cidr`)) - 1) - OR - `network_address` BETWEEN INET_ATON('$network_address') - AND (INET_ATON('$network_address') + POW(2, (32 - $cidr)) - 1) - )"; -; - - $child_result = $db->query($check_child_sql); - - while ($child_row = $child_result->fetch_assoc()) { - $existing_child_network_address_int = $child_row['network_address']; - $existing_child_cidr = $child_row['cidr']; - - // Check if the new network overlaps any existing child networks - if ($network_address_int < $existing_child_network_address_int && - ($network_address_int + pow(2, (32 - $cidr))) > $existing_child_network_address_int) { - throw new Exception("Network $network_address/$cidr conflicts with child network " . long2ip($existing_child_network_address_int) . "/$existing_child_cidr"); - } - } - - } else { - // If no parent ID provided and the new network is within an existing network, throw an error - throw new Exception("Network $network_address/$cidr must be a child of " . long2ip($existing_network_address_int) . "/$existing_cidr."); - } - - // For CIDR 32, check if it already exists but also check if $parent_network_id is same as $parentFoundId but if $parentFoundId is null, throw an error - if ($cidr == 32 && $parent_network_id != 'NULL' && $parent_network_id != $parentFoundId) { - throw new Exception("CIDR 32 address $network_address already exists within " . long2ip($existing_network_address_int) . "/$existing_cidr"); - } - } - - // Check if the new network overlaps any existing networks - if ($network_address_int < $existing_network_address_int && - ($network_address_int + pow(2, (32 - $cidr))) > $existing_network_address_int) { - throw new Exception("Network $network_address/$cidr overlaps with existing network " . long2ip($existing_network_address_int) . "/$existing_cidr"); - } + if($result->num_rows > 0) { + throw new Exception("Ein identisches Netzwerk existiert bereits."); } - if (!$parentFound && $parent_network_id === 'NULL' && intval($cidr) >= 32) { - throw new Exception("Root Networks cannot be single IPs"); + if ($parent_network_id === 'NULL' && $cidr >= 32) { + throw new Exception("Stamm-Netzwerke können keine einzelnen IPs sein."); } - // Proceed with insertion if no conflicts are found $sql = "INSERT INTO `IpNetwork` (`network_address`, `cidr`, `parent_network_id`, `status`, `name`, `description`, `location`, `create`, `edit`) - VALUES (INET_ATON('$network_address'), $cidr, $parent_network_id, '$status', '$name', '$description', '$location', UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"; - $result = $db->query($sql); + VALUES (INET_ATON('$network_address'), $cidr, $parent_network_id, '$status', '$name', '$description', '$location', UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"; - if (!$result) { - throw new Exception("Failed to insert network"); + if (!$db->query($sql)) { + throw new Exception("Fehler beim Einfügen des Netzwerks: " . $db->error); } } public static function updateIpNetwork($data): void { - $db = FronkDB::singleton(); + $db = FronkDB::singleton()->link; + $id = (int)$data['id']; - $sqlSetStr = ""; - $sqlSetStr .= isset($data['status']) ? "`status` = '" . $data['status'] . "', " : ""; - $sqlSetStr .= isset($data['name']) ? "`name` = '" . $data['name'] . "', " : ""; - $sqlSetStr .= isset($data['description']) ? "`description` = '" . $data['description'] . "', " : ""; - $sqlSetStr .= isset($data['location']) ? "`location` = '" . $data['location'] . "', " : ""; - $sqlSetStr .= "`edit` = UNIX_TIMESTAMP()"; + $sqlSetStr = []; + if (isset($data['status'])) $sqlSetStr[] = "`status` = '" . $db->real_escape_string($data['status']) . "'"; + if (isset($data['name'])) $sqlSetStr[] = "`name` = '" . $db->real_escape_string($data['name']) . "'"; + if (isset($data['description'])) $sqlSetStr[] = "`description` = '" . $db->real_escape_string($data['description']) . "'"; + if (isset($data['location'])) $sqlSetStr[] = "`location` = '" . $db->real_escape_string($data['location']) . "'"; - $sql = "UPDATE `IpNetwork` SET $sqlSetStr WHERE `id` = " . $data['id']; - $result = $db->query($sql); + if(empty($sqlSetStr)) return; - if (!$result) { - throw new Exception("Failed to update network"); + $sqlSetStr[] = "`edit` = UNIX_TIMESTAMP()"; + + $sql = "UPDATE `IpNetwork` SET " . implode(', ', $sqlSetStr) . " WHERE `id` = $id"; + + if (!$db->query($sql)) { + throw new Exception("Fehler beim Aktualisieren des Netzwerks: " . $db->error); } } - public static function getById($id) { - $db = FronkDB::singleton(); - $sql = "SELECT *, INET_NTOA(network_address) as network_address_str FROM `IpNetwork` WHERE `id` = $id"; + $db = FronkDB::singleton()->link; + $id = (int)$id; + $sql = "SELECT *, CONCAT(INET_NTOA(network_address), '/', cidr) as network_address_str FROM `IpNetwork` WHERE `id` = $id"; $result = $db->query($sql); $row = $result->fetch_assoc(); return $row ? new IpNetworkModel($row) : null; } public static function deleteIpNetwork($id) { - // delete this id and all children and children of children until no more children - $db = FronkDB::singleton(); - $sql = "SELECT `id` FROM `IpNetwork` WHERE `parent_network_id` = $id"; - $result = $db->query($sql); + $db = FronkDB::singleton()->link; + $id = (int)$id; + $child_sql = "SELECT `id` FROM `IpNetwork` WHERE `parent_network_id` = $id"; + $result = $db->query($child_sql); while ($row = $result->fetch_assoc()) { self::deleteIpNetwork($row['id']); } - $sql = "DELETE FROM `IpNetwork` WHERE `id` = $id"; - $result = $db->query($sql); - if (!$result) { - throw new Exception("Failed to delete network"); + $delete_sql = "DELETE FROM `IpNetwork` WHERE `id` = $id"; + if (!$db->query($delete_sql)) { + throw new Exception("Fehler beim Löschen des Netzwerks: " . $db->error); } - } - } \ No newline at end of file diff --git a/public/js/pages/IpNetwork/IpNetwork.js b/public/js/pages/IpNetwork/IpNetwork.js index dee1a0f00..026dac2be 100644 --- a/public/js/pages/IpNetwork/IpNetwork.js +++ b/public/js/pages/IpNetwork/IpNetwork.js @@ -1,226 +1,285 @@ +// IpNetwork.js Vue.component('IpNetwork', { - //language=Vue template: ` - - - - - - - - - - - - - -