Explorar o código

rewrite web for PDO support

Dmitriev Roman hai 3 meses
pai
achega
ea48090274

+ 4 - 4
html/admin/customers/building.php

@@ -6,8 +6,8 @@ if (isset($_POST["remove"])) {
     $fid = $_POST["f_id"];
     foreach ($fid as $key => $val) {
         if (isset($val) and $val > 1) {
-            LOG_INFO($db_link,'Remove building id: '. $val .' '. dump_record($db_link,'building','id='.$val));
-            delete_record($db_link, "building", "id=" . $val);
+            LOG_INFO($db_link,'Remove building id: '. $val .' '. dump_record($db_link,'building','id=?', [$val]));
+            delete_record($db_link, "building", "id=?", [$val]);
         }
     }
     header("Location: " . $_SERVER["REQUEST_URI"]);
@@ -32,7 +32,7 @@ if (isset($_POST['save'])) {
                 $new['name'] = $value;
                 $new['description'] = $value_description;
                 LOG_INFO($db_link,"Change building id='{$save_id}': name=".$value." description=".$value_description);
-                update_record($db_link, "building", "id='{$save_id}'", $new);
+                update_record($db_link, "building", "id=?", $new, [$save_id]);
             }
         }
     }
@@ -68,7 +68,7 @@ print_control_submenu($page_url);
 </td>
 </tr>
 <?php
