action !== 'CountOpenProvisionings') { http_response_code(403); die('Forbidden'); } else { header("Access-Control-Allow-Origin: https://noc.xinon.at"); header("Access-Control-Allow-Methods: POST, GET, OPTIONS"); header("Access-Control-Allow-Headers: Content-Type, Authorization"); return; } } $this->needlogin = true; $this->me = new User(); $this->me->loadMe(); $this->layout()->set("me", $this->me); if (!$this->me->is(["Admin"])) $this->redirect("Dashboard"); } protected function indexAction() { $cpecounter = 0; $r = $this->request; $page = $r->s; $this->layout()->setTemplate("Cpeprovisioning/Index"); $cpeproducts = []; $this->layout->set("filter", $this->request->filter); $filter = $this->getPreparedFilter($this->request->filter); // pagination defaults $pagination = []; $pagination['start'] = $page; $pagination['count'] = 25; $pagination['maxItems'] = 0; $order_filter = $filter; //var_dump($filter);exit; /* * Get orderproducts in need of sending a CPE */ //var_dump($order_filter);exit; $orders = OrderModel::search($order_filter); $orderproductsprefetch = OrderProductModel::precache(); foreach ($orders as $order) { if ($order_filter["hide_delayed_finish"] && $order->finish_after) { // show at most 4 weeks before finish_after date //$after_ts = Layout::dateToInt($order->finish_after); if ($order->finish_after > date("U") + (31 * 86400)) { //$this->log->debug("Before 4 weeks before finish_after oid " . $order->id); continue; } } if (is_array($orderproductsprefetch['terminations'][$order->id]) && count($orderproductsprefetch['terminations'][$order->id])) { if (!$order->cpeprovisioning_enabled && $orderproductsprefetch['terminations'][$order->id][0]['statuscode'] < TT_TERMSTATUS_CONNECTED) { continue; } } if(array_key_exists($order->id, $orderproductsprefetch)) { foreach ($orderproductsprefetch[$order->id] as $orderproduct) { if(!$orderproduct) continue; if(!is_array($orderproduct)) continue; if ($orderproduct['routerconfig_finished'] == 1) { if (!$filter['routerconfig_finished']) continue; } else { if ($filter['routerconfig_finished']) continue; } $productattributes = $orderproduct['attributes']; if (is_array($productattributes) && count($productattributes)) { // filter out products without bras_type if (array_key_exists("bras_type", $productattributes) && $productattributes) { $pagination['maxItems']++; if ($pagination['maxItems'] >= $pagination['start']+1 && $pagination['maxItems'] <= $pagination['start'] + $pagination['count']) { $cpeproducts[] = OrderProductModel::getOne($orderproduct['id']); } } else { //$this->log->debug("no bras_type oid " . $order->id); continue; } } else { // ignore products without attributes //$this->log->debug("no attributes oid " . $order->id); continue; } } } } $this->layout()->set("pagination", $pagination); $this->layout()->set("products", $cpeproducts); } private function getPreparedFilter($filter) { $new_filter = []; if (!is_array($filter)) $filter = []; if (array_key_exists("hide_delayed_finish", $filter)) { if ($filter["hide_delayed_finish"] == "1") { $new_filter["hide_delayed_finish"] = true; } else { $new_filter["hide_delayed_finish"] = false; } unset($filter["hide_delayed_finish"]); } else { $new_filter["hide_delayed_finish"] = true; } if (array_key_exists("routerconfig_finished", $filter)) { if ($filter["routerconfig_finished"] == "1") { $new_filter["routerconfig_finished"] = true; $new_filter["hide_delayed_finish"] = false; } else { $new_filter["routerconfig_finished"] = false; $order_filter["finish_date"] = null; } unset($filter["routerconfig_finished"]); } else { $new_filter["routerconfig_finished"] = false; $order_filter["finish_date"] = null; } $new_filter['upgrade'] = 0; foreach ($filter as $name => $value) { $new_filter[$name] = $value; } //var_dump($new_filter);exit; return $new_filter; } protected function saveAction() { $r = $this->request; $id = $r->id; //var_dump($r);exit; if (is_numeric($id) && $id > 0) { $mode = "edit"; $cpeprovisioning = new Cpeprovisioning($id); if (!$cpeprovisioning->id) { $this->layout()->setFlash("Eintrag nicht gefunden", "error"); $this->redirect("Cpeprovisioning"); } } else { $mode = "add"; } $order_id = $r->order_id; $termination_id = $r->termination_id; if (!(is_numeric($termination_id) && $termination_id > 0) && !(is_numeric($order_id) && $order_id > 0)) { $this->layout()->setFlash("Anschluss oder Bestellung nicht gefunden", "error"); $this->redirect("Cpeprovisioning"); } $orderproduct = OrderProductModel::getFirst(["order_id" => $order_id, "termination_id" => $termination_id]); if (!$orderproduct) { $this->layout()->setFlash("Anschluss gehört nicht zur Bestellung", "error"); $this->redirect("Cpeprovisioning"); } $prov_data = []; $prov_data["termination_id"] = ($r->termination_id) ? $r->termination_id : null; $prov_data["order_id"] = $r->order_id; $prov_data["orderproduct_id"] = $r->orderproduct_id; $prov_data["routerconfig_finished"] = ($r->routerconfig_finished) ? 1 : 0; $prov_data["routertype"] = $r->routertype; $prov_data["shipping"] = ($r->shipping) ? 1 : 0; $prov_data["wifi_ssid"] = $r->wifi_ssid; $prov_data["wifi_pass"] = $r->wifi_pass; $prov_data["mac"] = $r->mac; $prov_data["vlan_public"] = (strlen($r->vlan_public)) ? $r->vlan_public : null; $prov_data["vlan_nat"] = (strlen($r->vlan_nat)) ? $r->vlan_nat : null; $prov_data["vlan_ipv6"] = (strlen($r->vlan_ipv6)) ? $r->vlan_ipv6 : null; $prov_data["ship_weight"] = (strlen($r->ship_weight)) ? $r->ship_weight : null; $prov_data["ship_length"] = (strlen($r->ship_length)) ? $r->ship_length : null; $prov_data["ship_width"] = (strlen($r->ship_width)) ? $r->ship_width : null; $prov_data["ship_height"] = (strlen($r->ship_height)) ? $r->ship_height : null; $prov_data["note"] = $r->note; $prov_data["edit_by"] = $this->me->id; if ($mode == "add") { $prov_data["create_by"] = $this->me->id; $cpeprovisioning = CpeprovisioningModel::create($prov_data); } else { $cpeprovisioning->update($prov_data); } //var_dump($prov_data);exit; $new_id = $cpeprovisioning->save(); if (!$new_id) { $this->layout()->setFlash("Fehler beim Speichern", "error"); $this->redirect("Cpeprovisioning"); } // saved successfully, if routerconfig_finished make Journal entry in Order if ($cpeprovisioning->routerconfig_finished) { $order_product = new OrderProduct($r->orderproduct_id); if ($cpeprovisioning->shipping) { $text = "CPE zu Produkt \"" . $order_product->product->name . "\" zum Versand vorbereitet.\n\n"; } else { $text = "CPE zu Produkt \"" . $order_product->product->name . "\" vorbereitet für Techniker zur Vorortinstallation.\n\n"; } $text .= "Router: " . $cpeprovisioning->routertype . "\n"; $text .= "Zugangstyp: " . $order_product->product->attributes['bras_type']->value . "\n"; if ($cpeprovisioning->vlan_public) { $text .= "Vlan Public: " . $cpeprovisioning->vlan_public . "\n"; } if ($cpeprovisioning->vlan_nat) { $text .= "Vlan NAT: " . $cpeprovisioning->vlan_nat . "\n"; } if ($cpeprovisioning->vlan_ipv6) { $text .= "Vlan IPv6: " . $cpeprovisioning->vlan_ipv6 . "\n"; } $journal = new OrderJournal(); $journal->order_id = $order_id; $journal->text = $text; $journal->create_by = $this->me->id; $journal->edit_by = $this->me->id; $journal_id = $journal->save(); if (!$journal_id) { $this->layout()->setFlash("Konnte nicht ins Bestelljournal schreiben!", "warning"); } else { $cpeprovisioning->order_journal_id = $journal_id; $cpeprovisioning->save(); } } // save ONT sn if ($r->ont_sn) { $termination = new Termination($termination_id); $orig_sn = $termination->getWorkflowvalue("ont_sn", "string"); if ($orig_sn === null) { $sn_item = WorkflowitemModel::getFirst(["name" => "ont_sn", "object_type" => "termination"]); //var_dump($sn_item);exit; //var_dump(mfValuecache::singleton()->get("wfItemvalue-item-".$sn_item->id."-object-".$termination_id));exit; if (!$sn_item->id) { $this->log->error("ont_sn workflow item not found"); } else { $sn_item->setObjectId($termination_id); $termination->workflowitems["ont_sn"] = $sn_item; //$sn_item->value->setValue($r->ont_sn); //$sn_item->value->save(); } } if ($r->ont_sn != $orig_sn) { $termination->workflowitems["ont_sn"]->value->setValue($r->ont_sn); $termination->workflowitems["ont_sn"]->value->save(); } } $query = []; if (is_numeric($this->request->s) && $this->request->s > 0) { $query["s"] = $this->request->s; } if (is_array($this->request->filter)) { $query["filter"] = $this->request->filter; } $this->layout()->setFlash("Eintrag erfolgreich gespeichert.", "success"); $this->redirect("Cpeprovisioning", "Index", http_build_query($query)); } protected function apiSaveAction() { try { $p = json_decode(file_get_contents('php://input'), true); $id = $p['id'] ?? null; $mode = $id ? "edit" : "add"; if ($mode === 'edit') { $cpe = new Cpeprovisioning($id); if (!$cpe->id) throw new Exception("Eintrag nicht gefunden"); } if (empty($p['termination_id']) && empty($p['order_id'])) throw new Exception("Anschluss oder Bestellung nicht gefunden"); if (!OrderProductModel::getFirst(["order_id" => $p['order_id'], "termination_id" => $p['termination_id']])) throw new Exception("Anschluss gehört nicht zur Bestellung"); if (!empty($p['ont_sn'])) { $termination = new Termination($p['termination_id']); $orig_sn = $termination->getWorkflowvalue("ont_sn", "string"); if ($orig_sn === null) { if ($sn_item = WorkflowitemModel::getFirst(["name" => "ont_sn", "object_type" => "termination"])) { $sn_item->setObjectId($p['termination_id']); $termination->workflowitems["ont_sn"] = $sn_item; } else { $this->log->error("ont_sn workflow item not found"); } } if ($p['ont_sn'] !== $orig_sn && isset($termination->workflowitems["ont_sn"])) { $termination->workflowitems["ont_sn"]->value->setValue($p['ont_sn']); $termination->workflowitems["ont_sn"]->value->save(); } } $data = [ "termination_id" => $p['termination_id'] ?: null, "order_id" => $p['order_id'], "orderproduct_id" => $p['orderproduct_id'], "routerconfig_finished" => (int)($p['routerconfig_finished'] ?? 0), "shipping" => (int)($p['shipping'] ?? 0), "routertype" => $p['routertype'] ?? null, "wifi_ssid" => $p['wifi_ssid'] ?? null, "wifi_pass" => $p['wifi_pass'] ?? null, "mac" => $p['mac'] ?? null, "note" => $p['note'] ?? null, "edit_by" => $this->me->id, ]; foreach (['ship_weight', 'ship_length', 'ship_width', 'ship_height'] as $key) $data[$key] = empty($p[$key]) ? null : $p[$key]; foreach (['public', 'nat', 'ipv6'] as $type) $data["vlan_{$type}"] = !empty($p['vlans'][$type]['checked']) ? ($p['vlans'][$type]['tag'] ?? null) : null; if ($mode === 'add') { $data["create_by"] = $this->me->id; $cpe = CpeprovisioningModel::create($data); } else $cpe->update($data); if (!$cpe->save()) throw new Exception("Fehler beim Speichern"); if ($cpe->routerconfig_finished) { $order_product = new OrderProduct($p['orderproduct_id']); $shipping_text = $cpe->shipping ? "zum Versand vorbereitet" : "vorbereitet für Techniker zur Vorortinstallation"; $text = "CPE zu Produkt \"{$order_product->product->name}\" {$shipping_text}.\n\n" . "Router: {$cpe->routertype}\n" . "Zugangstyp: {$order_product->product->attributes['bras_type']->value}\n"; if ($cpe->vlan_public) $text .= "Vlan Public: {$cpe->vlan_public}\n"; if ($cpe->vlan_nat) $text .= "Vlan NAT: {$cpe->vlan_nat}\n"; if ($cpe->vlan_ipv6) $text .= "Vlan IPv6: {$cpe->vlan_ipv6}\n"; $journal = new OrderJournal(); $journal->order_id = $p['order_id']; $journal->text = $text; $journal->create_by = $this->me->id; $journal->edit_by = $this->me->id; if (!($journal_id = $journal->save())) { $this->layout()->setFlash("Konnte nicht ins Bestelljournal schreiben!", "warning"); } else { $cpe->order_journal_id = $journal_id; $cpe->save(); } } self::returnJson(['success' => true, 'message' => 'Eintrag erfolgreich gespeichert.']); } catch (Exception $e) { http_response_code(400); self::returnJson(['success' => false, 'message' => $e->getMessage()]); } } protected function apiGetAction(){ $p = json_decode(file_get_contents('php://input'), true) ?? []; // --- Pagination and Sorting setup --- $page = (int)($p['pagination']['page'] ?? 1); $perPage = (int)($p['pagination']['per_page'] ?? 25); $orderBy = $p['order']['key'] ?? null; $orderDir = $p['order']['order'] ?? 'asc'; // Calculate start and end indexes for manual pagination, just like in the old indexAction $start = ($page - 1) * $perPage; $end = $start + $perPage; // --- Data Fetching (same as before) --- $searchFilter = $this->getPreparedFilter($p['filters'] ?? []); // if routerconfig_finished === 0 then we can add key add-where to searchFilter and only show orders withing the last 180 days either by create or edit (caution this is unix timestamp) if (isset($searchFilter['routerconfig_finished']) && !$searchFilter['routerconfig_finished']) { $searchFilter['add-where'] = "`Order`.create > " . (time() - 365 * 86400) . " OR `Order`.edit > " . (time() - 365 * 86400); } $orders = OrderModel::search($searchFilter); // cpeprovisioning_enabled // Use the same uncached precache from indexAction to get all potential products // $prefetched = OrderProductModel::precache("`Terminationstatus`.`code` < " . TT_TERMSTATUS_CONNECTED . " AND `ProducttechAttribute`.`name` = 'bras_type'"); $sqlWhere = "`ProducttechAttribute`.`name` = 'bras_type'"; // if filter routerconfig_finished === 0 then we can filter the precache by routerconfig_finished = 0 or is null if (isset($searchFilter['routerconfig_finished']) && !$searchFilter['routerconfig_finished']) { // $sqlWhere .= " AND (`Cpeprovisioning`.`routerconfig_finished` = 0 OR `Cpeprovisioning`.`routerconfig_finished` IS NULL)"; } $prefetched = OrderProductModel::precache($sqlWhere); $paginatedRows = []; $totalRows = 0; $orderInfoCache = []; // Cache for order-level info to avoid repeated calculations foreach ($orders as $order) { // --- Apply the same initial filters as the old and new methods --- if (($searchFilter["hide_delayed_finish"] ?? false) && $order->finish_after && $order->finish_after > strtotime('+31 days')) continue; if (isset($prefetched['terminations'][$order->id][0]) && !$order->cpeprovisioning_enabled && $prefetched['terminations'][$order->id][0]['statuscode'] < TT_TERMSTATUS_CONNECTED) continue; if (empty($prefetched[$order->id])) continue; // Loop through the prefetched raw data, similar to indexAction foreach ($prefetched[$order->id] as $opData) { // --- Apply the same product-level filters --- if (!is_array($opData) || ($opData['routerconfig_finished'] xor ($searchFilter['routerconfig_finished'] ?? false)) || empty($opData['attributes']['bras_type'])) continue; // This item is a valid candidate for the list. $totalRows++; // *** THE CORE PERFORMANCE IMPROVEMENT *** // If the current item is not on the page we want, skip the expensive processing below. if ($totalRows <= $start || $totalRows > $end) { continue; } // --- Now, do the expensive processing ONLY for the items on the current page --- // Calculate and cache order-level info only when it's first needed for a visible item if (!isset($orderInfoCache[$order->id])) { $orderInfo = ['vot' => false, 'hw' => [], 'voip' => false]; foreach ($order->products as $prod) { $attrs = $prod->product->attributes ?? []; if (empty($attrs) || !is_array($attrs)) continue; if ($attrs['bras_type']->value ?? false) continue; $added = false; if ($attrs['hw_only']->value ?? false) { $orderInfo['hw'][] = (int)$prod->amount . "x " . $prod->product->name; $added = true; } if ($attrs['addon']->value ?? false) { $orderInfo['hw'][] = $prod->product->name; $added = true; } if (!$added && in_array($prod->product->productgroup_id, [6, 4, 8])) { $orderInfo['hw'][] = (int)$prod->amount . "x " . $prod->product->name; } if ($attrs['voip_chan']->value ?? false) $orderInfo['voip'] = true; if ($attrs['vot']->value ?? false) $orderInfo['vot'] = true; } $orderInfoCache[$order->id] = $orderInfo; } $orderInfo = $orderInfoCache[$order->id]; // Hydrate the full model object, but only for this one item $product = OrderProductModel::getOne($opData['id']); $term = $product->termination; $attrs = $product->product->attributes; $cpe = $product->cpeprovisioning; $vlanPublicDefault = $term ? $term->getPop()->vlan_public : ($attrs['vlan_default_public']->value ?? null); $vlanNatDefault = $term ? $term->getPop()->vlan_nat : ($attrs['vlan_default_nat']->value ?? null); $vlanIpv6Default = $term ? $term->getPop()->vlan_ipv6 : ($attrs['vlan_default_ipv6']->value ?? null); /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */ $paginatedRows[] = [ 'id' => $product->id, 'order_id' => $product->order_id, 'termination_id' => $product->termination_id, 'orderproduct_id' => $product->id, 'network' => $term->building->network->name ?? "{$order->owner->zip} {$order->owner->city}", 'spin' => $order->owner->spin, 'customer' => $order->owner->getCompanyOrName(), 'owner_email' => $order->owner->email, 'owner_phone' => $order->owner->phone, 'owner_customer_number' => $order->owner->customer_number ?? $order->partner_number, 'owner_full_address' => $order->owner->street . ", " . $order->owner->zip . " " . $order->owner->city, 'product_name' => $product->product->name, 'product_code' => $term->code ?? '', 'access_type' => $attrs['bras_type']->value, 'access_type_down' => $attrs["bw_down"]->value, 'access_type_up' => $attrs["bw_up"]->value, 'ont_deployed' => $term ? $term->getWorkflowValue("ont_deployed", "int") : 0, 'ont_sn' => $term ? $term->getWorkflowValue("ont_sn", "string") : null, 'vot' => $orderInfo['vot'] || $order->install_date, 'hw' => !empty($orderInfo['hw']) ? implode("
", $orderInfo['hw']) : null, 'voip' => $orderInfo['voip'], 'note' => $order->note, 'show_snopp_button' => ($attrs['hostnetwork_order']->value ?? 0) == 1 && !str_contains($product->product->name, 'XDSL'), 'snopp_url' => 'https://snopp.breitband-steiermark.at/Termination/index?filter[status][]=connected&filter[address]=' . urlencode($order->owner->street), 'vlans' => [ 'public' => ['tag' => ($cpe ? $cpe->vlan_public : null) ?? $vlanPublicDefault, 'checked' => ($cpe ? $cpe->vlan_public : null)], 'nat' => ['tag' => ($cpe ? $cpe->vlan_nat : null) ?? $vlanNatDefault, 'checked' => ($cpe ? $cpe->vlan_nat : null)], 'ipv6' => ['tag' => ($cpe ? $cpe->vlan_ipv6 : null) ?? $vlanIpv6Default, 'checked' => ($cpe ? $cpe->vlan_ipv6 : null)], ], 'cpe_id' => $cpe->id ?? null, 'cpe_data' => $this->fixCpeData($cpe->data ?? null), ]; } } // Sort the final (small) array of results for the current page if ($orderBy) { usort($paginatedRows, fn($a, $b) => ($orderDir === 'asc' ? 1 : -1) * strnatcasecmp($a[$orderBy] ?? '', $b[$orderBy] ?? '')); } // No need for array_slice, we already built the paginated list manually self::returnJson([ 'rows' => $paginatedRows, 'pagination' => [ 'page' => $page, 'per_page' => $perPage, 'total_rows' => $totalRows, 'filtered_available' => $totalRows, 'total_pages' => ceil($totalRows / $perPage) ] ]); } protected function newIndexAction() { $this->layout()->set('additionalJS', ['js/pages/Cpeprovisioning/Cpeprovisioning.js']); $this->layout()->set('additionalHead', ['']); Helper::renderVue( $this, "Cpeprovisioning", // The root Vue component name "CPE Provisioning", // The page title [ // Pass API URLs and initial data to the frontend "CPE_PROV_API_GET_URL" => $this->getUrl("Cpeprovisioning", "apiGet"), "CPE_PROV_API_SAVE_URL" => $this->getUrl("Cpeprovisioning", "apiSave"), "CPE_PROV_API_TEST_ACS_VLAN_URL" => $this->getUrl("Cpeprovisioning", "getAcsVlan"), "CPE_PROV_API_CREATE_RADIUS_USER_URL" => $this->getUrl("Cpeprovisioning", "createRadiusUser"), "CPE_PROV_PRINT_PDF_URL" => $this->getUrl("Cpeprovisioning", "printPDF"), "ORDER_URL" => $this->getUrl("Order"), "NETWORKS" => NetworkModel::getAll(), "ROUTER_OPTIONS" => [ ['value' => 'FritzBox 4050', 'text' => 'FritzBox 4050 (Inet, Phone IPTV)'], ['value' => 'FritzBox 7530', 'text' => 'FritzBox 7530 (Inet, Phone, IPTV)'], ['value' => 'FritzBox 7690', 'text' => 'FritzBox 7690 (Inet, Phone, IPTV)'], ['value' => 'FritzBox 6670 Cable', 'text' => 'FritzBox 6670 Cable (Inet, Phone, IPTV)'], // General Options ['value' => 'eigener Router', 'text' => 'Eigener Router'], ['value' => 'anderes CPE', 'text' => 'Anderes CPE'], // Static Routers ['value' => 'Mikrotik HAP AC', 'text' => 'Mikrotik HAP AC (Inet, IPTV)'], ['value' => 'Mikrotik HEX S', 'text' => 'Mikrotik HEX S (Inet, IPTV)'], ['value' => 'Mikrotik RB3011', 'text' => 'Mikrotik RB3011 (Inet, IPTV)'], // Legacy ['value' => 'FritzBox 6490 Cable', 'text' => 'FritzBox 6490 Cable (Inet, Phone, IPTV)'], ['value' => 'FritzBox 4040', 'text' => 'FritzBox 4040 (Inet, IPTV)'], ['value' => 'FritzBox 5530', 'text' => 'FritzBox 5530 (Inet FiberP2P, Phone, IPTV)'], ['value' => 'FritzBox 7590', 'text' => 'FritzBox 7590 (Inet, Phone, IPTV)'], ['value' => 'TP-Link Archer C80', 'text' => 'TP-Link Archer C80 (Inet, IPTV)'], ], "ROUTER_SHIPPING_DATA" => [ "TP-Link Archer C80" => ["weight" => 1, "length" => 35, "width" => 24, "height" => 8], "FritzBox 5530" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7], "FritzBox 4050" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7], "FritzBox 7690" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7], "FritzBox 4040" => ["weight" => 1, "length" => 30, "width" => 24, "height" => 7], "FritzBox 7530" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7], "FritzBox 7590" => ["weight" => 1, "length" => 27, "width" => 19, "height" => 7], "FritzBox 6490 Cable" => ["weight" => 1, "length" => 30, "width" => 26, "height" => 8] ] ] ); } protected function getAcsVlanAction() { $apiKey = $_SERVER['HTTP_X_API_KEY'] ?? null; $isApiCall = defined('TT_CPE_PROV_ACS_API_KEY') && $apiKey && $apiKey === TT_CPE_PROV_ACS_API_KEY; $isLoggedInUser = $this->me && $this->me->id; if (!$isApiCall && !$isLoggedInUser) { http_response_code(403); self::returnJson(['success' => false, 'message' => 'Forbidden']); return; } try { $p = json_decode(file_get_contents('php://input'), true); $mac = $p['mac'] ?? null; if (empty($mac)) { throw new Exception("MAC address is required."); } $cpe = CpeprovisioningModel::getFirst(['mac' => $mac]); if (!$cpe || !$cpe->termination_id) { throw new Exception("No active provisioning entry found for this MAC address."); } $term = new Termination($cpe->termination_id); $product = $cpe->orderproduct; if (!$term->id || !$product->id) { throw new Exception("Could not load termination or product details."); } $attrs = $product->product->attributes; // First, check if any VLAN is explicitly saved (checked in frontend) // The saved values take priority over defaults $assignedVlan = $cpe->vlan_public ?? $cpe->vlan_nat ?? $cpe->vlan_ipv6; // If no VLAN is explicitly saved, fall back to defaults if (!$assignedVlan) { $vlanPublicDefault = $term->getPop()->vlan_public ?? $attrs['vlan_default_public']->value ?? null; $vlanNatDefault = $term->getPop()->vlan_nat ?? $attrs['vlan_default_nat']->value ?? null; $vlanIpv6Default = $term->getPop()->vlan_ipv6 ?? $attrs['vlan_default_ipv6']->value ?? null; $assignedVlan = $vlanPublicDefault ?? $vlanNatDefault ?? $vlanIpv6Default; } if ($assignedVlan) { self::returnJson(['success' => true, 'vlan_id' => $assignedVlan]); } else { throw new Exception("No default VLAN could be determined for this product/POP combination."); } } catch (Exception $e) { http_response_code(400); self::returnJson(['success' => false, 'message' => $e->getMessage()]); } } protected function createRadiusUserAction() { $apiKey = $_SERVER['HTTP_X_API_KEY'] ?? null; $isApiCall = defined('TT_CPE_PROV_ACS_API_KEY') && $apiKey && $apiKey === TT_CPE_PROV_ACS_API_KEY; $isLoggedInUser = $this->me && $this->me->id; if (!$isApiCall && !$isLoggedInUser) { http_response_code(403); self::returnJson(['success' => false, 'message' => 'Forbidden']); return; } try { $p = json_decode(file_get_contents('php://input'), true); $mac = $p['mac'] ?? null; if (empty($mac)) { throw new Exception("MAC address is required."); } // Normalize MAC address format to uppercase with colons $mac = strtoupper(str_replace(['-', '.'], ':', $mac)); // Look up CPE provisioning entry $cpe = CpeprovisioningModel::getFirst(['mac' => $mac]); if (!$cpe || !$cpe->termination_id) { throw new Exception("No active provisioning entry found for this MAC address."); } $term = new Termination($cpe->termination_id); $product = $cpe->orderproduct; $order = new Order($cpe->order_id); if (!$term->id || !$product->id || !$order->id) { throw new Exception("Could not load termination, product, or order details."); } // Gather all data needed for RADIUS user $customerNumber = $order->owner->customer_number ?? $order->partner_number ?? ''; $ontSn = $term->getWorkflowValue("ont_sn", "string") ?? ''; $wifiKey = $cpe->wifi_pass ?? ''; // Check if RADIUS credentials are configured if (!defined('TT_RADIUS_URL') || !defined('TT_RADIUS_USERNAME') || !defined('TT_RADIUS_PASSWORD')) { throw new Exception("RADIUS server credentials are not configured."); } $radiusUrl = TT_RADIUS_URL; $radiusUsername = TT_RADIUS_USERNAME; $radiusPassword = TT_RADIUS_PASSWORD; // Step 1: Check if RADIUS user already exists $existingUser = $this->checkRadiusUserExists($mac); if ($existingUser) { $this->log->info("RADIUS user {$mac} already exists, skipping creation and updating details only."); } else { // Step 2: Login to RADIUS server $cookieFile = tempnam(sys_get_temp_dir(), 'radius_cookie_'); // Generate random password for RADIUS user $radiusUserPassword = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 8); $loginResult = $this->radiusHttpRequest( $radiusUrl . '/login.php', [ 'username' => $radiusUsername, 'password' => $radiusPassword, 'submit' => 'Login' ], $cookieFile ); if (strpos($loginResult, 'Login') === false && strpos($loginResult, 'logout') === false) { throw new Exception("Failed to login to RADIUS server."); } // Step 3: Create user with MAC address $createResult = $this->radiusHttpRequest( $radiusUrl . '/add_user.php', [ 'user' => $mac, 'pass' => $radiusUserPassword, 'submit' => ' Anlegen ' ], $cookieFile ); // Check if user creation was successful if (strpos($createResult, 'already exists') !== false) { // Somehow it exists now (race condition?), continue to update $this->log->info("RADIUS user {$mac} already exists, updating details."); } elseif (strpos($createResult, 'error') !== false || strpos($createResult, 'Error') !== false) { throw new Exception("Failed to create RADIUS user: User creation returned an error."); } else { $this->log->info("RADIUS user {$mac} created successfully."); } } // Step 4: Login to RADIUS server for update (in case we skipped creation) $cookieFile = tempnam(sys_get_temp_dir(), 'radius_cookie_'); $loginResult = $this->radiusHttpRequest( $radiusUrl . '/login.php', [ 'username' => $radiusUsername, 'password' => $radiusPassword, 'submit' => 'Login' ], $cookieFile ); if (strpos($loginResult, 'Login') === false && strpos($loginResult, 'logout') === false) { throw new Exception("Failed to login to RADIUS server for update."); } // Step 5: Update user details $updateData = [ 'userid' => '', 'user' => $mac, 'Cleartext-Password' => '', 'Custnum' => (strpos($customerNumber, '7000') === 0) ? $customerNumber : '', 'Custnume' => (strpos($customerNumber, '7000') === 0) ? '' : $customerNumber, 'Hotspot_Info' => '', 'ont_sn' => $ontSn, 'Wifikey' => $wifiKey, 'Mikrotik-Group' => '', 'Framed-Pool' => '', 'Pool-Name' => '', 'Framed-IP-Address' => '', 'Framed-IP-Netmask' => '', 'Framed-Route' => '', 'MS-Primary-DNS-Server' => '195.191.252.62', 'MS-Secondary-DNS-Server' => '193.105.204.194', 'DHCP-IP-Address-Lease-Time' => '', 'MaxLogins' => '', 'Valid-From' => '', 'Valid-To' => '', 'Hotspot_Duration' => '', 'Hotspot_Duration_Multiplicant' => '86400', 'Rate-Limit-Down' => '', 'Rate-Limit-Up' => '', 'ContractDown' => '', 'ContractUp' => '', 'Rate-Limit-Down-Burst' => '', 'Rate-Limit-Up-Burst' => '', 'Rate-Limit-Burst-Sec' => '', 'Rate-Limit-Down-Thresh' => '', 'Rate-Limit-Up-Thresh' => '', 'Session-Timeout' => '', 'timeout_max' => '', 'Mikrotik-Recv-Limit' => '', 'transfer_max' => '', 'cisco-avpair[vrf]' => '', 'cisco-avpair[interface]' => '', 'submit' => 'Update' ]; $updateResult = $this->radiusHttpRequest( $radiusUrl . '/edit_user.php', $updateData, $cookieFile ); // Clean up cookie file @unlink($cookieFile); // Check if update was successful if (strpos($updateResult, 'error') !== false || strpos($updateResult, 'Error') !== false) { throw new Exception("Failed to update RADIUS user details."); } $this->log->info("Successfully created/updated RADIUS user for MAC: {$mac}, Customer: {$customerNumber}"); self::returnJson([ 'success' => true, 'message' => 'RADIUS user created/updated successfully.', 'data' => [ 'mac' => $mac, 'customer_number' => $customerNumber, 'ont_sn' => $ontSn, 'wifi_key' => $wifiKey ] ]); } catch (Exception $e) { // Clean up cookie file on error if (isset($cookieFile) && file_exists($cookieFile)) { @unlink($cookieFile); } $this->log->error("Failed to create RADIUS user: " . $e->getMessage()); http_response_code(400); self::returnJson(['success' => false, 'message' => $e->getMessage()]); } } /** * Check if a RADIUS user exists by username (MAC address) * * @param string $username The username/MAC address to check * @return bool True if user exists, false otherwise */ private function checkRadiusUserExists($username) { try { if (!defined('TT_RADIUS_API_URL')) { // Fallback to default if not configured $apiUrl = 'http://radius.xinon.at/api.php'; } else { $apiUrl = TT_RADIUS_API_URL; } // Query the RADIUS API to check if user exists $queryParams = http_build_query(['username' => $username]); $url = $apiUrl . '?' . $queryParams; $opts = [ "http" => [ "method" => "GET", "header" => "Authorization: Basic " . base64_encode(TT_RADIUS_USERNAME . ":" . TT_RADIUS_PASSWORD), "timeout" => 10 ] ]; $context = stream_context_create($opts); $response = @file_get_contents($url, false, $context); if ($response === false) { $this->log->warning("Could not check if RADIUS user exists: API request failed"); return false; } $data = json_decode($response, true); // If we get results back, the user exists if (is_array($data) && count($data) > 0) { return true; } return false; } catch (Exception $e) { $this->log->error("Error checking RADIUS user existence: " . $e->getMessage()); return false; } } private function radiusHttpRequest($url, $postData, $cookieFile) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData)); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieFile); curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieFile); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $error = curl_error($ch); curl_close($ch); throw new Exception("HTTP request failed: " . $error); } curl_close($ch); if ($httpCode !== 200) { throw new Exception("HTTP request returned status code: " . $httpCode); } return $result; } private function fixCpeData($data) { if (!$data) return []; $data->shipping = (bool)$data->shipping; $data->routerconfig_finished = (bool)$data->routerconfig_finished; return $data; } protected function printPDFAction() { $order_id = $this->request->order_id; $order = OrderModel::getOne($order_id); if (!$order) self::sendError("Order not found", 404); $pdf_vars = [ 'firstline' => $order->owner->getCompanyOrName(), 'secondline' => $order->owner->street, 'thirdline' => $order->owner->zip . " " . $order->owner->city, 'fourthline' => $order->owner->customer_number ?? $order->partner_number ]; $pdf = new PdfForm("Cpeprovisioning/PDF_MAIN", $pdf_vars); $wkhtmltopdfArgs = "--page-height 32.5mm --page-width 57.5mm --margin-top 1mm --margin-bottom 0 --margin-left 0 --margin-right 0 --disable-smart-shrinking --encoding utf-8"; $filename = $pdf->render($wkhtmltopdfArgs); header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="' . $filename . '"'); readfile($filename); die(); } protected function countOpenProvisioningsAction() { $r = $this->request; $filter = $this->getPreparedFilter($r->filter ?? []); $filter['routerconfig_finished'] = 0; // only open ones $orders = OrderModel::search($filter); $orderproductsprefetch = OrderProductModel::precache(); $openCount = 0; foreach ($orders as $order) { if ($filter["hide_delayed_finish"] && $order->finish_after) { if ($order->finish_after > date("U") + (31 * 86400)) { continue; } } if (isset($orderproductsprefetch['terminations'][$order->id]) && is_array($orderproductsprefetch['terminations'][$order->id]) && count($orderproductsprefetch['terminations'][$order->id])) { if (!$order->cpeprovisioning_enabled && $orderproductsprefetch['terminations'][$order->id][0]['statuscode'] < TT_TERMSTATUS_CONNECTED) { continue; } } if (array_key_exists($order->id, $orderproductsprefetch)) { foreach ($orderproductsprefetch[$order->id] as $orderproduct) { if (!$orderproduct || !is_array($orderproduct)) continue; if ($orderproduct['routerconfig_finished'] == 1) { continue; } $productattributes = $orderproduct['attributes']; if (is_array($productattributes) && count($productattributes)) { if (array_key_exists("bras_type", $productattributes) && $productattributes) { $openCount++; } } } } } self::returnJson(['open_count' => $openCount]); } }