database.pm 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032
  1. package eyelib::database;
  2. #
  3. # Copyright (C) Roman Dmitriev, rnd@rajven.ru
  4. #
  5. # commit example
  6. # Начинаем транзакцию вручную
  7. #$db->{AutoCommit} = 0;
  8. #eval {
  9. # for my $row (@rows) {
  10. # insert_record($db, 'user_auth', $row);
  11. # insert_record($db, 'user_auth_alias', $row2);
  12. # }
  13. # $db->commit();
  14. #};
  15. #if ($@) {
  16. # eval { $db->rollback(); };
  17. # die "Migration failed: $@";
  18. #}
  19. #$db->{AutoCommit} = 1;
  20. use utf8;
  21. use open ":encoding(utf8)";
  22. use strict;
  23. use English;
  24. use FindBin '$Bin';
  25. use lib "/opt/Eye/scripts";
  26. use base 'Exporter';
  27. use vars qw(@EXPORT @ISA);
  28. use eyelib::config;
  29. use eyelib::main;
  30. use Net::Patricia;
  31. use eyelib::net_utils;
  32. use Data::Dumper;
  33. use DateTime;
  34. use POSIX qw(mktime ctime strftime);
  35. use File::Temp qw(tempfile);
  36. use DBI;
  37. use DBD::Pg qw(:pg_types);
  38. use Text::CSV;
  39. our @ISA = qw(Exporter);
  40. our @EXPORT = qw(
  41. get_notify_subnet
  42. is_hotspot
  43. get_queue
  44. get_group
  45. get_subnet_description
  46. get_filter_instance_description
  47. get_vendor_name
  48. get_ou
  49. get_device_name
  50. get_device_model
  51. get_device_model_name
  52. get_building
  53. get_filter
  54. get_login
  55. StrToIp
  56. IpToStr
  57. prepare_audit_message
  58. batch_db_sql_cached
  59. batch_db_sql_csv
  60. reconnect_db
  61. write_db_log
  62. db_log_debug
  63. db_log_error
  64. db_log_info
  65. db_log_verbose
  66. db_log_warning
  67. normalize_value
  68. get_table_columns
  69. init_db
  70. do_sql
  71. _execute_param
  72. do_sql_param
  73. get_option_safe
  74. get_count_records
  75. get_id_record
  76. get_records_sql
  77. get_record_sql
  78. get_diff_rec
  79. update_record
  80. insert_record
  81. delete_record
  82. get_option
  83. init_option
  84. is_system_ou
  85. Set_Variable
  86. Get_Variable
  87. Del_Variable
  88. clean_variables
  89. build_db_schema
  90. $add_rules
  91. $L_WARNING
  92. $L_INFO
  93. $L_DEBUG
  94. $L_ERROR
  95. $L_VERBOSE
  96. %db_schema
  97. );
  98. BEGIN
  99. {
  100. #---------------------------------------------------------------------------------------------------------------
  101. our $add_rules;
  102. our $L_ERROR = 0;
  103. our $L_WARNING = 1;
  104. our $L_INFO = 2;
  105. our $L_VERBOSE = 3;
  106. our $L_DEBUG = 255;
  107. our %acl_fields = (
  108. 'ip' => '1',
  109. 'ip_int' => '1',
  110. 'enabled'=>'1',
  111. 'dhcp'=>'1',
  112. 'filter_group_id'=>'1',
  113. 'deleted'=>'1',
  114. 'dhcp_acl'=>'1',
  115. 'queue_id'=>'1',
  116. 'mac'=>'1',
  117. 'blocked'=>'1'
  118. );
  119. our %dhcp_fields = (
  120. 'ip' => '1',
  121. 'dhcp_acl'=>'1',
  122. 'dhcp_option_set'=>'1',
  123. 'dhcp'=>'1',
  124. 'deleted'=>'1',
  125. 'mac'=>'1',
  126. );
  127. our %dns_fields = (
  128. 'ip' => '1',
  129. 'dns_name'=>'1',
  130. 'dns_ptr_only'=>'1',
  131. 'alias'=>'1',
  132. );
  133. our %db_schema;
  134. #---------------------------------------------------------------------------------------------------------------
  135. sub get_notify_subnet {
  136. my $db = shift;
  137. my $ip = shift;
  138. my $notify_flag = get_office_subnet($db,$ip);
  139. if ($notify_flag) { return $notify_flag->{notify}; }
  140. return 0;
  141. }
  142. #---------------------------------------------------------------------------------------------------------------
  143. sub is_hotspot {
  144. my ($db, $ip) = @_;
  145. return 0 unless $db && defined $ip;
  146. my @subnets = get_records_sql(
  147. $db,
  148. "SELECT subnet FROM subnets WHERE hotspot = 1 AND LENGTH(subnet) > 0"
  149. );
  150. my $pat = Net::Patricia->new;
  151. for my $row (@subnets) {
  152. $pat->add_string($row->{subnet}) if defined $row->{subnet};
  153. }
  154. return $pat->match_string($ip) ? 1 : 0;
  155. }
  156. #---------------------------------------------------------------------------------------------------------------
  157. # Вспомогательная функция для проверки "пустого" значения
  158. sub _is_empty {
  159. my ($val) = @_;
  160. return !defined $val || $val eq '';
  161. }
  162. #---------------------------------------------------------------------------------------------------------------
  163. sub get_queue {
  164. my ($dbh, $queue_value) = @_;
  165. return '' if _is_empty($queue_value);
  166. my $queue = get_record_sql($dbh, "SELECT queue_name FROM queue_list WHERE id = ?", $queue_value);
  167. return $queue->{queue_name} // '';
  168. }
  169. #---------------------------------------------------------------------------------------------------------------
  170. sub get_group {
  171. my ($dbh, $group_id) = @_;
  172. return '' if _is_empty($group_id);
  173. my $group = get_record_sql($dbh, "SELECT group_name FROM group_list WHERE id = ?", $group_id);
  174. return $group->{group_name} // '';
  175. }
  176. #---------------------------------------------------------------------------------------------------------------
  177. sub get_subnet_description {
  178. my ($dbh, $subnet_id) = @_;
  179. return '' if _is_empty($subnet_id);
  180. my $subnet = get_record_sql($dbh, "SELECT * FROM subnets WHERE id = ?", $subnet_id);
  181. return '' unless $subnet;
  182. my $desc = $subnet->{description} // '';
  183. return "$subnet->{subnet} ($desc)";
  184. }
  185. #---------------------------------------------------------------------------------------------------------------
  186. sub get_filter_instance_description {
  187. my ($dbh, $instance_id) = @_;
  188. return '' if _is_empty($instance_id);
  189. my $instance = get_record_sql($dbh, "SELECT * FROM filter_instances WHERE id = ?", $instance_id);
  190. return '' unless $instance;
  191. my $desc = $instance->{description} // '';
  192. return "$instance->{name} ($desc)";
  193. }
  194. #---------------------------------------------------------------------------------------------------------------
  195. sub get_vendor_name {
  196. my ($dbh, $v_id) = @_;
  197. return '' if _is_empty($v_id);
  198. my $vendor = get_record_sql($dbh, "SELECT name FROM vendors WHERE id = ?", $v_id);
  199. return $vendor->{name} // '';
  200. }
  201. #---------------------------------------------------------------------------------------------------------------
  202. sub get_ou {
  203. my ($dbh, $ou_value) = @_;
  204. return undef if _is_empty($ou_value);
  205. my $ou_name = get_record_sql($dbh, "SELECT ou_name FROM ou WHERE id = ?", $ou_value);
  206. return $ou_name ? $ou_name->{ou_name} : undef;
  207. }
  208. #---------------------------------------------------------------------------------------------------------------
  209. sub get_device_name {
  210. my ($dbh, $device_id) = @_;
  211. return undef if _is_empty($device_id);
  212. my $dev = get_record_sql($dbh, "SELECT device_name FROM devices WHERE id = ?", $device_id);
  213. return $dev ? $dev->{device_name} : undef;
  214. }
  215. #---------------------------------------------------------------------------------------------------------------
  216. sub get_device_model {
  217. my ($dbh, $model_value) = @_;
  218. return undef if _is_empty($model_value);
  219. my $model_name = get_record_sql($dbh, "SELECT model_name FROM device_models WHERE id = ?", $model_value);
  220. return $model_name ? $model_name->{model_name} : undef;
  221. }
  222. #---------------------------------------------------------------------------------------------------------------
  223. sub get_device_model_name {
  224. my ($dbh, $model_value) = @_;
  225. return '' if _is_empty($model_value);
  226. my $row = get_record_sql($dbh, "SELECT M.id, M.model_name, V.name FROM device_models M, vendors V WHERE M.vendor_id = V.id AND M.id = ?", $model_value);
  227. return '' unless $row;
  228. my $vendor = $row->{name} // '';
  229. my $model = $row->{model_name} // '';
  230. return "$vendor $model";
  231. }
  232. #---------------------------------------------------------------------------------------------------------------
  233. sub get_building {
  234. my ($dbh, $building_value) = @_;
  235. return undef if _is_empty($building_value);
  236. my $building_name = get_record_sql($dbh, "SELECT name FROM building WHERE id = ?", $building_value);
  237. return $building_name ? $building_name->{name} : undef;
  238. }
  239. #---------------------------------------------------------------------------------------------------------------
  240. sub get_filter {
  241. my ($dbh, $filter_value) = @_;
  242. return '' if _is_empty($filter_value);
  243. my $filter = get_record_sql($dbh, "SELECT name FROM filter_list WHERE id = ?", $filter_value);
  244. return $filter->{name} // '';
  245. }
  246. #---------------------------------------------------------------------------------------------------------------
  247. sub get_login {
  248. my ($dbh, $user_id) = @_;
  249. return '' if _is_empty($user_id);
  250. my $login = get_record_sql($dbh, "SELECT login FROM user_list WHERE id = ?", $user_id);
  251. return $login->{login} // '';
  252. }
  253. #---------------------------------------------------------------------------------------------------------------
  254. sub prepare_audit_message {
  255. my ($dbh, $table, $old_data, $new_data, $record_id, $operation) = @_;
  256. # === 1. Конфигурация отслеживаемых таблиц ===
  257. my %audit_config = (
  258. 'auth_rules' => {
  259. summary => ['rule'],
  260. fields => ['user_id', 'ou_id', 'rule_type', 'rule', 'description']
  261. },
  262. 'building' => {
  263. summary => ['name'],
  264. fields => ['name', 'description']
  265. },
  266. 'customers' => {
  267. summary => ['login'],
  268. fields => ['login', 'description', 'rights']
  269. },
  270. 'devices' => {
  271. summary => ['device_name'],
  272. fields => [
  273. 'device_type', 'device_model_id', 'vendor_id', 'device_name', 'building_id',
  274. 'ip', 'login', 'protocol', 'control_port', 'port_count', 'sn',
  275. 'description', 'snmp_version', 'snmp3_auth_proto', 'snmp3_priv_proto',
  276. 'snmp3_user_rw', 'snmp3_user_ro', 'community', 'rw_community',
  277. 'discovery', 'netflow_save', 'user_acl', 'dhcp', 'nagios',
  278. 'active', 'queue_enabled', 'connected_user_only', 'user_id'
  279. ]
  280. },
  281. 'device_filter_instances' => {
  282. summary => [],
  283. fields => ['instance_id', 'device_id']
  284. },
  285. 'device_l3_interfaces' => {
  286. summary => ['name'],
  287. fields => ['device_id', 'snmpin', 'interface_type', 'name']
  288. },
  289. 'device_models' => {
  290. summary => ['model_name'],
  291. fields => ['model_name', 'vendor_id', 'poe_in', 'poe_out', 'nagios_template']
  292. },
  293. 'device_ports' => {
  294. summary => ['port', 'ifname'],
  295. fields => [
  296. 'device_id', 'snmp_index', 'port', 'ifname', 'port_name', 'description',
  297. 'target_port_id', 'auth_id', 'last_mac_count', 'uplink', 'nagios',
  298. 'skip', 'vlan', 'tagged_vlan', 'untagged_vlan', 'forbidden_vlan'
  299. ]
  300. },
  301. 'filter_instances' => {
  302. summary => ['name'],
  303. fields => ['name', 'description']
  304. },
  305. 'filter_list' => {
  306. summary => ['name'],
  307. fields => ['name', 'description', 'proto', 'dst', 'dstport', 'srcport', 'filter_type']
  308. },
  309. 'gateway_subnets' => {
  310. summary => [],
  311. fields => ['device_id', 'subnet_id']
  312. },
  313. 'group_filters' => {
  314. summary => [],
  315. fields => ['group_id', 'filter_id', 'rule_order', 'action']
  316. },
  317. 'group_list' => {
  318. summary => ['group_name'],
  319. fields => ['instance_id', 'group_name', 'description']
  320. },
  321. 'ou' => {
  322. summary => ['ou_name'],
  323. fields => [
  324. 'ou_name', 'description', 'default_users', 'default_hotspot',
  325. 'nagios_dir', 'nagios_host_use', 'nagios_ping', 'nagios_default_service',
  326. 'enabled', 'filter_group_id', 'queue_id', 'dynamic', 'life_duration', 'parent_id'
  327. ]
  328. },
  329. 'queue_list' => {
  330. summary => ['queue_name'],
  331. fields => ['queue_name', 'download', 'upload']
  332. },
  333. 'subnets' => {
  334. summary => ['subnet'],
  335. fields => [
  336. 'subnet', 'vlan_tag', 'ip_int_start', 'ip_int_stop', 'dhcp_start', 'dhcp_stop',
  337. 'dhcp_lease_time', 'gateway', 'office', 'hotspot', 'vpn', 'free', 'dhcp',
  338. 'static', 'dhcp_update_hostname', 'discovery', 'notify', 'description'
  339. ]
  340. },
  341. 'user_auth' => {
  342. summary => ['ip', 'dns_name'],
  343. fields => [
  344. 'user_id', 'ou_id', 'ip', 'save_traf', 'enabled', 'dhcp', 'filter_group_id',
  345. 'dynamic', 'end_life', 'description', 'dns_name', 'dns_ptr_only', 'wikiname',
  346. 'dhcp_acl', 'queue_id', 'mac', 'dhcp_option_set', 'blocked', 'day_quota',
  347. 'month_quota', 'device_model_id', 'firmware', 'client_id', 'nagios',
  348. 'nagios_handler', 'link_check', 'deleted'
  349. ]
  350. },
  351. 'user_auth_alias' => {
  352. summary => ['alias'],
  353. fields => ['auth_id', 'alias', 'description']
  354. },
  355. 'user_list' => {
  356. summary => ['login'],
  357. fields => [
  358. 'login', 'description', 'enabled', 'blocked', 'deleted', 'ou_id',
  359. 'device_id', 'filter_group_id', 'queue_id', 'day_quota', 'month_quota', 'permanent'
  360. ]
  361. },
  362. 'vendors' => {
  363. summary => ['name'],
  364. fields => ['name']
  365. }
  366. );
  367. return undef unless exists $audit_config{$table};
  368. my $summary_fields = $audit_config{$table}{summary};
  369. my $monitored_fields = $audit_config{$table}{fields};
  370. # === 2. Нормализация данных и определение изменений ===
  371. my %changes;
  372. if ($operation eq 'insert') {
  373. for my $field (@$monitored_fields) {
  374. if (exists $new_data->{$field}) {
  375. $changes{$field} = { old => undef, new => $new_data->{$field} };
  376. }
  377. }
  378. }
  379. elsif ($operation eq 'delete') {
  380. for my $field (@$monitored_fields) {
  381. if (exists $old_data->{$field}) {
  382. $changes{$field} = { old => $old_data->{$field}, new => undef };
  383. }
  384. }
  385. }
  386. elsif ($operation eq 'update') {
  387. $old_data //= {};
  388. $new_data //= {};
  389. for my $field (@$monitored_fields) {
  390. next unless exists $new_data->{$field}; # частичное обновление
  391. my $old_val = exists $old_data->{$field} ? $old_data->{$field} : undef;
  392. my $new_val = $new_data->{$field};
  393. my $old_str = !defined($old_val) ? '' : "$old_val";
  394. my $new_str = !defined($new_val) ? '' : "$new_val";
  395. if ($old_str ne $new_str) {
  396. $changes{$field} = { old => $old_val, new => $new_val };
  397. }
  398. }
  399. }
  400. return undef unless %changes;
  401. # === 3. Краткое описание записи ===
  402. my @summary_parts;
  403. for my $field (@$summary_fields) {
  404. my $val = defined($new_data->{$field}) ? $new_data->{$field}
  405. : (defined($old_data->{$field}) ? $old_data->{$field} : undef);
  406. push @summary_parts, "$val" if defined $val && $val ne '';
  407. }
  408. my $summary_label = @summary_parts
  409. ? '"' . join(' | ', @summary_parts) . '"'
  410. : "ID=$record_id";
  411. # === 4. Расшифровка *_id полей ===
  412. my %resolved_changes;
  413. for my $field (keys %changes) {
  414. my $old_resolved = resolve_reference_value($dbh, $field, $changes{$field}{old});
  415. my $new_resolved = resolve_reference_value($dbh, $field, $changes{$field}{new});
  416. $resolved_changes{$field} = { old => $old_resolved, new => $new_resolved };
  417. }
  418. # === 5. Формирование сообщения ===
  419. my $op_label = 'Updated';
  420. if ($operation eq 'insert') {
  421. $op_label = 'Created';
  422. } elsif ($operation eq 'delete') {
  423. $op_label = 'Deleted';
  424. } else {
  425. $op_label = ucfirst($operation);
  426. }
  427. my $message = sprintf("[%s] %s (%s) in table `%s`:\n",
  428. $op_label,
  429. ucfirst($table),
  430. $summary_label,
  431. $table
  432. );
  433. for my $field (sort keys %resolved_changes) {
  434. my $change = $resolved_changes{$field};
  435. if ($operation eq 'insert') {
  436. if (defined $change->{new}) {
  437. $message .= sprintf(" %s: %s\n", $field, $change->{new});
  438. }
  439. } elsif ($operation eq 'delete') {
  440. if (defined $change->{old}) {
  441. $message .= sprintf(" %s: %s\n", $field, $change->{old});
  442. }
  443. } else { # update
  444. my $old_display = !defined($change->{old}) ? '[NULL]' : $change->{old};
  445. my $new_display = !defined($change->{new}) ? '[NULL]' : $change->{new};
  446. $message .= sprintf(" %s: \"%s\" → \"%s\"\n", $field, $old_display, $new_display);
  447. }
  448. }
  449. chomp $message;
  450. return $message;
  451. }
  452. #---------------------------------------------------------------------------------------------------------------
  453. sub resolve_reference_value {
  454. my ($dbh, $field, $value) = @_;
  455. return undef if !defined $value || $value eq '';
  456. # Проверка на целое число (как в PHP)
  457. if ($value !~ /^[+-]?\d+$/) {
  458. return "$value";
  459. }
  460. my $as_int = int($value);
  461. if ("$as_int" ne "$value") {
  462. return "$value";
  463. }
  464. my $id = $as_int;
  465. if ($field eq 'device_id') {
  466. return get_device_name($dbh, $id) // "Device#$id";
  467. }
  468. elsif ($field eq 'building_id') {
  469. return get_building($dbh, $id) // "Building#$id";
  470. }
  471. elsif ($field eq 'user_id') {
  472. return get_login($dbh, $id) // "User#$id";
  473. }
  474. elsif ($field eq 'ou_id') {
  475. return get_ou($dbh, $id) // "OU#$id";
  476. }
  477. elsif ($field eq 'vendor_id') {
  478. return get_vendor_name($dbh, $id) // "Vendor#$id";
  479. }
  480. elsif ($field eq 'device_model_id') {
  481. return get_device_model_name($dbh, $id) // "Model#$id";
  482. }
  483. elsif ($field eq 'instance_id') {
  484. return get_filter_instance_description($dbh, $id) // "FilterInstance#$id";
  485. }
  486. elsif ($field eq 'subnet_id') {
  487. return get_subnet_description($dbh, $id) // "Subnet#$id";
  488. }
  489. elsif ($field eq 'group_id') {
  490. return get_group($dbh, $id) // "FilterGroup#$id";
  491. }
  492. elsif ($field eq 'filter_id') {
  493. return get_filter($dbh, $id) // "Filter#$id";
  494. }
  495. elsif ($field eq 'filter_group_id') {
  496. return get_group($dbh, $id) // "FilterGroup#$id";
  497. }
  498. elsif ($field eq 'queue_id') {
  499. return get_queue($dbh, $id) // "Queue#$id";
  500. }
  501. elsif ($field eq 'auth_id') {
  502. return 'None' if $id <= 0;
  503. my $sql = "
  504. SELECT
  505. COALESCE(ul.login, CONCAT('User#', ua.user_id)) AS login,
  506. ua.ip,
  507. ua.dns_name
  508. FROM user_auth ua
  509. LEFT JOIN user_list ul ON ul.id = ua.user_id
  510. WHERE ua.id = ?
  511. ";
  512. my $row = get_record_sql($dbh, $sql, $id);
  513. return "Auth#$id" unless $row;
  514. my @parts;
  515. push @parts, "login: $row->{login}" if $row->{login} && $row->{login} ne '';
  516. push @parts, "IP: $row->{ip}" if $row->{ip} && $row->{ip} ne '';
  517. push @parts, "DNS: $row->{dns_name}" if $row->{dns_name} && $row->{dns_name} ne '';
  518. return @parts ? join(', ', @parts) : "Auth#$id";
  519. }
  520. elsif ($field eq 'target_port_id') {
  521. return 'None' if $id == 0;
  522. my $sql = "
  523. SELECT CONCAT(d.device_name, '[', dp.port, ']')
  524. FROM device_ports dp
  525. JOIN devices d ON d.id = dp.device_id
  526. WHERE dp.id = ?
  527. ";
  528. my $name = $dbh->selectrow_array($sql, undef, $id);
  529. return $name // "Port#$id";
  530. }
  531. else {
  532. return "$value";
  533. }
  534. }
  535. #---------------------------------------------------------------------------------------------------------------
  536. sub build_db_schema {
  537. my ($dbh) = @_;
  538. # Определяем тип СУБД
  539. my $db_type = lc($dbh->{Driver}->{Name});
  540. die "Unsupported database driver: $db_type"
  541. unless $db_type eq 'mysql' || $db_type eq 'pg';
  542. # Получаем имя базы данных
  543. my $db_name;
  544. if ($db_type eq 'mysql') {
  545. ($db_name) = $dbh->selectrow_array("SELECT DATABASE()");
  546. } elsif ($db_type eq 'pg') {
  547. ($db_name) = $dbh->selectrow_array("SELECT current_database()");
  548. }
  549. my $db_info;
  550. $db_info->{db_type}=$db_type;
  551. $db_info->{db_name}=$db_name;
  552. return $db_info if (exists $db_schema{$db_type}{$db_name});
  553. # Получаем список таблиц
  554. my @tables;
  555. if ($db_type eq 'mysql') {
  556. my $sth = $dbh->prepare("SHOW TABLES");
  557. $sth->execute();
  558. @tables = map { $_->[0] } @{$sth->fetchall_arrayref()};
  559. } elsif ($db_type eq 'pg') {
  560. my $sql = q{
  561. SELECT tablename
  562. FROM pg_tables
  563. WHERE schemaname = 'public'
  564. };
  565. my $sth = $dbh->prepare($sql);
  566. $sth->execute();
  567. @tables = map { $_->[0] } @{$sth->fetchall_arrayref()};
  568. }
  569. # Собираем схему
  570. for my $table (@tables) {
  571. my $sth = $dbh->column_info(undef, undef, $table, '%');
  572. while (my $col = $sth->fetchrow_hashref) {
  573. my $col_name = lc($col->{COLUMN_NAME});
  574. $db_schema{$db_type}{$db_name}{$table}{$col_name} = {
  575. type => $col->{TYPE_NAME} // '',
  576. nullable => $col->{NULLABLE} // 1,
  577. default => $col->{COLUMN_DEF} // undef,
  578. };
  579. }
  580. }
  581. return $db_info;
  582. }
  583. #---------------------------------------------------------------------------------------------------------------
  584. sub normalize_value {
  585. my ($value, $col_info) = @_;
  586. # Если значение пустое — обрабатываем по правилам колонки
  587. if (!defined $value || $value eq '' || $value =~ /^(?:NULL|\\N)$/i) {
  588. return $col_info->{nullable} ? undef : _default_for_type($col_info);
  589. }
  590. my $type = lc($col_info->{type});
  591. # --- Числовые типы: приводим к числу, если выглядит как число ---
  592. if ($type =~ /^(?:tinyint|smallint|mediumint|int|integer|bigint|serial|bigserial)$/i) {
  593. # Просто конвертируем строку в число (Perl сам обрежет мусор)
  594. # Например: "123abc" → 123, "abc" → 0
  595. return 0 + $value;
  596. }
  597. # --- Булевы: приводим к 0/1 ---
  598. if ($type =~ /^(?:bool|boolean|bit)$/i) {
  599. return $value ? 1 : 0;
  600. }
  601. # --- Временные типы: оставляем как есть, но фильтруем "нулевые" даты MySQL ---
  602. if ($type =~ /^(?:timestamp|datetime|date|time)$/i) {
  603. # Это частая проблема при миграции — '0000-00-00' ломает PostgreSQL
  604. return undef if $value =~ /^0000-00-00/;
  605. return $value;
  606. }
  607. # --- Все остальные типы (строки, inet, json и т.д.) — передаём как есть ---
  608. return $value;
  609. }
  610. # Вспомогательная: безопасное значение по умолчанию
  611. sub _default_for_type {
  612. my ($col) = @_;
  613. # Используем DEFAULT, только если он простой литерал (не выражение)
  614. if (defined $col->{default}) {
  615. my $def = $col->{default};
  616. # Пропускаем выражения: nextval(), CURRENT_TIMESTAMP, NOW(), uuid() и т.п.
  617. if ($def !~ /(nextval|current_timestamp|now|uuid|auto_increment|::)/i) {
  618. # Убираем одинарные кавычки, если строка: 'value' → value
  619. if ($def =~ /^'(.*)'$/) {
  620. return $1;
  621. }
  622. # Если похоже на число — вернём как число
  623. if ($def =~ /^[+-]?\d+$/) {
  624. return 0 + $def;
  625. }
  626. return $def;
  627. }
  628. }
  629. # Фолбэк по типу
  630. my $type = lc($col->{type});
  631. if ($type =~ /^(?:tinyint|smallint|int|integer|bigint)/i) { return 0; }
  632. if ($type =~ /^(?:char|varchar|text)/i) { return ''; }
  633. if ($type =~ /^(?:timestamp|datetime)/i) { return GetNowTime(); }
  634. return undef;
  635. }
  636. #---------------------------------------------------------------------------------------------------------------
  637. sub get_table_columns {
  638. my ($db, $table) = @_;
  639. my %columns;
  640. my $sth = $db->column_info(undef, undef, $table, '%');
  641. while (my $row = $sth->fetchrow_hashref) {
  642. my $name = lc($row->{COLUMN_NAME}); # ← приводим к нижнему регистру сразу!
  643. $columns{$name} = {
  644. type => $row->{TYPE_NAME} // '',
  645. nullable => $row->{NULLABLE} // 1,
  646. default => $row->{COLUMN_DEF} // undef,
  647. };
  648. }
  649. return %columns; # возвращает список: key1, val1, key2, val2...
  650. }
  651. #---------------------------------------------------------------------------------------------------------------
  652. sub StrToIp {
  653. return unpack('N',pack('C4',split(/\./,$_[0])));
  654. }
  655. #---------------------------------------------------------------------------------------------------------------
  656. sub IpToStr {
  657. my $nIP = shift;
  658. my $res = (($nIP>>24) & 255) .".". (($nIP>>16) & 255) .".". (($nIP>>8) & 255) .".". ($nIP & 255);
  659. return $res;
  660. }
  661. #---------------------------------------------------------------------------------------------------------------
  662. sub batch_db_sql_cached {
  663. my ( $sql, $data) = @_;
  664. my $db=init_db();
  665. # Запоминаем исходное состояние AutoCommit
  666. my $original_autocommit = $db->{AutoCommit};
  667. eval {
  668. # Выключаем AutoCommit для транзакции
  669. $db->{AutoCommit} = 0;
  670. my $sth = $db->prepare_cached($sql) or die "Unable to prepare SQL: " . $db->errstr;
  671. for my $params (@$data) {
  672. next unless @$params;
  673. $sth->execute(@$params) or die "Unable to execute with params [" . join(',', @$params) . "]: " . $sth->errstr;
  674. }
  675. $db->commit();
  676. 1;
  677. } or do {
  678. my $err = $@ || 'Unknown error';
  679. eval { $db->rollback() };
  680. warn "batch_sql_cached failed: $err";
  681. # Восстанавливаем AutoCommit даже при ошибке
  682. $db->{AutoCommit} = $original_autocommit;
  683. return 0;
  684. };
  685. # Восстанавливаем исходный режим AutoCommit
  686. $db->{AutoCommit} = $original_autocommit;
  687. $db->disconnect();
  688. return 1;
  689. }
  690. #---------------------------------------------------------------------------------------------------------------
  691. sub batch_db_sql_csv {
  692. my ($table, $data) = @_;
  693. return 0 unless @$data;
  694. # Первая строка — заголовки (имена столбцов)
  695. my $header_row = shift @$data;
  696. unless ($header_row && ref($header_row) eq 'ARRAY' && @$header_row) {
  697. log_error("First row must be column names (array reference)");
  698. return 0;
  699. }
  700. my @columns = @$header_row;
  701. # Теперь @$data содержит только строки данных
  702. my $data_rows = $data;
  703. # Если нет данных — только заголовок
  704. unless (@$data_rows) {
  705. log_debug("No data rows to insert, only header");
  706. return 1;
  707. }
  708. my $db = init_db();
  709. my $original_autocommit = $db->{AutoCommit};
  710. $db->{AutoCommit} = 0;
  711. if (get_db_type($db) eq 'mysql') {
  712. # --- MySQL: попытка LOAD DATA, fallback на INSERT ---
  713. log_debug("Using LOAD DATA LOCAL INFILE for MySQL");
  714. my $fh = File::Temp->new(UNLINK => 1);
  715. my $fname = $fh->filename;
  716. binmode($fh, ':utf8');
  717. my $csv = Text::CSV->new({
  718. binary => 1,
  719. quote_char => '"',
  720. escape_char => '"',
  721. sep_char => ',',
  722. eol => "\r\n",
  723. always_quote => 1,
  724. }) or do {
  725. my $err = "Cannot create Text::CSV: " . Text::CSV->error_diag();
  726. log_error($err);
  727. $db->{AutoCommit} = $original_autocommit;
  728. $db->disconnect();
  729. return 0;
  730. };
  731. # Пишем заголовок
  732. $csv->print($fh, \@columns);
  733. # Пишем данные
  734. for my $row (@$data_rows) {
  735. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  736. my @vals = map { defined($_) ? $_ : 'NULL' } @$row;
  737. $csv->print($fh, \@vals);
  738. }
  739. close $fh;
  740. my $col_list = join(', ', map { $db->quote_identifier($_) } @columns);
  741. my $query = qq{LOAD DATA LOCAL INFILE '$fname' INTO TABLE $table FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\r\n' IGNORE 1 LINES ($col_list)};
  742. my $load_ok = eval { $db->do($query); 1 };
  743. if (!$load_ok) {
  744. my $err = "MySQL LOAD DATA failed: $@";
  745. log_error($err);
  746. log_debug("Falling back to bulk INSERT for MySQL");
  747. goto FALLBACK_INSERT_MYSQL;
  748. }
  749. $db->commit();
  750. $db->{AutoCommit} = $original_autocommit;
  751. $db->disconnect();
  752. return 1;
  753. # ========================
  754. # Fallback для MySQL
  755. # ========================
  756. FALLBACK_INSERT_MYSQL:
  757. {
  758. my $quoted_cols = join(', ', map { $db->quote_identifier($_) } @columns);
  759. my $placeholders = join(',', ('?') x @columns);
  760. my $sql = "INSERT INTO $table ($quoted_cols) VALUES ($placeholders)";
  761. my $sth = $db->prepare($sql);
  762. my $success = eval {
  763. for my $row (@$data_rows) {
  764. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  765. my @vals = map { defined($_) ? $_ : undef } @$row;
  766. $sth->execute(@vals);
  767. }
  768. 1;
  769. };
  770. if ($success) {
  771. $db->commit();
  772. } else {
  773. eval { $db->rollback(); };
  774. my $err = "MySQL bulk INSERT failed: $@";
  775. log_error($err);
  776. $db->{AutoCommit} = $original_autocommit;
  777. $db->disconnect();
  778. return 0;
  779. }
  780. $db->{AutoCommit} = $original_autocommit;
  781. }
  782. } elsif (get_db_type($db) eq 'pg') {
  783. if (!$db->can('pg_putcopydata') || !$db->can('pg_putcopyend')) {
  784. log_debug("pg_putcopydata/pg_putcopyend not available — falling back to bulk INSERT");
  785. goto FALLBACK_INSERT_PG;
  786. }
  787. my $col_list = join(', ', map { $db->quote_identifier($_) } @columns);
  788. my $copy_sql = "COPY $table ($col_list) FROM STDIN WITH (FORMAT CSV, HEADER true)";
  789. my $use_header_as_data;
  790. my $start_ok = eval { $db->do($copy_sql); 1 };
  791. if (!$start_ok) {
  792. log_debug("COPY with HEADER failed: $@ — trying without HEADER");
  793. $copy_sql = "COPY $table ($col_list) FROM STDIN WITH (FORMAT CSV)";
  794. $start_ok = eval { $db->do($copy_sql); 1 };
  795. if (!$start_ok) {
  796. log_debug("COPY failed entirely: $@ — falling back to bulk INSERT");
  797. goto FALLBACK_INSERT_PG;
  798. }
  799. $use_header_as_data = 1;
  800. } else {
  801. $use_header_as_data = 0;
  802. }
  803. log_debug("Using CSV COPY for PostgreSQL");
  804. my $csv = Text::CSV->new({
  805. binary => 1,
  806. quote_char => '"',
  807. escape_char => '"',
  808. sep_char => ',',
  809. eol => "\n",
  810. always_quote => 1,
  811. }) or do {
  812. my $err = "Cannot create Text::CSV: " . Text::CSV->error_diag();
  813. log_error($err);
  814. eval { $db->pg_putcopyend(); };
  815. $db->{AutoCommit} = $original_autocommit;
  816. $db->disconnect();
  817. return 0;
  818. };
  819. my $success = eval {
  820. if ($use_header_as_data) {
  821. $csv->combine(@columns);
  822. $db->pg_putcopydata($csv->string);
  823. }
  824. for my $row (@$data_rows) {
  825. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  826. my @vals = map { defined($_) ? $_ : undef } @$row;
  827. $csv->combine(@vals);
  828. $db->pg_putcopydata($csv->string);
  829. }
  830. $db->pg_putcopyend();
  831. 1;
  832. };
  833. if ($success) {
  834. $db->commit();
  835. } else {
  836. eval { $db->rollback(); };
  837. my $err = "CSV COPY failed: $@";
  838. log_error($err);
  839. eval { $db->pg_putcopyend(); };
  840. goto FALLBACK_INSERT_PG;
  841. }
  842. # ========================
  843. # Fallback для PostgreSQL
  844. # ========================
  845. FALLBACK_INSERT_PG:
  846. {
  847. my $quoted_cols = join(', ', map { $db->quote_identifier($_) } @columns);
  848. my $placeholders = join(',', ('?') x @columns);
  849. my $sql = "INSERT INTO $table ($quoted_cols) VALUES ($placeholders)";
  850. my $sth = $db->prepare($sql);
  851. my $success = eval {
  852. for my $row (@$data_rows) {
  853. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  854. my @vals = map { defined($_) ? $_ : undef } @$row;
  855. $sth->execute(@vals);
  856. }
  857. 1;
  858. };
  859. if ($success) {
  860. $db->commit();
  861. } else {
  862. eval { $db->rollback(); };
  863. my $err = "PostgreSQL bulk INSERT failed: $@";
  864. log_error($err);
  865. $db->{AutoCommit} = $original_autocommit;
  866. $db->disconnect();
  867. return 0;
  868. }
  869. }
  870. } else {
  871. my $err = "Unsupported DBTYPE: ". get_db_type($db);
  872. log_error($err);
  873. $db->{AutoCommit} = $original_autocommit;
  874. $db->disconnect();
  875. return 0;
  876. }
  877. $db->{AutoCommit} = $original_autocommit;
  878. $db->disconnect();
  879. return 1;
  880. }
  881. #---------------------------------------------------------------------------------------------------------------
  882. sub reconnect_db {
  883. my $db_ref = shift;
  884. # Если соединение активно — ничего не делаем
  885. if ($$db_ref && $$db_ref->ping) {
  886. return 1;
  887. }
  888. # Сохраняем AutoCommit из текущего соединения (если есть)
  889. my $original_autocommit = 1;
  890. if ($$db_ref) {
  891. $original_autocommit = $$db_ref->{AutoCommit};
  892. eval { $$db_ref->disconnect; };
  893. $$db_ref = undef;
  894. }
  895. # Пытаемся переподключиться
  896. eval {
  897. $$db_ref = init_db($original_autocommit);
  898. unless ($$db_ref && $$db_ref->ping) {
  899. log_die "Failed to establish database connection";
  900. }
  901. 1;
  902. } or do {
  903. my $error = $@ || 'Unknown error';
  904. $$db_ref = undef;
  905. log_die "Database reconnection failed: $error";
  906. return 0;
  907. };
  908. return 1;
  909. }
  910. #---------------------------------------------------------------------------------------------------------------
  911. sub write_db_log {
  912. my $db=shift;
  913. my $msg=shift;
  914. my $level = shift || $L_VERBOSE;
  915. my $auth_id = shift || 0;
  916. return if (!$db);
  917. return if (!$msg);
  918. $msg=~s/[\'\"]//g;
  919. my $db_log = 0;
  920. # Переподключение
  921. unless (reconnect_db(\$db)) {
  922. log_error("No database connection available");
  923. $db_log = 0;
  924. }
  925. if ($level eq $L_ERROR and $log_level >= $L_ERROR) { log_error($msg); $db_log = 1; }
  926. if ($level eq $L_WARNING and $log_level >= $L_WARNING) { log_warning($msg); $db_log = 1; }
  927. if ($level eq $L_INFO and $log_level >= $L_INFO) { log_info($msg); $db_log = 1; }
  928. if ($level eq $L_VERBOSE and $log_level >= $L_VERBOSE) { log_verbose($msg); $db_log = 1; }
  929. if ($level eq $L_DEBUG and $log_level >= $L_DEBUG) { log_debug($msg); return; }
  930. if ($db_log) {
  931. #my $new_id = do_sql($dbh, 'INSERT INTO user_list (login) VALUES (?)', 'Ivan');
  932. do_sql($db,'INSERT INTO worklog(customer,message,level,auth_id,ip) VALUES( ?, ?, ?, ?, ?)',$MY_NAME,$msg,$level,$auth_id,$config_ref{self_ip});
  933. }
  934. }
  935. #---------------------------------------------------------------------------------------------------------------
  936. sub db_log_debug {
  937. my $db = shift;
  938. my $msg = shift;
  939. my $id = shift;
  940. if ($debug) { log_debug($msg); }
  941. }
  942. #---------------------------------------------------------------------------------------------------------------
  943. sub db_log_error {
  944. my $db = shift;
  945. my $msg = shift;
  946. if ($log_level >= $L_ERROR) {
  947. sendEmail("ERROR! ".get_first_line($msg),$msg,1);
  948. write_db_log($db,$msg,$L_ERROR);
  949. }
  950. }
  951. #---------------------------------------------------------------------------------------------------------------
  952. sub db_log_info {
  953. my $db = shift;
  954. my $msg = shift;
  955. my $id = shift;
  956. if ($log_level >= $L_INFO) { write_db_log($db,$msg,$L_INFO,$id); }
  957. }
  958. #---------------------------------------------------------------------------------------------------------------
  959. sub db_log_verbose {
  960. my $db = shift;
  961. my $msg = shift;
  962. my $id = shift;
  963. if ($log_level >= $L_VERBOSE) { write_db_log($db,$msg,$L_VERBOSE,$id); }
  964. }
  965. #---------------------------------------------------------------------------------------------------------------
  966. sub db_log_warning {
  967. my $db = shift;
  968. my $msg = shift;
  969. my $id = shift;
  970. if ($log_level >= $L_WARNING) { write_db_log($db,$msg,$L_WARNING,$id); }
  971. }
  972. #---------------------------------------------------------------------------------------------------------------
  973. sub init_db {
  974. my $autocommit = shift;
  975. if (!defined $autocommit) { $autocommit = 1; }
  976. my $db;
  977. if ($config_ref{DBTYPE} eq 'mysql') {
  978. $db = DBI->connect(
  979. "dbi:mysql:database=$DBNAME;host=$DBHOST;port=3306;mysql_local_infile=1", $DBUSER, $DBPASS,
  980. { RaiseError => 0, AutoCommit => $autocommit, mysql_enable_utf8 => 1 }
  981. );
  982. if (!defined $db) {
  983. log_die "Cannot connect to MySQL server: $DBI::errstr\n";
  984. }
  985. $db->do('SET NAMES utf8mb4');
  986. } else {
  987. $db = DBI->connect(
  988. "dbi:Pg:dbname=$DBNAME;host=$DBHOST;port=5432", $DBUSER, $DBPASS,
  989. { RaiseError => 0, AutoCommit => $autocommit, pg_enable_utf8 => 1, pg_server_prepare => 0 }
  990. );
  991. if (!defined $db) {
  992. log_die "Cannot connect to PostgreSQL server: $DBI::errstr\n";
  993. }
  994. }
  995. return $db;
  996. }
  997. #---------------------------------------------------------------------------------------------------------------
  998. # Обновленная функция get_option с параметризованными запросами
  999. sub get_option {
  1000. my $db = shift;
  1001. my $option_id = shift;
  1002. return if (!$option_id);
  1003. return if (!$db);
  1004. my $sql = q{
  1005. SELECT
  1006. COALESCE(c.value, co.default_value) AS value,
  1007. co.option_type
  1008. FROM config_options co
  1009. LEFT JOIN config c ON c.option_id = co.id
  1010. WHERE co.id = ?
  1011. };
  1012. my $record = get_record_sql($db, $sql, $option_id);
  1013. unless ($record) {
  1014. log_error("Option ID $option_id not found in config_options table");
  1015. return;
  1016. }
  1017. return $record->{value};
  1018. }
  1019. #---------------------------------------------------------------------------------------------------------------
  1020. sub get_records_sql {
  1021. my ($db, $sql, @params) = @_;
  1022. my @result;
  1023. return @result if (!$db);
  1024. return @result if (!$sql);
  1025. unless (reconnect_db(\$db)) {
  1026. log_error("No database connection available");
  1027. return @result;
  1028. }
  1029. my $result_ref = _execute_param($db, $sql, \@params, { mode => 'array' });
  1030. if (ref($result_ref) eq 'ARRAY') {
  1031. @result = @$result_ref;
  1032. }
  1033. return @result;
  1034. }
  1035. #---------------------------------------------------------------------------------------------------------------
  1036. sub get_record_sql {
  1037. my ($db, $sql, @params) = @_;
  1038. my @result;
  1039. return @result if (!$db);
  1040. return @result if (!$sql);
  1041. # Добавляем LIMIT только если его еще нет в запросе
  1042. if ($sql !~ /\bLIMIT\s+\d+/i && $sql !~ /\bFETCH\s+FIRST\s+\d+/i) {
  1043. $sql .= ' LIMIT 1';
  1044. }
  1045. # Переподключение
  1046. unless (reconnect_db(\$db)) {
  1047. log_error("No database connection available");
  1048. return;
  1049. }
  1050. return _execute_param($db, $sql, \@params, { mode => 'single' });
  1051. }
  1052. #---------------------------------------------------------------------------------------------------------------
  1053. sub get_count_records {
  1054. my ($db, $table, $filter, @params) = @_;
  1055. my $result = 0;
  1056. return $result if (!$db);
  1057. return $result if (!$table);
  1058. my $sSQL='SELECT COUNT(*) as rec_cnt FROM '.$table;
  1059. if ($filter) { $sSQL=$sSQL." WHERE ".$filter; }
  1060. my $record = get_record_sql($db,$sSQL, @params);
  1061. if ($record->{rec_cnt}) { $result = $record->{rec_cnt}; }
  1062. return $result;
  1063. }
  1064. #---------------------------------------------------------------------------------------------------------------
  1065. sub get_id_record {
  1066. my ($db, $table, $filter, @params) = @_;
  1067. my $result = 0;
  1068. return $result if (!$db);
  1069. return $result if (!$table);
  1070. my $record = get_record_sql($db,"SELECT id FROM $table WHERE $filter", @params);
  1071. if ($record->{id}) { $result = $record->{id}; }
  1072. return $result;
  1073. }
  1074. #---------------------------------------------------------------------------------------------------------------
  1075. sub get_diff_rec {
  1076. my ($db, $table, $record, $filter_sql, @filter_params) = @_;
  1077. return unless $db && $table && $filter_sql;
  1078. unless (reconnect_db(\$db)) {
  1079. log_error("No database connection available");
  1080. return;
  1081. }
  1082. my $old_record = get_record_sql($db,"SELECT * FROM $table WHERE $filter_sql",@filter_params);
  1083. return unless $old_record;
  1084. my $result;
  1085. foreach my $field (keys %$record) {
  1086. if (!$record->{$field}) { $record->{$field}=''; }
  1087. if (!$old_record->{$field}) { $old_record->{$field}=''; }
  1088. if ($record->{$field}!~/^$old_record->{$field}$/) { $result->{$field} = "$record->{$field} [ old: " . $old_record->{$field} . "]"; }
  1089. }
  1090. return hash_to_text($result);
  1091. }
  1092. #---------------------------------------------------------------------------------------------------------------
  1093. sub get_db_type {
  1094. my $db = shift;
  1095. return lc($db->{Driver}->{Name});
  1096. #'mysql', 'pg'
  1097. }
  1098. #---------------------------------------------------------------------------------------------------------------
  1099. # Внутренняя функция для выполнения параметризованных запросов
  1100. sub _execute_param {
  1101. my ($db, $sql, $params, $options) = @_;
  1102. return unless $db && $sql;
  1103. my $mode = $options->{mode} || 'execute';
  1104. # --- Автоматическая поддержка RETURNING для PostgreSQL ---
  1105. my $was_modified = 0;
  1106. my $original_sql = $sql;
  1107. if ($mode eq 'id' && $sql =~ /^\s*INSERT\b/i) {
  1108. if (get_db_type($db) eq 'pg') {
  1109. unless ($sql =~ /\bRETURNING\b/i) {
  1110. $sql .= ' RETURNING id';
  1111. $was_modified = 1;
  1112. $mode = 'scalar';
  1113. }
  1114. }
  1115. }
  1116. # Логируем не-SELECT
  1117. unless ($original_sql =~ /^\s*SELECT/i) {
  1118. log_debug($original_sql . ($params ? ' | params: [' . join(', ', map { defined $_ ? $_ : 'undef' } @$params) . ']' : ''));
  1119. }
  1120. # === не переподключаемся внутри транзакции ===
  1121. my $autocommit_enabled = $db->{AutoCommit};
  1122. unless ($autocommit_enabled) {
  1123. # В транзакции: нельзя переподключаться!
  1124. unless ($db->ping) {
  1125. log_error("Database connection lost during transaction");
  1126. return wantarray ? () : undef;
  1127. }
  1128. } else {
  1129. # Вне транзакции: можно переподключиться
  1130. unless (reconnect_db(\$db)) {
  1131. log_error("No database connection available");
  1132. return wantarray ? () : undef;
  1133. }
  1134. }
  1135. my $sth = $db->prepare($sql) or do {
  1136. log_error("Unable to prepare SQL [$original_sql]: " . $db->errstr);
  1137. return wantarray ? () : undef;
  1138. };
  1139. my $rv = $params ? $sth->execute(@$params) : $sth->execute();
  1140. unless ($rv) {
  1141. log_error("Unable to execute SQL [$original_sql]" . ($params ? " with params: [" . join(', ', map { defined $_ ? $_ : 'undef' } @$params) . "]" : "") . ": " . $sth->errstr);
  1142. $sth->finish();
  1143. return wantarray ? () : undef;
  1144. }
  1145. # --- Обработка результатов ---
  1146. if ($was_modified && $mode eq 'scalar') {
  1147. my $row = $sth->fetchrow_arrayref();
  1148. $sth->finish();
  1149. my $id = $row ? $row->[0] : 0;
  1150. return $id;
  1151. }
  1152. elsif ($mode eq 'single') {
  1153. my $row = $sth->fetchrow_hashref();
  1154. $sth->finish();
  1155. return $row;
  1156. }
  1157. elsif ($mode eq 'array') {
  1158. my @rows;
  1159. while (my $row = $sth->fetchrow_hashref()) {
  1160. push @rows, $row;
  1161. }
  1162. $sth->finish();
  1163. return \@rows;
  1164. }
  1165. elsif ($mode eq 'arrayref') {
  1166. my $rows = $sth->fetchall_arrayref({});
  1167. $sth->finish();
  1168. return $rows;
  1169. }
  1170. elsif ($mode eq 'scalar') {
  1171. my $row = $sth->fetchrow_arrayref();
  1172. $sth->finish();
  1173. return $row ? $row->[0] : undef;
  1174. }
  1175. elsif ($mode eq 'id') {
  1176. if ($original_sql =~ /^\s*INSERT/i) {
  1177. my $id;
  1178. if (get_db_type($db) eq 'mysql') {
  1179. $id = $sth->{mysql_insertid};
  1180. } else {
  1181. ($id) = $db->selectrow_array("SELECT lastval()");
  1182. }
  1183. $sth->finish();
  1184. return $id || 0;
  1185. }
  1186. $sth->finish();
  1187. return 1;
  1188. }
  1189. else {
  1190. $sth->finish();
  1191. return 1;
  1192. }
  1193. }
  1194. #---------------------------------------------------------------------------------------------------------------
  1195. sub do_sql {
  1196. my ($db, $sql, @bind_values) = @_;
  1197. return unless $db && $sql; # Возвращаем undef при ошибке входных данных
  1198. my $mode;
  1199. if ($sql =~ /^\s*insert\b/i) {
  1200. $mode = 'id';
  1201. } elsif ($sql =~ /^\s*select\b/i) {
  1202. $mode = 'arrayref';
  1203. } else {
  1204. $mode = 'execute';
  1205. }
  1206. my $result = _execute_param($db, $sql, \@bind_values, { mode => $mode });
  1207. # Если _execute_param вернул undef/ложь — это ошибка
  1208. unless (defined $result) {
  1209. return; # Возвращаем undef (лучше, чем 0)
  1210. }
  1211. if ($mode eq 'id') {
  1212. return $result; # число (возможно 0 — допустимо для ID)
  1213. } elsif ($mode eq 'arrayref') {
  1214. # _execute_param всегда возвращает ARRAYREF при успехе
  1215. return $result;
  1216. } else {
  1217. # Для UPDATE/DELETE: возвращаем количество затронутых строк или 1
  1218. return $result ? $result : 1;
  1219. }
  1220. }
  1221. #---------------------------------------------------------------------------------------------------------------
  1222. sub insert_record {
  1223. my ($db, $table, $record) = @_;
  1224. return unless $db && $table && ref($record) eq 'HASH' && %$record;
  1225. # Переподключаемся ТОЛЬКО если не в транзакции
  1226. if ($db->{AutoCommit}) {
  1227. unless (reconnect_db(\$db)) {
  1228. log_error("No database connection available");
  1229. return;
  1230. }
  1231. } else {
  1232. unless ($db->ping) {
  1233. log_error("Database connection lost during transaction");
  1234. return;
  1235. }
  1236. }
  1237. my $db_info= build_db_schema($db);
  1238. my $dns_changed = 0;
  1239. my $rec_id = 0;
  1240. if ($table eq "user_auth") {
  1241. foreach my $field (keys %$record) {
  1242. if (exists $acl_fields{$field}) { $record->{changed}="1"; }
  1243. if (exists $dhcp_fields{$field}) { $record->{dhcp_changed}="1"; }
  1244. if (exists $dns_fields{$field}) { $dns_changed=1; }
  1245. }
  1246. }
  1247. my @insert_params;
  1248. my $fields = '';
  1249. my $values = '';
  1250. foreach my $field (keys %$record) {
  1251. my $val = normalize_value($record->{$field}, $db_schema{$db_info->{db_type}}{$db_info->{db_name}}{$table}{$field});
  1252. # Экранируем имя поля в зависимости от СУБД
  1253. my $quoted_field = get_db_type($db) eq 'mysql'
  1254. ? '`' . $field . '`'
  1255. : '"' . $field . '"';
  1256. $fields .= "$quoted_field, ";
  1257. $values .= "?, ";
  1258. push @insert_params, $val;
  1259. }
  1260. $fields =~ s/,\s*$//;
  1261. $values =~ s/,\s*$//;
  1262. my $sSQL = "INSERT INTO $table($fields) VALUES($values)";
  1263. my $result = do_sql($db,$sSQL,@insert_params);
  1264. if ($result) {
  1265. $rec_id = $result;
  1266. my $changed_msg = prepare_audit_message($db, $table, undef, $record, $rec_id, 'insert');
  1267. if ($table !~ /session/i) {
  1268. if (defined $changed_msg && $changed_msg ne '') {
  1269. if ($table !~ /user/i) {
  1270. db_log_info($db, $changed_msg);
  1271. } else {
  1272. if ($table eq 'user_auth' && defined $record->{ip} && $record->{ip} ne '') {
  1273. if (is_hotspot($db, $record->{ip})) {
  1274. db_log_info($db, $changed_msg, $rec_id);
  1275. } else {
  1276. db_log_warning($db, $changed_msg, $rec_id);
  1277. my $send_alert_create = isNotifyCreate(get_notify_subnet($db, $record->{ip}));
  1278. sendEmail("WARN! " . get_first_line($changed_msg), $changed_msg, 1) if $send_alert_create;
  1279. }
  1280. } else {
  1281. db_log_warning($db, $changed_msg);
  1282. }
  1283. }
  1284. }
  1285. if ($table eq 'user_auth_alias' and $dns_changed) {
  1286. if ($record->{'alias'} and $record->{'alias'}!~/\.$/) {
  1287. my $add_dns;
  1288. $add_dns->{'name_type'}='CNAME';
  1289. $add_dns->{'name'}=$record->{'alias'};
  1290. $add_dns->{'value'}=get_dns_name($db,$record->{'auth_id'});
  1291. $add_dns->{'operation_type'}='add';
  1292. $add_dns->{'auth_id'}=$record->{'auth_id'};
  1293. insert_record($db,'dns_queue',$add_dns);
  1294. }
  1295. }
  1296. if ($table eq 'user_auth' and $dns_changed) {
  1297. if ($record->{'dns_name'} and $record->{'ip'} and !$record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  1298. my $add_dns;
  1299. $add_dns->{'name_type'}='A';
  1300. $add_dns->{'name'}=$record->{'dns_name'};
  1301. $add_dns->{'value'}=$record->{'ip'};
  1302. $add_dns->{'operation_type'}='add';
  1303. $add_dns->{'auth_id'}=$result;
  1304. insert_record($db,'dns_queue',$add_dns);
  1305. }
  1306. if ($record->{'dns_name'} and $record->{'ip'} and $record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  1307. my $add_dns;
  1308. $add_dns->{'name_type'}='PTR';
  1309. $add_dns->{'name'}=$record->{'dns_name'};
  1310. $add_dns->{'value'}=$record->{'ip'};
  1311. $add_dns->{'operation_type'}='add';
  1312. $add_dns->{'auth_id'}=$result;
  1313. insert_record($db,'dns_queue',$add_dns);
  1314. }
  1315. }
  1316. }
  1317. }
  1318. return $result;
  1319. }
  1320. #---------------------------------------------------------------------------------------------------------------
  1321. sub update_record {
  1322. my ($db, $table, $record, $filter_sql, @filter_params) = @_;
  1323. return unless $db && $table && $filter_sql;
  1324. # Переподключаемся ТОЛЬКО если не в транзакции
  1325. if ($db->{AutoCommit}) {
  1326. unless (reconnect_db(\$db)) {
  1327. log_error("No database connection available");
  1328. return;
  1329. }
  1330. } else {
  1331. unless ($db->ping) {
  1332. log_error("Database connection lost during transaction");
  1333. return;
  1334. }
  1335. }
  1336. my $db_info = build_db_schema($db);
  1337. my $select_sql = "SELECT * FROM $table WHERE $filter_sql";
  1338. my $old_record = get_record_sql($db, $select_sql, @filter_params);
  1339. return unless $old_record;
  1340. my @update_params;
  1341. my $set_clause = '';
  1342. my $dns_changed = 0;
  1343. my $rec_id = $old_record->{id} || 0;
  1344. if ($table eq "user_auth") {
  1345. $rec_id = $old_record->{'id'} if ($old_record->{'id'});
  1346. my $cur_ou_id = $old_record->{'ou_id'} if ($old_record->{'ou_id'});
  1347. if (exists $record->{ou_id}) { $cur_ou_id = $record->{'ou_id'}; }
  1348. #disable update field 'created_by'
  1349. #if ($old_record->{'created_by'} and exists ($record->{'created_by'})) { delete $record->{'created_by'}; }
  1350. foreach my $field (keys %$record) {
  1351. if (exists $acl_fields{$field}) { $record->{changed}="1"; }
  1352. if (exists $dhcp_fields{$field} and !is_system_ou($db,$cur_ou_id)) { $record->{dhcp_changed}="1"; }
  1353. if (exists $dns_fields{$field}) { $dns_changed=1; }
  1354. }
  1355. }
  1356. for my $field (keys %$record) {
  1357. my $old_val = defined $old_record->{$field} ? $old_record->{$field} : '';
  1358. my $new_val = normalize_value($record->{$field}, $db_schema{$db_info->{db_type}}{$db_info->{db_name}}{$table}{$field});
  1359. if ($new_val ne $old_val) {
  1360. $set_clause .= " $field = ?, ";
  1361. push @update_params, $new_val;
  1362. }
  1363. }
  1364. return 1 unless $set_clause;
  1365. # Добавляем служебные поля
  1366. if ($table eq 'user_auth') {
  1367. $set_clause .= "changed_time = ?, ";
  1368. push @update_params, GetNowTime();
  1369. }
  1370. $set_clause =~ s/,\s*$//;
  1371. if ($table eq 'user_auth') {
  1372. if ($dns_changed) {
  1373. my $del_dns;
  1374. if ($old_record->{'dns_name'} and $old_record->{'ip'} and !$old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1375. $del_dns->{'name_type'}='A';
  1376. $del_dns->{'name'}=$old_record->{'dns_name'};
  1377. $del_dns->{'value'}=$old_record->{'ip'};
  1378. $del_dns->{'operation_type'}='del';
  1379. if ($rec_id) { $del_dns->{'auth_id'}=$rec_id; }
  1380. insert_record($db,'dns_queue',$del_dns);
  1381. }
  1382. if ($old_record->{'dns_name'} and $old_record->{'ip'} and $old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1383. $del_dns->{'name_type'}='PTR';
  1384. $del_dns->{'name'}=$old_record->{'dns_name'};
  1385. $del_dns->{'value'}=$old_record->{'ip'};
  1386. $del_dns->{'operation_type'}='del';
  1387. if ($rec_id) { $del_dns->{'auth_id'}=$rec_id; }
  1388. insert_record($db,'dns_queue',$del_dns);
  1389. }
  1390. my $new_dns;
  1391. my $dns_rec_ip = $old_record->{ip};
  1392. my $dns_rec_name = $old_record->{dns_name};
  1393. if ($record->{'dns_name'}) { $dns_rec_name = $record->{'dns_name'}; }
  1394. if ($record->{'ip'}) { $dns_rec_ip = $record->{'ip'}; }
  1395. if ($dns_rec_name and $dns_rec_ip and !$record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  1396. $new_dns->{'name_type'}='A';
  1397. $new_dns->{'name'}=$dns_rec_name;
  1398. $new_dns->{'value'}=$dns_rec_ip;
  1399. $new_dns->{'operation_type'}='add';
  1400. if ($rec_id) { $new_dns->{'auth_id'}=$rec_id; }
  1401. insert_record($db,'dns_queue',$new_dns);
  1402. }
  1403. if ($dns_rec_name and $dns_rec_ip and $record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  1404. $new_dns->{'name_type'}='PTR';
  1405. $new_dns->{'name'}=$dns_rec_name;
  1406. $new_dns->{'value'}=$dns_rec_ip;
  1407. $new_dns->{'operation_type'}='add';
  1408. if ($rec_id) { $new_dns->{'auth_id'}=$rec_id; }
  1409. insert_record($db,'dns_queue',$new_dns);
  1410. }
  1411. }
  1412. }
  1413. if ($table eq 'user_auth_alias') {
  1414. if ($dns_changed) {
  1415. my $del_dns;
  1416. if ($old_record->{'alias'} and $old_record->{'alias'}!~/\.$/) {
  1417. $del_dns->{'name_type'}='CNAME';
  1418. $del_dns->{'name'}=$old_record->{'alias'};
  1419. $del_dns->{'operation_type'}='del';
  1420. $del_dns->{'value'}=get_dns_name($db,$old_record->{auth_id});
  1421. $del_dns->{'auth_id'}=$old_record->{auth_id};
  1422. insert_record($db,'dns_queue',$del_dns);
  1423. }
  1424. my $new_dns;
  1425. my $dns_rec_name = $old_record->{alias};
  1426. if ($record->{'alias'}) { $dns_rec_name = $record->{'alias'}; }
  1427. if ($dns_rec_name and $record->{'alias'}!~/\.$/) {
  1428. $new_dns->{'name_type'}='CNAME';
  1429. $new_dns->{'name'}=$dns_rec_name;
  1430. $new_dns->{'operation_type'}='add';
  1431. $new_dns->{'value'}=get_dns_name($db,$old_record->{auth_id});
  1432. $new_dns->{'auth_id'}=$rec_id;
  1433. insert_record($db,'dns_queue',$new_dns);
  1434. }
  1435. }
  1436. }
  1437. my @all_params = (@update_params, @filter_params);
  1438. my $update_sql = "UPDATE $table SET $set_clause WHERE $filter_sql";
  1439. my $result = do_sql($db, $update_sql, @all_params);
  1440. if ($result) {
  1441. my $changed_msg = prepare_audit_message($db, $table, $old_record, $record , $rec_id, 'update');
  1442. if ($table !~ /session/i) {
  1443. if (defined $changed_msg && $changed_msg ne '') {
  1444. if ($table !~ /user/i) {
  1445. db_log_info($db, $changed_msg);
  1446. } else {
  1447. if (is_hotspot($db, $old_record->{ip})) {
  1448. db_log_info($db, $changed_msg, $rec_id);
  1449. } else {
  1450. db_log_warning($db, $changed_msg, $rec_id);
  1451. if ($table eq 'user_auth' && defined $old_record->{ip} && $old_record->{ip} ne '') {
  1452. my $send_alert_update = isNotifyUpdate(get_notify_subnet($db, $old_record->{ip}));
  1453. sendEmail("WARN! " . get_first_line($changed_msg), $changed_msg, 1) if $send_alert_update;
  1454. }
  1455. }
  1456. }
  1457. }
  1458. }
  1459. }
  1460. return $result;
  1461. }
  1462. #---------------------------------------------------------------------------------------------------------------
  1463. sub delete_record {
  1464. my ($db, $table, $filter_sql, @filter_params) = @_;
  1465. return unless $db && $table && $filter_sql;
  1466. # Переподключаемся ТОЛЬКО если не в транзакции
  1467. if ($db->{AutoCommit}) {
  1468. unless (reconnect_db(\$db)) {
  1469. log_error("No database connection available");
  1470. return;
  1471. }
  1472. } else {
  1473. unless ($db->ping) {
  1474. log_error("Database connection lost during transaction");
  1475. return;
  1476. }
  1477. }
  1478. my $select_sql = "SELECT * FROM $table WHERE $filter_sql";
  1479. my $old_record = get_record_sql($db, $select_sql, @filter_params);
  1480. return unless $old_record;
  1481. my $rec_id = $old_record->{'id'};
  1482. #never delete user ip record!
  1483. if ($table eq 'user_auth') {
  1484. my $sSQL = "UPDATE user_auth SET changed = 1, deleted = 1, changed_time = ? WHERE $filter_sql";
  1485. my $ret = do_sql($db, $sSQL, GetNowTime(), @filter_params);
  1486. if ($old_record->{'dns_name'} and $old_record->{'ip'} and !$old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1487. my $del_dns;
  1488. $del_dns->{'name_type'}='A';
  1489. $del_dns->{'name'}=$old_record->{'dns_name'};
  1490. $del_dns->{'value'}=$old_record->{'ip'};
  1491. $del_dns->{'operation_type'}='del';
  1492. $del_dns->{'auth_id'}=$old_record->{'id'};
  1493. insert_record($db,'dns_queue',$del_dns);
  1494. }
  1495. if ($old_record->{'dns_name'} and $old_record->{'ip'} and $old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1496. my $del_dns;
  1497. $del_dns->{'name_type'}='PTR';
  1498. $del_dns->{'name'}=$old_record->{'dns_name'};
  1499. $del_dns->{'value'}=$old_record->{'ip'};
  1500. $del_dns->{'operation_type'}='del';
  1501. $del_dns->{'auth_id'}=$old_record->{'id'};
  1502. insert_record($db,'dns_queue',$del_dns);
  1503. }
  1504. my $changed_msg = prepare_audit_message($db, $table, $old_record, undef , $rec_id, 'delete');
  1505. if ($ret) {
  1506. if (defined $changed_msg && $changed_msg ne '') {
  1507. if (defined $old_record->{ip} && $old_record->{ip} ne '') {
  1508. if (is_hotspot($db, $old_record->{ip})) {
  1509. db_log_info($db, $changed_msg, $rec_id);
  1510. } else {
  1511. db_log_warning($db, $changed_msg, $rec_id);
  1512. my $send_alert_delete = isNotifyDelete(get_notify_subnet($db, $old_record->{ip}));
  1513. sendEmail("WARN! " . get_first_line($changed_msg), $changed_msg, 1) if $send_alert_delete;
  1514. }
  1515. }
  1516. }
  1517. }
  1518. return $ret;
  1519. }
  1520. if ($table eq 'user_list' and $old_record->{'permanent'}) { return; }
  1521. if ($table eq 'user_auth_alias') {
  1522. if ($old_record->{'alias'} and $old_record->{'auth_id'} and $old_record->{'alias'}!~/\.$/) {
  1523. my $del_dns;
  1524. $del_dns->{'name_type'}='CNAME';
  1525. $del_dns->{'name'}=$old_record->{'alias'};
  1526. $del_dns->{'value'}=get_dns_name($db,$old_record->{'auth_id'});
  1527. $del_dns->{'operation_type'}='del';
  1528. $del_dns->{'auth_id'}=$old_record->{'auth_id'};
  1529. insert_record($db,'dns_queue',$del_dns);
  1530. }
  1531. }
  1532. my $sSQL = "DELETE FROM ".$table." WHERE ".$filter_sql;
  1533. my $result = do_sql($db,$sSQL,@filter_params);
  1534. my $changed_msg = prepare_audit_message($db, $table, $old_record, undef , $rec_id, 'delete');
  1535. if ($result && $table !~ /session/i) {
  1536. if (defined $changed_msg && $changed_msg ne '') {
  1537. if ($table !~ /user/i) {
  1538. db_log_info($db, $changed_msg);
  1539. } else {
  1540. db_log_warning($db, $changed_msg);
  1541. }
  1542. }
  1543. }
  1544. return $result;
  1545. }
  1546. #---------------------------------------------------------------------------------------------------------------
  1547. sub is_system_ou {
  1548. my ($db, $ou_id) = @_;
  1549. return 0 if !defined $ou_id || $ou_id !~ /^\d+$/ || $ou_id <= 0;
  1550. my $sql = "SELECT 1 FROM ou WHERE id = ? AND (default_users = 1 OR default_hotspot = 1)";
  1551. my $record = get_record_sql($db, $sql, $ou_id);
  1552. return $record ? 1 : 0;
  1553. }
  1554. #---------------------------------------------------------------------------------------------------------------
  1555. sub init_option {
  1556. my $db=shift;
  1557. $last_refresh_config = time();
  1558. $config_ref{version}='';
  1559. my $version_record = get_record_sql($db,"SELECT version FROM version WHERE version is NOT NULL");
  1560. if ($version_record) { $config_ref{version}=$version_record->{version}; }
  1561. $config_ref{self_ip} = '127.0.0.1';
  1562. if ($DBHOST ne '127.0.0.1') {
  1563. my $ip_route = qx(ip r get $DBHOST 2>&1 | head -1);
  1564. if ($? == 0) {
  1565. if ($ip_route =~ /src\s+(\d+\.\d+\.\d+\.\d+)/) { $config_ref{self_ip} = $1; }
  1566. }
  1567. }
  1568. $config_ref{dbh}=$db;
  1569. $config_ref{save_detail}=get_option($db,23);
  1570. $config_ref{add_unknown_user}=get_option($db,22);
  1571. $config_ref{dhcp_server}=get_option($db,5);
  1572. $config_ref{snmp_default_version}=get_option($db,9);
  1573. $config_ref{snmp_default_community}=get_option($db,11);
  1574. $config_ref{KB}=get_option($db,1);
  1575. if ($config_ref{KB} ==0) { $config_ref{KB}=1000; }
  1576. if ($config_ref{KB} ==1) { $config_ref{KB}=1024; }
  1577. $config_ref{admin_email}=get_option($db,21);
  1578. $config_ref{sender_email}=get_option($db,52);
  1579. $config_ref{send_email}=get_option($db,51);
  1580. $config_ref{history}=get_option($db,26);
  1581. $config_ref{history_dhcp}=get_option($db,27);
  1582. $config_ref{router_login}=get_option($db,28);
  1583. $config_ref{router_password}=get_option($db,29);
  1584. $config_ref{router_port}=get_option($db,30);
  1585. $config_ref{org_name}=get_option($db,32);
  1586. $config_ref{domain_name}=get_option($db,33);
  1587. $config_ref{connections_history}=get_option($db,35);
  1588. $config_ref{debug}=get_option($db,34);
  1589. $config_ref{log_level} = get_option($db,53);
  1590. if ($config_ref{debug}) { $config_ref{log_level} = 255; }
  1591. $config_ref{urgent_sync}=get_option($db,50);
  1592. $config_ref{ignore_hotspot_dhcp_log} = get_option($db,44);
  1593. $config_ref{ignore_update_dhcp_event} = get_option($db,45);
  1594. $config_ref{update_hostname_from_dhcp} = get_option($db,46);
  1595. $config_ref{history_log_day}=get_option($db,47);
  1596. $config_ref{history_syslog_day} = get_option($db,48);
  1597. $config_ref{history_trafstat_day} = get_option($db,49);
  1598. $config_ref{enable_quotes} = get_option($db,54);
  1599. $config_ref{netflow_step} = get_option($db,55);
  1600. $config_ref{traffic_ipstat_history} = get_option($db,56);
  1601. $config_ref{nagios_url} = get_option($db,57);
  1602. $config_ref{cacti_url} = get_option($db,58);
  1603. $config_ref{torrus_url} = get_option($db,59);
  1604. $config_ref{wiki_url} = get_option($db,60);
  1605. $config_ref{stat_url} = get_option($db,62);
  1606. $config_ref{wiki_path} = get_option($db,61);
  1607. $config_ref{auto_mac_rule} = get_option($db,64);
  1608. #network configuration mode
  1609. $config_ref{config_mode}=get_option($db,68);
  1610. #auto clean old user record
  1611. $config_ref{clean_empty_user}=get_option($db,69);
  1612. #dns_server_type
  1613. $config_ref{dns_server}=get_option($db,3);
  1614. $config_ref{dns_server_type}=get_option($db,70);
  1615. $config_ref{enable_dns_updates}=get_option($db,71);
  1616. #$save_detail = 1; id=23
  1617. $save_detail=get_option($db,23);
  1618. #$add_unknown_user = 1; id=22
  1619. $add_unknown_user=get_option($db,22);
  1620. #$dns_server='192.168.2.12'; id=3
  1621. $dns_server=get_option($db,3);
  1622. #$dhcp_server='192.168.2.12'; id=5
  1623. $dhcp_server=get_option($db,5);
  1624. #$snmp_default_version='2'; id=9
  1625. $snmp_default_version=get_option($db,9);
  1626. #$snmp_default_community='public'; id=11
  1627. $snmp_default_community=get_option($db,11);
  1628. #$KB=1024; id=1
  1629. $KB=$config_ref{KB};
  1630. #$admin_email; id=21
  1631. $admin_email=get_option($db,21);
  1632. #sender email
  1633. $sender_email=get_option($db,52);
  1634. #send email
  1635. $send_email=get_option($db,51);
  1636. #$history=15; id=26
  1637. $history=get_option($db,26);
  1638. #$history_dhcp=7; id=27
  1639. $history_dhcp=get_option($db,27);
  1640. #$router_login="admin"; id=28
  1641. $router_login=get_option($db,28);
  1642. #$router_password="admin"; id=29
  1643. $router_password=get_option($db,29);
  1644. #$router_port=23; id=30
  1645. $router_port=get_option($db,30);
  1646. #32
  1647. $org_name=get_option($db,32);
  1648. #33
  1649. $domain_name=get_option($db,33);
  1650. #35
  1651. $connections_history=get_option($db,35);
  1652. #debug
  1653. $debug=get_option($db,34);
  1654. #log level
  1655. $log_level = get_option($db,53);
  1656. if ($debug) { $log_level = 255; }
  1657. #urgent sync access
  1658. $urgent_sync=get_option($db,50);
  1659. $ignore_hotspot_dhcp_log = get_option($db,44);
  1660. $ignore_update_dhcp_event = get_option($db,45);
  1661. $update_hostname_from_dhcp = get_option($db,46);
  1662. $history_log_day=get_option($db,47);
  1663. $history_syslog_day = get_option($db,48);
  1664. $history_trafstat_day = get_option($db,49);
  1665. my $ou = get_record_sql($db,"SELECT id FROM ou WHERE default_users = 1");
  1666. if (!$ou) { $default_user_ou_id = 0; } else { $default_user_ou_id = $ou->{'id'}; }
  1667. $ou = get_record_sql($db,"SELECT id FROM ou WHERE default_hotspot = 1 ");
  1668. if (!$ou) { $default_hotspot_ou_id = $default_user_ou_id; } else { $default_hotspot_ou_id = $ou->{'id'}; }
  1669. @subnets=get_records_sql($db,'SELECT * FROM subnets ORDER BY ip_int_start');
  1670. if (defined $office_networks) { undef $office_networks; }
  1671. if (defined $free_networks) { undef $free_networks; }
  1672. if (defined $vpn_networks) { undef $vpn_networks; }
  1673. if (defined $hotspot_networks) { undef $hotspot_networks; }
  1674. if (defined $all_networks) { undef $all_networks; }
  1675. $office_networks = new Net::Patricia;
  1676. $free_networks = new Net::Patricia;
  1677. $vpn_networks = new Net::Patricia;
  1678. $hotspot_networks = new Net::Patricia;
  1679. $all_networks = new Net::Patricia;
  1680. @office_network_list=();
  1681. @free_network_list=();
  1682. @free_network_list=();
  1683. @vpn_network_list=();
  1684. @hotspot_network_list=();
  1685. @all_network_list=();
  1686. foreach my $net (@subnets) {
  1687. next if (!$net->{subnet});
  1688. $subnets_ref{$net->{subnet}}=$net;
  1689. if ($net->{office}) {
  1690. push(@office_network_list,$net->{subnet});
  1691. $office_networks->add_string($net->{subnet},$net);
  1692. }
  1693. if ($net->{free}) {
  1694. push(@free_network_list,$net->{subnet});
  1695. $free_networks->add_string($net->{subnet},$net);
  1696. }
  1697. if ($net->{vpn}) {
  1698. push(@vpn_network_list,$net->{subnet});
  1699. $vpn_networks->add_string($net->{subnet},$net);
  1700. }
  1701. if ($net->{hotspot}) {
  1702. push(@hotspot_network_list,$net->{subnet});
  1703. push(@all_network_list,$net->{subnet});
  1704. $hotspot_networks->add_string($net->{subnet},$net);
  1705. }
  1706. push(@all_network_list,$net->{subnet});
  1707. $all_networks->add_string($net->{subnet},$net);
  1708. }
  1709. }
  1710. #---------------------------------------------------------------------------------------------------------------
  1711. sub Set_Variable {
  1712. my ($db, $name, $value, $timeshift) = @_;
  1713. $name //= $MY_NAME;
  1714. $value //= $$;
  1715. $timeshift //= 60;
  1716. Del_Variable($db, $name);
  1717. my $clean_time = time() + $timeshift;
  1718. my ($sec, $min, $hour, $day, $month, $year) = localtime($clean_time);
  1719. $month++;
  1720. $year += 1900;
  1721. my $clear_time_str = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $day, $hour, $min, $sec;
  1722. my $sql = "INSERT INTO variables (name, value, clear_time) VALUES (?, ?, ?)";
  1723. do_sql($db, $sql, $name, $value, $clear_time_str);
  1724. }
  1725. #---------------------------------------------------------------------------------------------------------------
  1726. sub Get_Variable {
  1727. my $db = shift;
  1728. my $name = shift || $MY_NAME;
  1729. my $variable = get_record_sql($db, 'SELECT value FROM variables WHERE name = ?', $name);
  1730. if ($variable and $variable->{'value'}) { return $variable->{'value'}; }
  1731. return;
  1732. }
  1733. #---------------------------------------------------------------------------------------------------------------
  1734. sub Del_Variable {
  1735. my ($db, $name) = @_;
  1736. $name //= $MY_NAME;
  1737. do_sql($db, "DELETE FROM variables WHERE name = ?", $name);
  1738. }
  1739. #---------------------------------------------------------------------------------------------------------------
  1740. sub clean_variables {
  1741. my ($db) = @_;
  1742. # 1. Clean temporary variables
  1743. my $now = time();
  1744. my ($sec, $min, $hour, $day, $month, $year) = localtime($now);
  1745. $month++;
  1746. $year += 1900;
  1747. my $now_str = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $day, $hour, $min, $sec;
  1748. do_sql($db, "DELETE FROM variables WHERE clear_time <= ?", $now_str);
  1749. # 2. Clean old AD computer cache
  1750. my $yesterday = DateTime->now(time_zone => 'local')->subtract(days => 1);
  1751. my $clean_str = $yesterday->strftime("%Y-%m-%d 00:00:00");
  1752. do_sql($db, "DELETE FROM ad_comp_cache WHERE last_found <= ?", $clean_str);
  1753. }
  1754. #---------------------------------------------------------------------------------------------------------------
  1755. #skip init for upgrade
  1756. if ($MY_NAME!~/upgrade.pl/) {
  1757. $dbh=init_db();
  1758. init_option($dbh);
  1759. clean_variables($dbh);
  1760. Set_Variable($dbh);
  1761. warn "DBI driver name: ", $dbh->{Driver}->{Name}, "\n" if ($debug);
  1762. warn "Full dbh class: ", ref($dbh), "\n" if ($debug);
  1763. }
  1764. 1;
  1765. }