-$t_building = get_records($db_link,'building','TRUE ORDER BY id');
+$t_building = get_records_sql($db_link,'SELECT * FROM building ORDER BY name');
 foreach ($t_building as $row) {
     print "<tr align=center>\n";
     print "<td class=\"data\" style='padding:0'><input type=checkbox name=f_id[] value='{$row['id']}'></td>\n";

+ 5 - 5
html/admin/customers/control-options.php

@@ -7,9 +7,9 @@ if (isset($_POST["remove"])) {
     if (!empty($_POST["f_id"])) {
         $fid = $_POST["f_id"];
         foreach ($fid as $option_id => $config_id) {
-            $opt_def = get_record($db_link, "config_options", "id=" . $option_id);
+            $opt_def = get_record($db_link, "config_options", "id=?" ,[ $option_id ]);
             LOG_INFO($db_link, WEB_config_remove_option . " id: " . $config_id . " name: " . $opt_def["option_name"]);
-            delete_record($db_link, "config", "id=" . $config_id);
+            delete_record($db_link, "config", "id=?" ,[ $config_id ]);
         }
     }
     header("Location: " . $_SERVER["REQUEST_URI"]);
@@ -21,7 +21,7 @@ if (isset($_POST['save'])) {
         $fid = $_POST["f_id"];
         foreach ($fid as $option_id => $config_id) {
             $value = $_POST['f_config_value'][$config_id];
-            $option = get_record_sql($db_link, "SELECT * FROM config_options WHERE id=" . $option_id);
+            $option = get_record_sql($db_link, "SELECT * FROM config_options WHERE id=?",[ $option_id ]);
             if (isset($value)) {
                 $new['value'] = $value;
             }
@@ -33,7 +33,7 @@ if (isset($_POST['save'])) {
                 LOG_INFO($db_link, WEB_config_set_option . " id: " . $config_id . " name: " . $option["option_name"] . " = " . $value);
             }
             if (!empty($new)) {
-                update_record($db_link, "config", "id=" . $config_id, $new);
+                update_record($db_link, "config", "id=?" , $new, [$config_id ]);
             }
         }
     }
@@ -46,7 +46,7 @@ if (isset($_POST["create"])) {
     if (isset($new_option)) {
         $new['option_id'] = $new_option;
         $new['value'] = get_option($db_link, $new_option);
-        $opt_def = get_record($db_link, "config_options", "id=$new_option");
+        $opt_def = get_record($db_link, "config_options", "id=?", [$new_option]);
         LOG_INFO($db_link, WEB_config_add_option . " id: " . $new_option . " name: " . $opt_def["option_name"] . " = " . $new['value']);
         insert_record($db_link, "config", $new);
     }

+ 7 - 6
html/admin/customers/control-subnets-usage.php

@@ -6,7 +6,8 @@ unset($_POST);
 require_once ($_SERVER['DOCUMENT_ROOT']."/inc/header.php");
 print_control_submenu($page_url);
 $zombi_days = get_option($db_link, 35);
-
+// Создаём временную метку: сейчас минус $zombi_days дней
+$zombi_threshold = date('Y-m-d H:i:s', strtotime("-$zombi_days days"));
 ?>
 <div id="cont">
 <br>
@@ -25,7 +26,7 @@ $zombi_days = get_option($db_link, 35);
 	<td><b><?php echo WEB_network_zombi_dhcp; ?><br><?php print "(> ".$zombi_days." ".WEB_days.")"; ?></b></td>
 </tr>
 <?php
-$t_subnets = get_records($db_link,'subnets','office=1 ORDER BY ip_int_start');
+$t_subnets = get_records_SQL($db_link,'SELECT * FROM subnets WHERE office=1 ORDER BY ip_int_start');
 if (!empty($t_subnets)) {
 foreach ( $t_subnets as $row ) {
     print "<tr align=center>\n";
@@ -34,22 +35,22 @@ foreach ( $t_subnets as $row ) {
     $all_ips = $row['ip_int_stop']-$row['ip_int_start']-3;
     print "<td class=\"$cl\">".$all_ips."</td>\n";
 #used
-    $used_all = get_count_records($db_link,'user_auth','deleted=0 and ip_int>='.$row['ip_int_start'].' and ip_int<='.$row['ip_int_stop']);
+    $used_all = get_count_records($db_link,'user_auth','deleted = 0 AND ip_int >= ? AND ip_int <= ?', [ $row['ip_int_start'], $row['ip_int_stop']]);
     print "<td class=\"$cl\">".$used_all."</td>\n";
     $free_all = $all_ips - $used_all;
     print "<td class=\"$cl\">".$free_all."</td>\n";
     $dhcp_pool = $row['dhcp_stop']-$row['dhcp_start']+1;
     print "<td class=\"$cl\">".$dhcp_pool."</td>\n";
 #used pool
-    $used_dhcp = get_count_records($db_link,'user_auth','deleted=0 and ip_int>='.$row['dhcp_start'].' and ip_int<='.$row['dhcp_stop']);
+    $used_dhcp = get_count_records($db_link,'user_auth','deleted = 0 AND ip_int >= ? AND ip_int <= ?', [$row['dhcp_start'], $row['dhcp_stop']]);
     print "<td class=\"$cl\">".$used_dhcp."</td>\n";
     $free_dhcp = $dhcp_pool - $used_dhcp;
     print "<td class=\"$cl\">".$free_dhcp."</td>\n";
     $free_static = $free_all -  $free_dhcp;
     print "<td class=\"$cl\">".$free_static."</td>\n";
-    $zombi = get_count_records($db_link,'user_auth','deleted=0 and ip_int>='.$row['ip_int_start'].' and ip_int<='.$row['ip_int_stop'].' and last_found<=(NOW() - INTERVAL '.$zombi_days.' DAY)');
+    $zombi = get_count_records( $db_link, 'user_auth', 'deleted = 0 AND ip_int >= ? AND ip_int <= ? AND last_found <= ?', [ $row['ip_int_start'], $row['ip_int_stop'], $zombi_threshold]);
     print "<td class=\"$cl\">".$zombi."</td>\n";
-    $zombi = get_count_records($db_link,'user_auth','deleted=0 and ip_int>='.$row['dhcp_start'].' and ip_int<='.$row['dhcp_stop'].' and last_found<=(NOW() - INTERVAL '.$zombi_days.' DAY)');
+    $zombi = get_count_records( $db_link, 'user_auth', 'deleted = 0 AND ip_int >= ? AND ip_int <= ? AND last_found <= ?', [ $row['dhcp_start'], $row['dhcp_stop'], $zombi_threshold]);
     print "<td class=\"$cl\">".$zombi."</td>\n";
     print "</tr>\n";
     }

+ 0 - 12
html/admin/customers/control.php

@@ -87,18 +87,6 @@ if (!empty($_POST["not_save_traf_all"]) and $_POST["not_save_traf_all"]) {
     exit;
 }
 
-if (isset($_POST["s_remove"])) {
-    $s_id = $_POST["s_id"];
-    foreach ($s_id as $key => $val) {
-        if (isset($val)) {
-            LOG_INFO($db_link, "Remove subnet id: $val ". dump_record($db_link,'subnets','id='.$val));
-            delete_record($db_link, "subnets", "id=" . $val);
-        }
-    }
-    header("Location: " . $_SERVER["REQUEST_URI"]);
-    exit;
-}
-
 if (isset($_POST["clean_cache"])) {
     LOG_VERBOSE($db_link, "Clean dns cache");
     run_sql($db_link,"DELETE FROM dns_cache");

+ 17 - 8
html/admin/customers/devmodels.php

@@ -44,7 +44,7 @@ if (isset($_POST['save'])) {
             if (!isset($new['poe_in']) OR empty($new['poe_in'])) { $new['poe_in'] = 0; }
             if (!isset($new['poe_out']) OR empty($new['poe_out'])) { $new['poe_out'] = 0; }
             $new['nagios_template'] = $_POST['f_nagios'][$j];
-            update_record($db_link, "device_models", "id='{$save_id}'", $new);
+            update_record($db_link, "device_models", "id = ?", $new, [$save_id]);
             }
         }
     header("Location: " . $_SERVER["REQUEST_URI"]);
@@ -78,8 +78,8 @@ if (isset($_POST['remove'])) {
             for ($j = 0; $j < $len_all; $j ++) {
                 if (intval($_POST['r_id'][$j]) != $save_id) { continue; }
                 if ($save_id>=10000) { 
-                    delete_record($db_link, "device_models", "id='{$save_id}'");
-                    run_sql($db_link,"UPDATE devices set device_model_id=NULL WHERE device_model_id=".$save_id);
+                    delete_record($db_link, "device_models", "id= ?", [$save_id]);
+                    update_records($db_link, 'devices', 'device_model_id = ?', ['device_model_id' => null], [$save_id]);
                     }
                 }
             }
@@ -121,16 +121,26 @@ print_control_submenu($page_url);
 </table>
 
 <?php
-$v_filter='';
-if (!empty($f_vendor_select)) { $v_filter = "WHERE vendor_id=".$f_vendor_select; }
+$params = [];
+$filter = '';
+
+if (!empty($f_vendor_select)) {
+    $filter = 'WHERE vendor_id = ?';
+    $params[] = $f_vendor_select;
+}
+
+$countSQL = "SELECT COUNT(*) FROM device_models $filter";
+$count_records = get_single_field($db_link, $countSQL, $params);
 
-$countSQL="SELECT Count(*) FROM device_models $v_filter";
-$count_records = get_single_field($db_link,$countSQL);
 $total=ceil($count_records/$displayed);
 if ($page>$total) { $page=$total; }
 if ($page<1) { $page=1; }
 $start = ($page * $displayed) - $displayed;
 print_navigation($page_url,$page,$displayed,$count_records,$total);
+$sql = "SELECT * FROM device_models $filter ORDER BY vendor_id, model_name LIMIT ? OFFSET ?";
+$params[] = $displayed;
+$params[] = $start;
+$t_ou = get_records_sql($db_link, $sql, $params);
 ?>
 <br>
 <table class="data">
@@ -146,7 +156,6 @@ print_navigation($page_url,$page,$displayed,$count_records,$total);
 <td><input id='remove' type="submit" name='remove' value="<?php echo WEB_btn_delete; ?>"></td>
 </tr>
 <?php
-$t_ou = get_records_sql($db_link,'SELECT * FROM device_models '.$v_filter." ORDER BY vendor_id, model_name LIMIT $displayed OFFSET $start");
 foreach ($t_ou as $row) {
     print "<tr align=center>\n";
     print "<td class=\"data\" style='padding:0'><input type=checkbox name=f_id[] value='{$row['id']}'></td>\n";

+ 4 - 2
html/admin/customers/devvendors.php

@@ -34,7 +34,7 @@ if (isset($_POST['save'])) {
         for ($j = 0; $j < $len_all; $j ++) {
             if (intval($_POST['r_id'][$j]) != $save_id) { continue; }
             $new['name'] = $_POST['f_name'][$j];
-            update_record($db_link, "vendors", "id='{$save_id}'", $new);
+            update_record($db_link, "vendors", "id= ?", $new, [$save_id]);
             }
         }
     header("Location: " . $_SERVER["REQUEST_URI"]);
@@ -91,7 +91,9 @@ print_navigation($page_url,$page,$displayed,$count_records,$total);
 <td><input type="submit" name='save' value="<?php echo WEB_btn_save; ?>"></td>
 </tr>
 <?php
-$t_ou = get_records_sql($db_link,"SELECT * FROM vendors ORDER BY name LIMIT $displayed OFFSET $start");
+$params[]=$displayed;
+$params[]=$start;
+$t_ou = get_records_sql($db_link,"SELECT * FROM vendors ORDER BY name LIMIT ? OFFSET ?", $params);
 foreach ($t_ou as $row) {
     print "<tr align=center>\n";
     print "<td class=\"data\" style='padding:0'><input type=checkbox name=f_id[] value='{$row['id']}'></td>\n";

+ 2 - 2
html/admin/customers/editcustom.php

@@ -16,7 +16,7 @@ if (isset($_POST["edituser"])) {
         $new['api_key'] = $_POST["api_key"];
 	}
     $new['rights'] = $_POST["f_acl"] * 1;
-    update_record($db_link, "customers", "id='$id'", $new);
+    update_record($db_link, "customers", "id= ?", $new, [ $id ]);
     unset($_POST["pass"]);
     header("Location: " . $_SERVER["REQUEST_URI"]);
     exit;
@@ -27,7 +27,7 @@ unset($_POST);
 print_control_submenu($page_url);
 
 require_once ($_SERVER['DOCUMENT_ROOT']."/inc/header.php");
-$customer=get_record($db_link,'customers',"id=".$id);
+$customer=get_record($db_link,'customers',"id=?", [$id]);
 ?>
 
 <div id="cont">

+ 3 - 3
html/admin/customers/editsubnet.php

@@ -90,7 +90,7 @@ if (isset($_POST['s_save'])) {
             $new['dhcp_stop'] = 0;
         }
 
-        update_record($db_link, "subnets", "id='$id'", $new);
+        update_record($db_link, "subnets", "id= ?", $new, [ $id ]);
         header("Location: /admin/customers/index-subnets.php");
         exit;
 }
@@ -99,8 +99,8 @@ unset($_POST);
 require_once($_SERVER['DOCUMENT_ROOT'] . "/inc/header.php");
 print_control_submenu($page_url);
 
-$sSQL = "SELECT * FROM subnets WHERE id=$id";
-$subnet_info = get_record_sql($db_link, $sSQL);
+$sSQL = "SELECT * FROM subnets WHERE id= ?";
+$subnet_info = get_record_sql($db_link, $sSQL, [ $id ]);
 ?>
 
 <div id="cont">

+ 4 - 4
html/admin/customers/index-subnets.php

@@ -8,9 +8,9 @@ if (isset($_POST["s_remove"])) {
         $s_id = $_POST["s_id"];
         foreach ($s_id as $key => $net_id) {
             if (isset($net_id)) {
-                LOG_INFO($db_link, "Remove subnet id: $net_id ". dump_record($db_link,'subnets','id='.$val));
-                delete_record($db_link, "subnets", "id=" . $net_id);
-                delete_record($db_link, "gateway_subnets", "subnet_id=" . $net_id);
+                LOG_INFO($db_link, "Remove subnet id: $net_id ". dump_record($db_link,'subnets','id=?', [$val]));
+                delete_record($db_link, "subnets", "id= ?", [ $net_id ]);
+                delete_record($db_link, "gateway_subnets", "subnet_id= ?" , [ $net_id ]);
             }
         }
     }
@@ -78,7 +78,7 @@ print_control_submenu($page_url);
                 <td><b><?php echo WEB_cell_description; ?></b></td>
             </tr>
             <?php
-            $t_subnets = get_records($db_link, 'subnets', 'True ORDER BY ip_int_start');
+            $t_subnets = get_records_sql($db_link, 'SELECT * FROM subnets ORDER BY ip_int_start');
             foreach ($t_subnets as $row) {
                 print "<tr align=center>\n";
                 print "<td class=\"data\" style='padding:0'><input type=checkbox name=s_id[] value='" . $row['id'] . "'></td>\n";

+ 5 - 5
html/admin/customers/index.php

@@ -7,7 +7,7 @@ $msg_error = "";
 if (isset($_POST["create"])) {
     $login = $_POST["newlogin"];
     if ($login) {
-	$customer = get_record_sql($db_link,"Select * from customers WHERE LCase(login)=LCase('$login')");
+        $customer = get_record_sql($db_link, "SELECT * FROM customers WHERE LOWER(login) = LOWER(?)", [$login]);
         if (!empty($customer)) {
             $msg_error = "Login $login already exists!";
             LOG_ERROR($db_link, $msg_error);
@@ -29,8 +29,8 @@ if (isset($_POST["remove"])) {
     $fid = $_POST["fid"];
     foreach ($fid as $key => $val) {
         if ($val) {
-            LOG_INFO($db_link, "Remove login with id: $val ". dump_record($db_link,'customers','id='.$val));
-            delete_record($db_link, "customers", "id=" . $val);
+            LOG_INFO($db_link, "Remove login with id: $val ". dump_record($db_link,'customers','id=?', [ $val]));
+            delete_record($db_link, "customers", "id=?", [ $val ]);
         }
     }
     header("Location: " . $_SERVER["REQUEST_URI"]);
@@ -54,10 +54,10 @@ print_control_submenu($page_url);
 <td><b><?php echo WEB_customer_mode;?></b></td>
 </tr>
 <?php
-$users = get_records($db_link,'customers','True ORDER BY login');
+$users = get_records_sql($db_link,'SELECT * FROM customers ORDER BY login');
 foreach ($users as $row) {
     $cl = "data";
-    $acl = get_record_sql($db_link,'SELECT * FROM acl WHERE id='.$row['rights']);
+    $acl = get_record_sql($db_link,'SELECT * FROM acl WHERE id=?', [ $row['rights'] ]);
     print "<tr align=center>\n";
     print "<td class=\"$cl\" style='padding:0'><input type=checkbox name=fid[] value=".$row['id']."></td>\n";
     print "<td class=\"$cl\" align=left width=200><a href=editcustom.php?id=".$row['id'].">" . $row['login'] . "</a></td>\n";

+ 1 - 1
html/admin/customers/ipcam.php

@@ -41,7 +41,7 @@ print_ou_select($db_link, 'f_ou_id', $f_ou_id);
 print "</td>\n";
 print "</tr>\n";
 print "<tr><td colspan=3><br></td></tr>\n";
-$t_config = get_records_sql($db_link, "select id,name from building ORDER BY name");
+$t_config = get_records_sql($db_link, "SELECT * FROM building ORDER BY name");
 foreach ($t_config as $row) {
     print "<tr align=center>\n";
     print "<td class=\"$cl\" style='padding:0'><input type=checkbox name=fid[] value=".$row['id']."></td>\n";

+ 1 - 1
html/admin/users/editauth.php

@@ -183,7 +183,7 @@ if (isset($_POST["moveauth"]) and !$old_auth_info['deleted']) {
     $moved_auth = get_record_sql($db_link,"SELECT description FROM user_auth WHERE id=".$id);
     $changes = apply_auth_rule($db_link, $moved_auth, $new_parent_id);
     update_record($db_link, "user_auth", "id='$id'", $changes);
-    LOG_WARNING($db_link, "IP-address moved to another user! Applyed: " . get_rec_str($changes), $id);
+    LOG_WARNING($db_link, "IP-address moved to another user! Applyed: " . hash_to_text($changes), $id);
     run_sql($db_link,"DELETE FROM auth_rules WHERE user_id=".$old_auth_info["user_id"]." AND rule='".$old_auth_info["mac"]."' AND rule_type=2");
     run_sql($db_link,"DELETE FROM auth_rules WHERE user_id=".$old_auth_info["user_id"]." AND rule='".$old_auth_info["ip"]."' AND rule_type=1");
     LOG_INFO($db_link,"Autorules removed for user_id: ".$old_auth_info["user_id"]." login: ".$user_info["login"]." by mac and ip");

+ 1 - 1
html/admin/users/edituser.php

@@ -216,7 +216,7 @@ if (isset($_POST["new_user"])) {
             if (empty($login)) {
                 $login = $auth_info["ip"];
             }
-            $new_user = get_record_sql($db_link, "SELECT * FROM user_list WHERE LCase(login)=LCase('$login') and deleted=0");
+            $new_user = get_record_sql($db_link, "SELECT * FROM user_list WHERE LOWER(login)=LOWER('$login') and deleted=0");
             if (!empty($new_user)) {
                 // move auth
                 $auth["user_id"] = $new_user["id"];

+ 1 - 1
html/admin/users/index.php

@@ -12,7 +12,7 @@ $msg_error = "";
 if (isset($_POST["create"])) {
     $login = trim($_POST["newlogin"]);
     if (!empty($login)) {
-        $lcount = get_count_records($db_link,"user_list","LCase(login)=LCase('$login')");
+        $lcount = get_count_records($db_link,"user_list","LOWER(login)=LOWER('$login')");
         if ($lcount > 0) {
             $msg_error = WEB_cell_login." ".$login." ".$msg_exists."!";
             unset($_POST);

+ 1 - 1
html/inc/auth.utils.php

@@ -55,7 +55,7 @@ ini_set('session.gc_maxlifetime', SESSION_LIFETIME);
 //ini_set('session.use_only_cookies', false);
 
 
-// Функция для логирования отладки сессий
+// Функция для логирования отладки сессий, нужна только для отладки
 function log_session_debug($db, $message, $data = null) {
     $log_message = "SESSION_DEBUG: " . $message;
     if ($data !== null) {

+ 42 - 11
html/inc/common.php

@@ -89,8 +89,13 @@ FILTER_FLAG_ENCODE_HIGH      // Кодирует символы с ASCII > 127
 FILTER_FLAG_ENCODE_AMP       // Кодирует амперсанд (&)
 */
 
-function getParam($name, $page_url, $default = null, $filter = FILTER_DEFAULT) {
-    $value = filter_input(INPUT_GET, $name, $filter) ?? filter_input(INPUT_POST, $name, $filter);
+function getParam($name, $page_url, $default = null, $filter = FILTER_DEFAULT, $options = []) {
+    $value = filter_input(INPUT_POST, $name, $filter, $options) ?? filter_input(INPUT_GET, $name, $filter, $options);
+    return $value !== null ? $value : ($_SESSION[$page_url][$name] ?? $default);
+}
+
+function getPOST($name, $page_url, $default = null, $filter = FILTER_DEFAULT, $options = []) {
+    $value = filter_input(INPUT_POST, $name, $filter, $options);
     return $value !== null ? $value : ($_SESSION[$page_url][$name] ?? $default);
 }
 
@@ -2727,18 +2732,18 @@ function email($level = L_WARNING, $msg = '') {
 
 function write_log($db, $msg, $level = L_INFO, $auth_id = 0)
 {
+    if (!isset($msg)) { return; }
+
     // Безопасное получение данных сессии
     $currentIp = filter_var($_SESSION['ip'] ?? '127.0.0.1', FILTER_VALIDATE_IP) ?: '127.0.0.1';
     $currentLogin = htmlspecialchars($_SESSION['login'] ?? 'http', ENT_QUOTES, 'UTF-8');
-    
-    if (!isset($msg)) { return; }
-    
+
     // Для уровня L_DEBUG пишем в error_log
     if ($level === L_DEBUG) {
         error_log("DEBUG: " . $msg);
         return;
     }
-    
+
     try {
         // Используем подготовленный запрос PDO напрямую
         $stmt = $db->prepare("INSERT INTO worklog(customer, message, level, auth_id, ip) 
@@ -3343,12 +3348,23 @@ function print_navigation($url, $page, $displayed, $count_records, $total)
 
 function get_option($db, $option_id)
 {
-    $option = get_record($db, "config", "option_id=" . $option_id);
-    if (empty($option) or empty($option['value'])) {
-        $default = get_record($db, "config_options", "id=$option_id");
-        return $default['default_value'];
+    // Валидация входного параметра
+    if (!is_numeric($option_id) || $option_id <= 0) {
+        return null;
     }
-    return $option['value'];
+    $sql = "
+        SELECT
+            COALESCE(c.value, co.default_value) AS value,
+            co.option_type
+        FROM config_options co
+        LEFT JOIN config c ON c.option_id = co.id
+        WHERE co.id = ?
+    ";
+    $record = get_record_sql($db, $sql, [$option_id]);
+    if ($record && isset($record['value'])) {
+        return $record['value'];
+    }
+    return null;
 }
 
 function is_option($db, $option_id)
@@ -3750,6 +3766,21 @@ function arrayToNotifyFlags(array $selectedValues): int {
     return $flags;
 }
 
+/**
+ * Проверяет, является ли OU системным (используется по умолчанию для пользователей или хотспотов)
+ *
+ * @param PDO $db
+ * @param int $ou_id
+ * @return bool
+ */
+function is_system_ou($db, $ou_id = null) {
+    if (empty($ou_id) || !is_numeric($ou_id) || $ou_id <= 0) {
+        return false;
+    }
+    $sql = "SELECT 1 FROM ou WHERE id = ? AND (default_users = 1 OR default_hotspot = 1)";
+    return !empty(get_record_sql($db, $sql, [$ou_id]));
+}
+
 $config["org_name"] = get_option($db_link, 32);
 
 $config["version"] = get_eye_version($db_link);

+ 221 - 245
html/inc/sql.php

@@ -422,98 +422,125 @@ function normalize_records($records) {
     return $normalized;
 }
 
-function run_sql($db, $query)
+/**
+ * Выполняет SQL-запрос с поддержкой параметров.
+ * 
+ * @param PDO $db
+ * @param string $query
+ * @param array $params (опционально)
+ * @return mixed
+ */
+function run_sql($db, $query, $params = [])
 {
-    // Проверка прав доступа для UPDATE, DELETE, INSERT
-    if (preg_match('/^\s*(UPDATE|DELETE|INSERT)/i', $query)) {
-        $table_name = null;
-        // Определяем имя таблицы для проверки прав
-        if (preg_match('/^\s*UPDATE\s+(\w+)/i', $query, $matches)) {
-            $table_name = $matches[1];
-            $operation = 'update';
-        } elseif (preg_match('/^\s*DELETE\s+FROM\s+(\w+)/i', $query, $matches)) {
-            $table_name = $matches[1];
-            $operation = 'del';
-        } elseif (preg_match('/^\s*INSERT\s+INTO\s+(\w+)/i', $query, $matches)) {
-            $table_name = $matches[1];
-            $operation = 'add';
-        }
-        // Проверяем права доступа
-        if ($table_name && !allow_update($table_name, $operation)) {
-            LOG_DEBUG($db, "Access denied: $query");
-            return false;
-        }
+    // Определяем тип запроса и таблицу для проверки прав
+    $table_name = null;
+    $operation = null;
+
+    if (preg_match('/^\s*UPDATE\s+([a-zA-Z_][a-zA-Z0-9_]*)/i', $query, $matches)) {
+        $table_name = $matches[1];
+        $operation = 'update';
+    } elseif (preg_match('/^\s*DELETE\s+FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)/i', $query, $matches)) {
+        $table_name = $matches[1];
+        $operation = 'del';
+    } elseif (preg_match('/^\s*INSERT\s+INTO\s+([a-zA-Z_][a-zA-Z0-9_]*)/i', $query, $matches)) {
+        $table_name = $matches[1];
+        $operation = 'add';
+    }
+
+    // Проверка прав доступа
+    if ($table_name && $operation && !allow_update($table_name, $operation)) {
+        LOG_DEBUG($db, "Access denied: $query");
+        return false;
     }
-    
-    // Выполняем запрос
+
     try {
-        $stmt = $db->query($query);
-        
+        $stmt = $db->prepare($query);
+        $success = $stmt->execute($params);
+
+        if (!$success) {
+            LOG_ERROR($db, "Query execution failed: $query | params: " . json_encode($params));
+            return false;
+        }
+
         // Возвращаем результат в зависимости от типа запроса
         if (preg_match('/^\s*SELECT/i', $query)) {
-            // Для SELECT возвращаем PDOStatement
-            return $stmt;
+            return $stmt; // PDOStatement для последующего fetch
         } elseif (preg_match('/^\s*INSERT/i', $query)) {
-            // Для INSERT возвращаем ID вставленной записи
             return $db->lastInsertId();
         } elseif (preg_match('/^\s*(UPDATE|DELETE)/i', $query)) {
-            // Для UPDATE/DELETE возвращаем количество затронутых строк
             return $stmt->rowCount();
         }
-        // Для других типов запросов возвращаем результат как есть
+
         return $stmt;
-        
+
     } catch (PDOException $e) {
-        LOG_ERROR($db, "At simple SQL: $query :" . $e->getMessage());
+        LOG_ERROR($db, "SQL error: $query | params: " . json_encode($params) . " | " . $e->getMessage());
         return false;
     }
 }
 
-function get_count_records($db, $table, $filter)
+function get_count_records($db, $table, $filter, $filter_params = [])
 {
+    // Валидация имени таблицы (защита от SQL-инъекций)
+    if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $table)) {
+        return 0;
+    }
+
+    $sql = "SELECT COUNT(*) AS cnt FROM $table";
     if (!empty($filter)) {
-        $filter = 'where ' . $filter;
+        $sql .= " WHERE $filter";
     }
-    $t_count = get_record_sql($db, "SELECT count(*) as cnt FROM $table $filter");
-    if (!empty($t_count) and isset($t_count['cnt'])) { return $t_count['cnt']; }
-    return 0;
+
+    $result = get_record_sql($db, $sql, $filter_params);
+    return !empty($result['cnt']) ? (int)$result['cnt'] : 0;
 }
 
 /**
  * Получить одну запись из таблицы по фильтру
  */
-function get_record($db, $table, $filter) {
-    if (!isset($table) || !isset($filter)) {
+function get_record($db, $table, $filter, $filter_params = [])
+{
+    if (empty($table) || empty($filter)) {
         return null;
     }
-    
-    if (preg_match('/=$/', $filter)) {
-        LOG_ERROR($db, "Search record ($table) with illegal filter $filter! Skip command.");
+    // Валидация имени таблицы
+    if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $table)) {
+        LOG_ERROR($db, "Invalid table name: $table");
+        return null;
+    }
+    if (preg_match('/=$/', trim($filter))) {
+        LOG_ERROR($db, "Search record ($table) with illegal filter '$filter'! Skip command.");
         return null;
     }
-    
     $sql = "SELECT * FROM $table WHERE $filter";
-    return get_record_sql($db, $sql);
+    return get_record_sql($db, $sql, $filter_params);
 }
 
 /**
  * Получить несколько записей из таблицы по фильтру
  */
-function get_records($db, $table, $filter = '') {
-    if (!isset($table)) {
+function get_records($db, $table, $filter = '', $filter_params = [])
+{
+    if (empty($table)) {
         return [];
     }
-    
+
+    // Валидация имени таблицы
+    if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $table)) {
+        LOG_ERROR($db, "Invalid table name: $table");
+        return [];
+    }
+
     if (!empty($filter)) {
-        if (preg_match('/=$/', $filter)) {
-            LOG_ERROR($db, "Search record ($table) with illegal filter $filter! Skip command.");
+        if (preg_match('/=$/', trim($filter))) {
+            LOG_ERROR($db, "Search records ($table) with illegal filter '$filter'! Skip command.");
             return [];
         }
         $filter = "WHERE $filter";
     }
-    
+
     $sql = "SELECT * FROM $table $filter";
-    return get_records_sql($db, $sql);
+    return get_records_sql($db, $sql, $filter_params);
 }
 
 /**
@@ -524,7 +551,7 @@ function get_records($db, $table, $filter = '') {
  * @param array|null $params
  * @return array|null
  */
-function get_record_sql($db, $sql, $params = null) {
+function get_record_sql($db, $sql, $params = []) {
     if (empty($sql)) {
         return null;
     }
@@ -548,14 +575,21 @@ function get_record_sql($db, $sql, $params = null) {
  * @param array|null $params
  * @return array
  */
-function get_records_sql($db, $sql, $params = null) {
+function get_records_sql($db, $sql, $params = [])
+{
     if (empty($sql)) {
         return [];
     }
 
+    // Приводим $params к массиву
+    $params = $params ?: [];
+
+    // Логируем в DEBUG
+    // LOG_DEBUG($db, "SQL: $sql | params: " . json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
+
     try {
         $stmt = $db->prepare($sql);
-        $stmt->execute($params ?: []);
+        $stmt->execute($params);
         $records = $stmt->fetchAll(PDO::FETCH_ASSOC);
 
         if (!empty($records)) {
@@ -563,8 +597,10 @@ function get_records_sql($db, $sql, $params = null) {
         }
 
         return [];
+
     } catch (PDOException $e) {
-        LOG_ERROR($db, "SQL: $sql : " . $e->getMessage());
+        // Логируем ошибку с параметрами
+        LOG_ERROR($db, "SQL error: $sql | params: " . json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . " | " . $e->getMessage());
         return [];
     }
 }
@@ -572,14 +608,13 @@ function get_records_sql($db, $sql, $params = null) {
 /**
  * Получить одно значение поля по SQL-запросу
  */
-function get_single_field($db, $sql, $params = null) {
+function get_single_field($db, $sql, $params = []) {
     $record = get_record_sql($db, $sql, $params);
-    
+
     if (!empty($record) && is_array($record)) {
-        // Получаем первое значение из записи
         return reset($record) ?: 0;
     }
-    
+
     return 0;
 }
 
@@ -680,45 +715,30 @@ function allow_update($table, $action = 'update', $field = '')
     return 0;
 }
 
-function update_record($db, $table, $filter, $newvalue)
+function update_record($db, $table, $filter, $newvalue, $filter_params = [])
 {
-
-    if (!isset($table)) {
-#        LOG_WARNING($db, "Change record for unknown table! Skip command.");
+    if (!isset($table) || trim($table) === '') {
         return;
     }
-    if (!isset($filter)) {
-#        LOG_WARNING($db, "Change record ($table) with empty filter! Skip command.");
+    if (!isset($filter) || trim($filter) === '') {
         return;
     }
-    if (preg_match('/=$/', $filter)) {
+    if (preg_match('/=$/', trim($filter))) {
         LOG_WARNING($db, "Change record ($table) with illegal filter $filter! Skip command.");
         return;
     }
-    if (!isset($newvalue)) {
-#        LOG_WARNING($db, "Change record ($table [ $filter ]) with empty data! Skip command.");
+    if (!isset($newvalue) || !is_array($newvalue)) {
         return;
     }
 
-
     if (!allow_update($table, 'update')) {
         LOG_INFO($db, "Access denied: $table [ $filter ]");
         return 1;
     }
 
-    $old_sql = "SELECT * FROM $table WHERE $filter";
-    try {
-        $stmt = $db->query($old_sql);
-        $old = $stmt->fetch(PDO::FETCH_ASSOC);
-    } catch (PDOException $e) {
-        LOG_ERROR($db, "SQL: $old_sql :" . $e->getMessage());
-        return;
-    }
-
-    $rec_id = NULL;
-    if (!empty($old['id'])) {
-        $rec_id = $old['id'];
-    }
+    $old_record = get_record_sql($db,"SELECT * FROM $table WHERE $filter",$filter_params);
+    if (empty($old_record)) { return; }
+    $rec_id = $old_record['id'];
 
     $changed_log = '';
     $set_parts = [];
@@ -766,7 +786,7 @@ function update_record($db, $table, $filter, $newvalue)
             $value = '';
         }
         $value = trim($value);
-        if (isset($old[$key]) && strcmp($old[$key], $value) == 0) {
+        if (isset($old_record[$key]) && strcmp($old_record[$key], $value) == 0) {
             continue;
         }
         if ($table === "user_auth") {
@@ -786,27 +806,27 @@ function update_record($db, $table, $filter, $newvalue)
             }
         }
         if (!preg_match('/password/i', $key)) {
-            $changed_log = $changed_log . " $key => $value (old: " . ($old[$key] ?? '') . "),";
+            $changed_log = $changed_log . " $key => $value (old: " . ($old_record[$key] ?? '') . "),";
         }
         $set_parts[] = "$key = ?";
         $params[] = $value;
     }
 
     if ($table === "user_auth" and $dns_changed) {
-        if (!empty($old['dns_name']) and !empty($old['ip']) and !$old['dns_ptr_only'] and !preg_match('/\.$/', $old['dns_name'])) {
+        if (!empty($old_record['dns_name']) and !empty($old_record['ip']) and !$old_record['dns_ptr_only'] and !preg_match('/\.$/', $old_record['dns_name'])) {
             $del_dns['name_type'] = 'A';
-            $del_dns['name'] = $old['dns_name'];
-            $del_dns['value'] = $old['ip'];
+            $del_dns['name'] = $old_record['dns_name'];
+            $del_dns['value'] = $old_record['ip'];
             $del_dns['operation_type'] = 'del';
             if (!empty($rec_id)) {
                 $del_dns['auth_id'] = $rec_id;
             }
             insert_record($db, 'dns_queue', $del_dns);
         }
-        if (!empty($old['dns_name']) and !empty($old['ip']) and $old['dns_ptr_only'] and !preg_match('/\.$/', $old['dns_name'])) {
+        if (!empty($old_record['dns_name']) and !empty($old_record['ip']) and $old_record['dns_ptr_only'] and !preg_match('/\.$/', $old_record['dns_name'])) {
             $del_dns['name_type'] = 'PTR';
-            $del_dns['name'] = $old['dns_name'];
-            $del_dns['value'] = $old['ip'];
+            $del_dns['name'] = $old_record['dns_name'];
+            $del_dns['value'] = $old_record['ip'];
             $del_dns['operation_type'] = 'del';
             if (!empty($rec_id)) {
                 $del_dns['auth_id'] = $rec_id;
@@ -838,12 +858,12 @@ function update_record($db, $table, $filter, $newvalue)
 
     if ($table === "user_auth_alias" and $dns_changed) {
         $auth_id = NULL;
-        if ($old['auth_id']) {
-            $auth_id = $old['auth_id'];
+        if ($old_record['auth_id']) {
+            $auth_id = $old_record['auth_id'];
         }
-        if (!empty($old['alias']) and !preg_match('/\.$/', $old['alias'])) {
+        if (!empty($old_record['alias']) and !preg_match('/\.$/', $old_record['alias'])) {
             $del_dns['name_type'] = 'CNAME';
-            $del_dns['name'] = $old['alias'];
+            $del_dns['name'] = $old_record['alias'];
             $del_dns['operation_type'] = 'del';
             if (!empty($auth_id)) {
                 $del_dns['auth_id'] = $auth_id;
@@ -868,11 +888,11 @@ function update_record($db, $table, $filter, $newvalue)
     }
 
     if ($network_changed) {
-        $set_parts[] = "changed = '1'";
+        $set_parts[] = "changed = 1";
     }
 
     if ($dhcp_changed) {
-        $set_parts[] = "dhcp_changed = '1'";
+        $set_parts[] = "dhcp_changed = 1";
     }
 
     $changed_log = substr_replace($changed_log, "", -1);
@@ -885,35 +905,60 @@ function update_record($db, $table, $filter, $newvalue)
     }
 
     $new_sql = "UPDATE $table SET $run_sql WHERE $filter";
-    LOG_DEBUG($db, "Run sql: $new_sql");
-    
+    $all_params = array_merge($params, $filter_params);
+    LOG_DEBUG($db, "Run sql: $new_sql | params: " . json_encode($all_params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
     try {
         $stmt = $db->prepare($new_sql);
-        $sql_result = $stmt->execute($params);
-        
+        $sql_result = $stmt->execute($all_params);
         if (!$sql_result) {
-            LOG_ERROR($db, "UPDATE Request: $new_sql");
+            LOG_ERROR($db, "UPDATE Request: $new_sql | params: " . json_encode($all_params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
             return;
-        }
+            }
         if ($table !== "sessions") {
             LOG_VERBOSE($db, "Change table $table WHERE $filter set $changed_log");
-        }
+            }
         return $sql_result;
-        
     } catch (PDOException $e) {
-        LOG_ERROR($db, "SQL: $new_sql :" . $e->getMessage());
+        LOG_ERROR($db, "SQL: $new_sql | params: " . json_encode($all_params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . " | error: " . $e->getMessage());
         return;
     }
 }
 
-function delete_record($db, $table, $filter)
+function delete_records($db, $table, $filter, $filter_params = [])
+{
+    // Сначала получаем ID записей, подходящих под фильтр
+    $records = get_records_sql($db, "SELECT id FROM $table WHERE $filter", $filter_params);
+    if (empty($records)) {
+        return true; // ничего не найдено — успех
+    }
+    // Удаляем каждую запись через уже существующую функцию delete_record
+    foreach ($records as $record) {
+        // Формируем фильтр по id и вызываем delete_record
+        delete_record($db, $table, "id = ?", [$record['id']]);
+    }
+    return true;
+}
+
+function update_records($db, $table, $filter, $newvalue, $filter_params = [])
+{
+    // Получаем ID всех записей, подходящих под фильтр
+    $records = get_records_sql($db, "SELECT id FROM $table WHERE $filter", $filter_params);
+    if (empty($records)) {
+        return true; // ничего не найдено — считаем успехом
+    }
+    // Обновляем каждую запись по отдельности через уже существующую логику
+    foreach ($records as $record) {
+        update_record($db, $table, "id = ?", $newvalue, [$record['id']]);
+    }
+    return true;
+}
+
+function delete_record($db, $table, $filter, $filter_params = [])
 {
     if (!allow_update($table, 'del')) {
-#        LOG_INFO($db, "User does not have write permission");
         return;
     }
     if (!isset($table)) {
-#        LOG_WARNING($db, "Delete FROM unknown table! Skip command.");
         return;
     }
     if (!isset($filter)) {
@@ -925,41 +970,19 @@ function delete_record($db, $table, $filter)
         return;
     }
 
-    $old_sql = "SELECT * FROM $table WHERE $filter";
-    try {
-        $stmt = $db->query($old_sql);
-        $old = $stmt->fetch(PDO::FETCH_ASSOC);
-    } catch (PDOException $e) {
-        LOG_ERROR($db, "SQL: $old_sql :" . $e->getMessage());
-        return;
-    }
-
-    $rec_id = NULL;
-    if (!empty($old['id'])) {
-        $rec_id = $old['id'];
-    }
+    $old_record = get_record_sql($db,"SELECT * FROM $table WHERE $filter",$filter_params);
+    if (empty($old_record)) { return; }
+    $rec_id = $old_record['id'];
 
     $changed_log = 'record: ';
-    if (!empty($old)) {
-        asort($old, SORT_STRING);
-        $old = array_reverse($old, 1);
-        foreach ($old as $key => $value) {
-            if (empty($value)) {
-                continue;
-            }
-            if (preg_match('/action/', $key)) {
-                continue;
-            }
-            if (preg_match('/status/', $key)) {
+    if (!empty($old_record)) {
+        asort($old_record, SORT_STRING);
+        $old_record = array_reverse($old_record, 1);
+        foreach ($old_record as $key => $value) {
+            if (empty($value) || preg_match('/\b(action|status|time|found)\b/i', $key)) {
                 continue;
-            }
-            if (preg_match('/time/', $key)) {
-                continue;
-            }
-            if (preg_match('/found/', $key)) {
-                continue;
-            }
-            $changed_log = $changed_log . " $key => $value,";
+                }
+            $changed_log .= " $key => $value,";
         }
     }
 
@@ -968,24 +991,12 @@ function delete_record($db, $table, $filter)
     //never delete user ip record
     if ($table === 'user_auth') {
         $delete_it = 0;
-        $changed_time = GetNowTimeString();
-        $new_sql = "UPDATE $table SET deleted=1, changed=1, changed_time='" . $changed_time . "' WHERE $filter";
-        LOG_DEBUG($db, "Run sql: $new_sql");
-        try {
-            $sql_result = $db->exec($new_sql);
-            if ($sql_result === false) {
-                LOG_ERROR($db, "UPDATE Request (from delete)");
-                return;
-            }
-        } catch (PDOException $e) {
-            LOG_ERROR($db, "SQL: $new_sql :" . $e->getMessage());
-            return;
-        }
+        update_record($db, $table, $filter, [ 'deleted'=>1, 'changed'=>1 ], $filter_params);
         //dns - A-record
-        if (!empty($old['dns_name']) and !empty($old['ip']) and !$old['dns_ptr_only']  and !preg_match('/\.$/', $old['dns_name'])) {
+        if (!empty($old_record['dns_name']) and !empty($old_record['ip']) and !$old_record['dns_ptr_only']  and !preg_match('/\.$/', $old_record['dns_name'])) {
             $del_dns['name_type'] = 'A';
-            $del_dns['name'] = $old['dns_name'];
-            $del_dns['value'] = $old['ip'];
+            $del_dns['name'] = $old_record['dns_name'];
+            $del_dns['value'] = $old_record['ip'];
             $del_dns['operation_type'] = 'del';
             if (!empty($rec_id)) {
                 $del_dns['auth_id'] = $rec_id;
@@ -993,10 +1004,10 @@ function delete_record($db, $table, $filter)
             insert_record($db, 'dns_queue', $del_dns);
             }
         //ptr
-        if (!empty($old['dns_name']) and !empty($old['ip']) and $old['dns_ptr_only']  and !preg_match('/\.$/', $old['dns_name'])) {
+        if (!empty($old_record['dns_name']) and !empty($old_record['ip']) and $old_record['dns_ptr_only']  and !preg_match('/\.$/', $old_record['dns_name'])) {
             $del_dns['name_type'] = 'PTR';
-            $del_dns['name'] = $old['dns_name'];
-            $del_dns['value'] = $old['ip'];
+            $del_dns['name'] = $old_record['dns_name'];
+            $del_dns['value'] = $old_record['ip'];
             $del_dns['operation_type'] = 'del';
             if (!empty($rec_id)) {
                 $del_dns['auth_id'] = $rec_id;
@@ -1008,19 +1019,19 @@ function delete_record($db, $table, $filter)
         }
 
     //never delete permanent user
-    if ($table === 'user_list' and $old['permanent']) { return; }
+    if ($table === 'user_list' and $old_record['permanent']) { return; }
 
     //remove aliases
     if ($table === 'user_auth_alias') {
         //dns
-        if (!empty($old['alias'])  and !preg_match('/\.$/', $old['alias'])) {
+        if (!empty($old_record['alias'])  and !preg_match('/\.$/', $old_record['alias'])) {
             $del_dns['name_type'] = 'CNAME';
-            $del_dns['name'] = $old['alias'];
+            $del_dns['name'] = $old_record['alias'];
             $del_dns['value'] = '';
             $del_dns['operation_type'] = 'del';
-            if (!empty($old['auth_id'])) {
-                $del_dns['auth_id'] = $old['auth_id'];
-                $del_dns['value'] = get_dns_name($db, $old['auth_id']);
+            if (!empty($old_record['auth_id'])) {
+                $del_dns['auth_id'] = $old_record['auth_id'];
+                $del_dns['value'] = get_dns_name($db, $old_record['auth_id']);
             }
             insert_record($db, 'dns_queue', $del_dns);
         }
@@ -1028,22 +1039,24 @@ function delete_record($db, $table, $filter)
 
     if ($delete_it) {
         $new_sql = "DELETE FROM $table WHERE $filter";
-        LOG_DEBUG($db, "Run sql: $new_sql");
+        LOG_DEBUG($db, "Run sql: $new_sql | params: " . json_encode($filter_params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
         try {
-            $sql_result = $db->exec($new_sql);
+            $stmt = $db->prepare($new_sql);
+            $sql_result = $stmt->execute($filter_params);
             if ($sql_result === false) {
-                LOG_ERROR($db, "DELETE Request: $new_sql");
+                LOG_ERROR($db, "DELETE Request: $new_sql | params: " . json_encode($filter_params));
                 return;
             }
         } catch (PDOException $e) {
-            LOG_ERROR($db, "SQL: $new_sql : " . $e->getMessage());
+            LOG_ERROR($db, "SQL: $new_sql | params: " . json_encode($filter_params) . " : " . $e->getMessage());
             return;
         }
-    } else { return; }
+        } else { return; }
 
     if ($table !== "sessions") {
         LOG_VERBOSE($db, "Deleted FROM table $table WHERE $filter $changed_log");
     }
+
     return $changed_log;
 }
 
@@ -1062,36 +1075,21 @@ function insert_record($db, $table, $newvalue)
         return;
     }
 
-    // Валидация имени таблицы (защита от SQL-инъекций через имя таблицы)
-    if (!preg_match('/^[a-z_][a-z0-9_]*$/', $table)) {
-        // LOG_WARNING($db, "Invalid table name: $table");
-        return;
-    }
-
     $changed_log = '';
     $field_list = [];
     $value_list = [];
     $params = [];
 
-    foreach ($newvalue as $key => $value) {
-        // Валидация имени колонки
-        if (!preg_match('/^[a-z_][a-z0-9_]*$/', $key)) {
-            // Пропускаем недопустимые имена колонок
-            continue;
-        }
-
-        // Обработка пустых значений
-        if ('' === $value && '0' !== $value) {
-            $value = null; // или оставить как '', но null безопаснее для SQL
-        } else {
-            $value = trim((string)$value);
+    if ($table === 'user_auth') {
+        $newvalue['changed']=1;
+        if (!empty($newvalue['ou_id']) and !is_system_ou($db,$newvalue['ou_id'])) { $newvalue['dhcp_changed']=1; }
         }
 
+    foreach ($newvalue as $key => $value) {
         // Логирование (без паролей)
         if (!preg_match('/password/i', $key)) {
             $changed_log .= " $key => " . ($value ?? 'NULL') . ",";
         }
-
         $field_list[] = $key;
         $value_list[] = '?';
         $params[] = $value;
@@ -1106,13 +1104,12 @@ function insert_record($db, $table, $newvalue)
     $value_list_str = implode(',', $value_list);
     $new_sql = "INSERT INTO $table ($field_list_str) VALUES ($value_list_str)";
 
-    LOG_DEBUG($db, "Run sql: $new_sql");
-
+    LOG_DEBUG($db, "Run sql: $new_sql | params: " . json_encode($params, JSON_UNESCAPED_UNICODE));
 
     try {
         $stmt = $db->prepare($new_sql);
         $sql_result = $stmt->execute($params);
-        
+
         if (!$sql_result) {
             LOG_ERROR($db, "INSERT Request");
             return;
@@ -1121,9 +1118,6 @@ function insert_record($db, $table, $newvalue)
         if ($table !== "sessions") {
             LOG_VERBOSE($db, "Create record in table $table: $changed_log with id: $last_id");
         }
-        if ($table === 'user_auth') {
-            run_sql($db, "UPDATE user_auth SET changed=1, dhcp_changed=1 WHERE id=" . $last_id);
-        }
 
         if ($table === 'user_auth_alias') {
             //dns
@@ -1159,84 +1153,66 @@ function insert_record($db, $table, $newvalue)
         }
 
         return $last_id;
-        
+
     } catch (PDOException $e) {
-        LOG_ERROR($db, "SQL: $new_sql :" . $e->getMessage());
-        return;
+        LOG_ERROR($db, "SQL error: $new_sql | params: " . json_encode($params) . " | " . $e->getMessage());
+        return false;
     }
 }
 
-function dump_record($db, $table, $filter)
+function dump_record($db, $table, $filter, $params = [])
 {
     $result = '';
-    $old = get_record($db, $table, $filter);
+    $old = get_record($db, $table, $filter, $params);
     if (empty($old)) {
         return $result;
     }
-    $result = 'record: ' . get_rec_str($old);
+    $result = 'record: ' . hash_to_text($old);
     return $result;
 }
 
-function get_rec_str($array)
+function get_diff_rec($db, $table, $filter, $newvalue, $only_changed = true, $filter_params = [])
 {
-    $result = '';
-    foreach ($array as $key => $value) {
-        $result .= "[" . $key . "]=" . $value . ", ";
+    if (empty($table) || empty($filter) || !is_array($newvalue)) {
+        return '';
     }
-    $result = preg_replace('/,\s+$/', '', $result);
-    return $result;
-}
-
-function get_diff_rec($db, $table, $filter, $newvalue, $only_changed = false)
-{
-    if (!isset($table) || !isset($filter) || !isset($newvalue)) {
+    // Валидация имени таблицы
+    if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $table)) {
         return '';
     }
     $old_sql = "SELECT * FROM $table WHERE $filter";
-    try {
-        $stmt = $db->query($old_sql);
-        $old = $stmt->fetch(PDO::FETCH_ASSOC);
-        
-        if (!$old) {
-            // Запись не найдена — возможно, ошибка или новая запись
-            return "Record not found for filter: $filter";
-        }
-        
-        $changed = [];
-        $unchanged = [];
-        foreach ($newvalue as $key => $new_val) {
-            // Пропускаем ключи, которых нет в старой записи (например, служебные поля)
-            if (!array_key_exists($key, $old)) {
+    $old_record = get_record_sql($db, $old_sql, $filter_params);
+    if (empty($old_record)) { return ''; }
+    
+    $changed = [];
+    $unchanged = [];
+    foreach ($newvalue as $key => $new_val) {
+            // Пропускаем поля, отсутствующие в БД
+            if (!array_key_exists($key, $old_record)) {
                 continue;
             }
-            $old_val = $old[$key];
-            // Сравниваем как строки, но аккуратно с null
-            $old_str = ($old_val === null) ? '' : (string)$old_val;
+            $old_record_val = $old_record[$key];
+            // Приведение к строке с учётом NULL
+            $old_record_str = ($old_record_val === null) ? '' : (string)$old_record_val;
             $new_str = ($new_val === null) ? '' : (string)$new_val;
-            if ($old_str !== $new_str) {
-                $changed[$key] = $new_str . ' [ old: ' . $old_str . ' ]';
-            } else {
-                $unchanged[$key] = $old_val;
+            if ($old_record_str !== $new_str) {
+                $changed[$key] = "$new_str [old: $old_record_str]";
+            } elseif (!$only_changed) {
+                $unchanged[$key] = $old_record_val;
             }
         }
-        if ($only_changed) {
-            return empty($changed) ? '' : hash_to_text($changed);
+    if ($only_changed) {
+            return !empty($changed) ? hash_to_text($changed) : '';
         }
-        $output = '';
-        if (!empty($changed)) {
-            $output .= hash_to_text($changed);
+    if (!empty($changed)) {
+            $output = hash_to_text($changed);
         } else {
-            $output .= "# no changes";
+            $output = "";
         }
-        if (!empty($unchanged)) {
+    if (!empty($unchanged)) {
             $output .= "\r\nHas not changed:\r\n" . hash_to_text($unchanged);
         }
-        return $output;
-        
-    } catch (PDOException $e) {
-        LOG_ERROR($db, "SQL: $old_sql :" . $e->getMessage());
-        return '';
-    }
+    return $output;
 }
 
 function delete_user_auth($db, $id) {

+ 1 - 1
install-eye.sh

@@ -281,7 +281,7 @@ install_deps_altlinux() {
             perl-Net-Telnet perl-DBI \
             perl-Parallel-ForkManager perl-Proc-Daemon \
             perl-DateTime-Format-DateParse perl-DateTime-Format-Strptime \
-            perl-Net-OpenSSH perl-File-Tail \
+            perl-Net-OpenSSH perl-File-Tail perl-Tie-File \
             perl-Crypt-Rijndael perl-Crypt-CBC perl-CryptX perl-Crypt-DES \
             perl-File-Path-Tiny perl-Expect perl-Proc-ProcessTable
 

+ 22 - 24
scripts/eyelib/database.pm

@@ -52,6 +52,7 @@ insert_record
 delete_record
 get_option
 init_option
+is_system_ou
 Set_Variable
 Get_Variable
 Del_Variable
@@ -376,35 +377,22 @@ sub get_option {
     my $option_id = shift;
     return if (!$option_id);
     return if (!$db);
-    
     my $sql = q{
-        SELECT 
-            COALESCE(c.value, co.default_value) as value,
-            co.option_type
-        FROM config_options co
-        LEFT JOIN config c ON c.option_id = co.id AND c.option_id = ?
-        WHERE co.id = ?
-        LIMIT 1
+    SELECT
+    COALESCE(c.value, co.default_value) AS value,
+    co.option_type
+    FROM config_options co
+    LEFT JOIN config c ON c.option_id = co.id
+    WHERE co.id = ?
     };
-    
-    my $record = get_record_sql($db, $sql, $option_id, $option_id);
-    
+    my $record = get_record_sql($db, $sql, $option_id);
     unless ($record) {
         log_error("Option ID $option_id not found in config_options table");
         return;
     }
-    
-    my $result = $record->{value};
-    
-    # Приводим к правильному типу
-    if ($record->{option_type} =~ /^(int|bool)/i) { 
-        $result = $result * 1; 
-    }
-    
-    return $result;
+    return $record->{value};
 }
 
-
 #---------------------------------------------------------------------------------------------------------------
 
 # Внутренняя функция для выполнения параметризованных запросов
@@ -546,8 +534,6 @@ return $result;
 
 #---------------------------------------------------------------------------------------------------------------
 
-#---------------------------------------------------------------------------------------------------------------
-
 sub get_diff_rec {
 my ($db, $table, $record, $filter_sql, @filter_params) = @_;
 return unless $db && $table && $filter_sql;
@@ -676,11 +662,13 @@ my $rec_id = $old_record->{id} || 0;
 
 if ($table eq "user_auth") {
     $rec_id = $old_record->{'id'} if ($old_record->{'id'});
+    my $cur_ou_id = $old_record->{'ou_id'} if ($old_record->{'ou_id'});
+    if (exists $record->{ou_id}) { $cur_ou_id = $record->{'ou_id'}; }
     #disable update field 'created_by'
     if ($old_record->{'created_by'} and exists ($record->{'created_by'})) { delete $record->{'created_by'}; }
     foreach my $field (keys %$record) {
 	if (exists $acl_fields{$field}) { $record->{changed}="1"; }
-        if (exists $dhcp_fields{$field}) { $record->{dhcp_changed}="1"; }
+        if (exists $dhcp_fields{$field} and !is_system_ou($db,$cur_ou_id)) { $record->{dhcp_changed}="1"; }
 	if (exists $dns_fields{$field}) { $dns_changed=1; }
         }
     }
@@ -858,6 +846,16 @@ return do_sql($db,$sSQL,@filter_params);
 
 #---------------------------------------------------------------------------------------------------------------
 
+sub is_system_ou {
+    my ($db, $ou_id) = @_;
+    return 0 if !defined $ou_id || $ou_id !~ /^\d+$/ || $ou_id <= 0;
+    my $sql = "SELECT 1 FROM ou WHERE id = ? AND (default_users = 1 OR default_hotspot = 1)";
+    my $record = get_record_sql($db, $sql, [$ou_id]);
+    return $record ? 1 : 0;
+}
+
+#---------------------------------------------------------------------------------------------------------------
+
 sub init_option {
 my $db=shift;
 

+ 1 - 0
scripts/eyelib/main.pm

@@ -15,6 +15,7 @@ use base 'Exporter';
 use vars qw(@EXPORT @ISA);
 use eyelib::config;
 use Socket;
+use POSIX;
 use IO::Select;
 use IO::Handle;
 use Crypt::CBC;

+ 20 - 23
scripts/garbage.pl

@@ -91,12 +91,12 @@ foreach my $net (@office_network_list) {
 # Define the current month’s start and end (for monthly stats)
 my $now = DateTime->now(time_zone => 'local');
 $now->set(day => 1);
-my $month_start = $dbh->quote($now->ymd("-") . " 00:00:00");
+my $month_start = $now->ymd("-") . " 00:00:00";
 
 my $month_dur = DateTime::Duration->new(months => 1);
 my $next_month = $now + $month_dur;
 $next_month->set(day => 1);
-my $month_stop = $dbh->quote($next_month->ymd("-") . " 00:00:00");
+my $month_stop = $next_month->ymd("-") . " 00:00:00";
 
 # Build DHCP network structures for lease validation
 my $dhcp_networks = Net::Patricia->new;
@@ -121,17 +121,15 @@ if ($day == 1) {
     log_info($dbh, 'Daily traffic-based unblocking started');
 
     my $month_sql = "
-        SELECT user_list.id, user_list.login, SUM(traf_all) AS traf_sum, user_list.month_quota AS uquota
-        FROM (
-            SELECT user_stats.auth_id, SUM(byte_in + byte_out) AS traf_all
-            FROM user_stats
-            WHERE user_stats.ts >= ? AND user_stats.ts < ?
-            GROUP BY user_stats.auth_id
-        ) AS V, user_auth, user_list
-        WHERE V.auth_id = user_auth.id
-          AND user_auth.user_id = user_list.id
-          AND user_list.blocked = 1
-        GROUP BY login
+    SELECT
+        ul.id,
+        SUM(us.byte_in + us.byte_out) AS traf_sum,
+        ul.month_quota AS uquota
+    FROM user_stats us
+    JOIN user_auth ua ON us.auth_id = ua.id
+    JOIN user_list ul ON ua.user_id = ul.id
+    WHERE ul.blocked = 1 AND us.ts >= ? AND us.ts < ?
+    GROUP BY ul.id, ul.month_quota;
     ";
 
     my @month_stats = get_records_sql($dbh, $month_sql, $month_start, $month_stop);
@@ -178,7 +176,7 @@ $now = DateTime->now(time_zone => 'local');
 if ($history_dhcp) {
     my $day_dur = DateTime::Duration->new(days => $history_dhcp);
     my $clean_date = $now - $day_dur;
-    my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
+    my $clean_str = $clean_date->ymd("-") . " 00:00:00";
     log_info($dbh, 'Clearing outdated DHCP log records');
     do_sql($dbh, "DELETE FROM dhcp_log WHERE ts < ?",$clean_str);
     log_verbose($dbh, "Removed DHCP log entries older than $clean_str");
@@ -189,9 +187,8 @@ if ($connections_history) {
     log_info($dbh, 'Clearing outdated connection records');
     my $day_dur = DateTime::Duration->new(days => $connections_history);
     my $clean_date = $now - $day_dur;
-    my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
-    $users_sql = "SELECT id FROM user_auth WHERE last_found < ? AND last_found > 0";
-    log_debug($dbh, $users_sql) if ($debug);
+    my $clean_str = $clean_date->ymd("-") . " 00:00:00";
+    $users_sql = "SELECT id FROM user_auth WHERE last_found < ? AND last_found IS NOT NULL";
     @users_auth = get_records_sql($dbh, $users_sql, $clean_str);
     foreach my $row (@users_auth) {
         log_debug($dbh, "Clearing old connection for auth_id: " . $row->{id});
@@ -262,7 +259,7 @@ foreach my $row (@users_auth) {
 if ($history) {
     my $day_dur = DateTime::Duration->new(days => $history);
     my $clean_date = $now - $day_dur;
-    my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
+    my $clean_str = $clean_date->ymd("-") . " 00:00:00";
     log_info($dbh, "Cleaning traffic detail records older than $clean_str");
     do_sql($dbh, "DELETE FROM traffic_detail WHERE ts < ?", $clean_str);
 }
@@ -271,7 +268,7 @@ if ($history) {
 if ($history_log_day) {
     my $day_dur = DateTime::Duration->new(days => $history_log_day);
     my $clean_date = $now - $day_dur;
-    my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
+    my $clean_str = $clean_date->ymd("-") . " 00:00:00";
     log_info($dbh, "Cleaning VERBOSE worklog entries older than $clean_str");
     do_sql($dbh, "DELETE FROM worklog WHERE level > ? AND ts < ?", $L_INFO, $clean_str);
 }
@@ -280,7 +277,7 @@ if ($history_log_day) {
 if ($debug_history) {
     my $day_dur = DateTime::Duration->new(days => 3);
     my $clean_date = $now - $day_dur;
-    my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
+    my $clean_str = $clean_date->ymd("-") . " 00:00:00";
     log_info($dbh, "Cleaning debug worklog entries older than $clean_str");
     do_sql($dbh, "DELETE FROM worklog WHERE level >= ? AND ts < ?",$L_DEBUG, $clean_str);
 }
@@ -289,7 +286,7 @@ if ($debug_history) {
 if ($history_syslog_day) {
     my $day_dur = DateTime::Duration->new(days => $history_syslog_day);
     my $clean_date = $now - $day_dur;
-    my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
+    my $clean_str = $clean_date->ymd("-") . " 00:00:00";
     log_info($dbh, "Cleaning syslog entries older than $clean_str");
     do_sql($dbh, "DELETE FROM remote_syslog WHERE ts < ?",$clean_str);
 }
@@ -298,7 +295,7 @@ if ($history_syslog_day) {
 if ($history_trafstat_day) {
     my $day_dur = DateTime::Duration->new(days => $history_trafstat_day);
     my $clean_date = $now - $day_dur;
-    my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
+    my $clean_str = $clean_date->ymd("-") . " 00:00:00";
     log_info($dbh, "Cleaning traffic statistics older than $clean_str");
     do_sql($dbh, "DELETE FROM user_stats WHERE ts < ?",$clean_str);
 }
@@ -308,7 +305,7 @@ my $iptraf_history = $config_ref{traffic_ipstat_history};
 if ($iptraf_history) {
     my $day_dur = DateTime::Duration->new(days => $iptraf_history);
     my $clean_date = $now - $day_dur;
-    my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
+    my $clean_str = $clean_date->ymd("-") . " 00:00:00";
     log_info($dbh, "Cleaning full traffic statistics older than $clean_str");
     do_sql($dbh, "DELETE FROM user_stats_full WHERE ts < ?",$clean_str);
 }