database.pm 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510
  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. StrToIp
  42. IpToStr
  43. batch_db_sql_cached
  44. batch_db_sql_csv
  45. reconnect_db
  46. write_db_log
  47. db_log_debug
  48. db_log_error
  49. db_log_info
  50. db_log_verbose
  51. db_log_warning
  52. normalize_value
  53. get_table_columns
  54. init_db
  55. do_sql
  56. _execute_param
  57. do_sql_param
  58. get_option_safe
  59. get_count_records
  60. get_id_record
  61. get_records_sql
  62. get_record_sql
  63. get_diff_rec
  64. update_record
  65. insert_record
  66. delete_record
  67. get_option
  68. init_option
  69. is_system_ou
  70. Set_Variable
  71. Get_Variable
  72. Del_Variable
  73. clean_variables
  74. build_db_schema
  75. $add_rules
  76. $L_WARNING
  77. $L_INFO
  78. $L_DEBUG
  79. $L_ERROR
  80. $L_VERBOSE
  81. %db_schema
  82. );
  83. BEGIN
  84. {
  85. #---------------------------------------------------------------------------------------------------------------
  86. our $add_rules;
  87. our $L_ERROR = 0;
  88. our $L_WARNING = 1;
  89. our $L_INFO = 2;
  90. our $L_VERBOSE = 3;
  91. our $L_DEBUG = 255;
  92. our %acl_fields = (
  93. 'ip' => '1',
  94. 'ip_int' => '1',
  95. 'enabled'=>'1',
  96. 'dhcp'=>'1',
  97. 'filter_group_id'=>'1',
  98. 'deleted'=>'1',
  99. 'dhcp_acl'=>'1',
  100. 'queue_id'=>'1',
  101. 'mac'=>'1',
  102. 'blocked'=>'1'
  103. );
  104. our %dhcp_fields = (
  105. 'ip' => '1',
  106. 'dhcp_acl'=>'1',
  107. 'dhcp_option_set'=>'1',
  108. 'dhcp'=>'1',
  109. 'deleted'=>'1',
  110. 'mac'=>'1',
  111. );
  112. our %dns_fields = (
  113. 'ip' => '1',
  114. 'dns_name'=>'1',
  115. 'dns_ptr_only'=>'1',
  116. 'alias'=>'1',
  117. );
  118. our %db_schema;
  119. #---------------------------------------------------------------------------------------------------------------
  120. sub build_db_schema {
  121. my ($dbh) = @_;
  122. # Определяем тип СУБД
  123. my $db_type = lc($dbh->{Driver}->{Name});
  124. die "Unsupported database driver: $db_type"
  125. unless $db_type eq 'mysql' || $db_type eq 'pg';
  126. # Получаем имя базы данных
  127. my $db_name;
  128. if ($db_type eq 'mysql') {
  129. ($db_name) = $dbh->selectrow_array("SELECT DATABASE()");
  130. } elsif ($db_type eq 'pg') {
  131. ($db_name) = $dbh->selectrow_array("SELECT current_database()");
  132. }
  133. my $db_info;
  134. $db_info->{db_type}=$db_type;
  135. $db_info->{db_name}=$db_name;
  136. return $db_info if (exists $db_schema{$db_type}{$db_name});
  137. # Получаем список таблиц
  138. my @tables;
  139. if ($db_type eq 'mysql') {
  140. my $sth = $dbh->prepare("SHOW TABLES");
  141. $sth->execute();
  142. @tables = map { $_->[0] } @{$sth->fetchall_arrayref()};
  143. } elsif ($db_type eq 'pg') {
  144. my $sql = q{
  145. SELECT tablename
  146. FROM pg_tables
  147. WHERE schemaname = 'public'
  148. };
  149. my $sth = $dbh->prepare($sql);
  150. $sth->execute();
  151. @tables = map { $_->[0] } @{$sth->fetchall_arrayref()};
  152. }
  153. # Собираем схему
  154. for my $table (@tables) {
  155. my $sth = $dbh->column_info(undef, undef, $table, '%');
  156. while (my $col = $sth->fetchrow_hashref) {
  157. my $col_name = lc($col->{COLUMN_NAME});
  158. $db_schema{$db_type}{$db_name}{$table}{$col_name} = {
  159. type => $col->{TYPE_NAME} // '',
  160. nullable => $col->{NULLABLE} // 1,
  161. default => $col->{COLUMN_DEF} // undef,
  162. };
  163. }
  164. }
  165. return $db_info;
  166. }
  167. #---------------------------------------------------------------------------------------------------------------
  168. sub normalize_value {
  169. my ($value, $col_info) = @_;
  170. # Если значение пустое — обрабатываем по правилам колонки
  171. if (!defined $value || $value eq '' || $value =~ /^(?:NULL|\\N)$/i) {
  172. return $col_info->{nullable} ? undef : _default_for_type($col_info);
  173. }
  174. my $type = lc($col_info->{type});
  175. # --- Числовые типы: приводим к числу, если выглядит как число ---
  176. if ($type =~ /^(?:tinyint|smallint|mediumint|int|integer|bigint|serial|bigserial)$/i) {
  177. # Просто конвертируем строку в число (Perl сам обрежет мусор)
  178. # Например: "123abc" → 123, "abc" → 0
  179. return 0 + $value;
  180. }
  181. # --- Булевы: приводим к 0/1 ---
  182. if ($type =~ /^(?:bool|boolean|bit)$/i) {
  183. return $value ? 1 : 0;
  184. }
  185. # --- Временные типы: оставляем как есть, но фильтруем "нулевые" даты MySQL ---
  186. if ($type =~ /^(?:timestamp|datetime|date|time)$/i) {
  187. # Это частая проблема при миграции — '0000-00-00' ломает PostgreSQL
  188. return undef if $value =~ /^0000-00-00/;
  189. return $value;
  190. }
  191. # --- Все остальные типы (строки, inet, json и т.д.) — передаём как есть ---
  192. return $value;
  193. }
  194. # Вспомогательная: безопасное значение по умолчанию
  195. sub _default_for_type {
  196. my ($col) = @_;
  197. # Используем DEFAULT, только если он простой литерал (не выражение)
  198. if (defined $col->{default}) {
  199. my $def = $col->{default};
  200. # Пропускаем выражения: nextval(), CURRENT_TIMESTAMP, NOW(), uuid() и т.п.
  201. if ($def !~ /(nextval|current_timestamp|now|uuid|auto_increment|::)/i) {
  202. # Убираем одинарные кавычки, если строка: 'value' → value
  203. if ($def =~ /^'(.*)'$/) {
  204. return $1;
  205. }
  206. # Если похоже на число — вернём как число
  207. if ($def =~ /^[+-]?\d+$/) {
  208. return 0 + $def;
  209. }
  210. return $def;
  211. }
  212. }
  213. # Фолбэк по типу
  214. my $type = lc($col->{type});
  215. if ($type =~ /^(?:tinyint|smallint|int|integer|bigint)/i) { return 0; }
  216. if ($type =~ /^(?:char|varchar|text)/i) { return ''; }
  217. if ($type =~ /^(?:timestamp|datetime)/i) { return GetNowTime(); }
  218. return undef;
  219. }
  220. #---------------------------------------------------------------------------------------------------------------
  221. sub get_table_columns {
  222. my ($db, $table) = @_;
  223. my %columns;
  224. my $sth = $db->column_info(undef, undef, $table, '%');
  225. while (my $row = $sth->fetchrow_hashref) {
  226. my $name = lc($row->{COLUMN_NAME}); # ← приводим к нижнему регистру сразу!
  227. $columns{$name} = {
  228. type => $row->{TYPE_NAME} // '',
  229. nullable => $row->{NULLABLE} // 1,
  230. default => $row->{COLUMN_DEF} // undef,
  231. };
  232. }
  233. return %columns; # возвращает список: key1, val1, key2, val2...
  234. }
  235. #---------------------------------------------------------------------------------------------------------------
  236. sub StrToIp {
  237. return unpack('N',pack('C4',split(/\./,$_[0])));
  238. }
  239. #---------------------------------------------------------------------------------------------------------------
  240. sub IpToStr {
  241. my $nIP = shift;
  242. my $res = (($nIP>>24) & 255) .".". (($nIP>>16) & 255) .".". (($nIP>>8) & 255) .".". ($nIP & 255);
  243. return $res;
  244. }
  245. #---------------------------------------------------------------------------------------------------------------
  246. sub batch_db_sql_cached {
  247. my ( $sql, $data) = @_;
  248. my $db=init_db();
  249. # Запоминаем исходное состояние AutoCommit
  250. my $original_autocommit = $db->{AutoCommit};
  251. eval {
  252. # Выключаем AutoCommit для транзакции
  253. $db->{AutoCommit} = 0;
  254. my $sth = $db->prepare_cached($sql) or die "Unable to prepare SQL: " . $db->errstr;
  255. for my $params (@$data) {
  256. next unless @$params;
  257. $sth->execute(@$params) or die "Unable to execute with params [" . join(',', @$params) . "]: " . $sth->errstr;
  258. }
  259. $db->commit();
  260. 1;
  261. } or do {
  262. my $err = $@ || 'Unknown error';
  263. eval { $db->rollback() };
  264. warn "batch_sql_cached failed: $err";
  265. # Восстанавливаем AutoCommit даже при ошибке
  266. $db->{AutoCommit} = $original_autocommit;
  267. return 0;
  268. };
  269. # Восстанавливаем исходный режим AutoCommit
  270. $db->{AutoCommit} = $original_autocommit;
  271. $db->disconnect();
  272. return 1;
  273. }
  274. #---------------------------------------------------------------------------------------------------------------
  275. sub batch_db_sql_csv {
  276. my ($table, $data) = @_;
  277. return 0 unless @$data;
  278. # Первая строка — заголовки (имена столбцов)
  279. my $header_row = shift @$data;
  280. unless ($header_row && ref($header_row) eq 'ARRAY' && @$header_row) {
  281. log_error("First row must be column names (array reference)");
  282. return 0;
  283. }
  284. my @columns = @$header_row;
  285. # Теперь @$data содержит только строки данных
  286. my $data_rows = $data;
  287. # Если нет данных — только заголовок
  288. unless (@$data_rows) {
  289. log_debug("No data rows to insert, only header");
  290. return 1;
  291. }
  292. my $db = init_db();
  293. my $original_autocommit = $db->{AutoCommit};
  294. $db->{AutoCommit} = 0;
  295. if (get_db_type($db) eq 'mysql') {
  296. # --- MySQL: попытка LOAD DATA, fallback на INSERT ---
  297. log_debug("Using LOAD DATA LOCAL INFILE for MySQL");
  298. my $fh = File::Temp->new(UNLINK => 1);
  299. my $fname = $fh->filename;
  300. binmode($fh, ':utf8');
  301. my $csv = Text::CSV->new({
  302. binary => 1,
  303. quote_char => '"',
  304. escape_char => '"',
  305. sep_char => ',',
  306. eol => "\r\n",
  307. always_quote => 1,
  308. }) or do {
  309. my $err = "Cannot create Text::CSV: " . Text::CSV->error_diag();
  310. log_error($err);
  311. $db->{AutoCommit} = $original_autocommit;
  312. $db->disconnect();
  313. return 0;
  314. };
  315. # Пишем заголовок
  316. $csv->print($fh, \@columns);
  317. # Пишем данные
  318. for my $row (@$data_rows) {
  319. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  320. my @vals = map { defined($_) ? $_ : 'NULL' } @$row;
  321. $csv->print($fh, \@vals);
  322. }
  323. close $fh;
  324. my $col_list = join(', ', map { $db->quote_identifier($_) } @columns);
  325. 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)};
  326. my $load_ok = eval { $db->do($query); 1 };
  327. if (!$load_ok) {
  328. my $err = "MySQL LOAD DATA failed: $@";
  329. log_error($err);
  330. log_debug("Falling back to bulk INSERT for MySQL");
  331. goto FALLBACK_INSERT_MYSQL;
  332. }
  333. $db->commit();
  334. $db->{AutoCommit} = $original_autocommit;
  335. $db->disconnect();
  336. return 1;
  337. # ========================
  338. # Fallback для MySQL
  339. # ========================
  340. FALLBACK_INSERT_MYSQL:
  341. {
  342. my $quoted_cols = join(', ', map { $db->quote_identifier($_) } @columns);
  343. my $placeholders = join(',', ('?') x @columns);
  344. my $sql = "INSERT INTO $table ($quoted_cols) VALUES ($placeholders)";
  345. my $sth = $db->prepare($sql);
  346. my $success = eval {
  347. for my $row (@$data_rows) {
  348. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  349. my @vals = map { defined($_) ? $_ : undef } @$row;
  350. $sth->execute(@vals);
  351. }
  352. 1;
  353. };
  354. if ($success) {
  355. $db->commit();
  356. } else {
  357. eval { $db->rollback(); };
  358. my $err = "MySQL bulk INSERT failed: $@";
  359. log_error($err);
  360. $db->{AutoCommit} = $original_autocommit;
  361. $db->disconnect();
  362. return 0;
  363. }
  364. $db->{AutoCommit} = $original_autocommit;
  365. }
  366. } elsif (get_db_type($db) eq 'pg') {
  367. if (!$db->can('pg_putcopydata') || !$db->can('pg_putcopyend')) {
  368. log_debug("pg_putcopydata/pg_putcopyend not available — falling back to bulk INSERT");
  369. goto FALLBACK_INSERT_PG;
  370. }
  371. my $col_list = join(', ', map { $db->quote_identifier($_) } @columns);
  372. my $copy_sql = "COPY $table ($col_list) FROM STDIN WITH (FORMAT CSV, HEADER true)";
  373. my $use_header_as_data;
  374. my $start_ok = eval { $db->do($copy_sql); 1 };
  375. if (!$start_ok) {
  376. log_debug("COPY with HEADER failed: $@ — trying without HEADER");
  377. $copy_sql = "COPY $table ($col_list) FROM STDIN WITH (FORMAT CSV)";
  378. $start_ok = eval { $db->do($copy_sql); 1 };
  379. if (!$start_ok) {
  380. log_debug("COPY failed entirely: $@ — falling back to bulk INSERT");
  381. goto FALLBACK_INSERT_PG;
  382. }
  383. $use_header_as_data = 1;
  384. } else {
  385. $use_header_as_data = 0;
  386. }
  387. log_debug("Using CSV COPY for PostgreSQL");
  388. my $csv = Text::CSV->new({
  389. binary => 1,
  390. quote_char => '"',
  391. escape_char => '"',
  392. sep_char => ',',
  393. eol => "\n",
  394. always_quote => 1,
  395. }) or do {
  396. my $err = "Cannot create Text::CSV: " . Text::CSV->error_diag();
  397. log_error($err);
  398. eval { $db->pg_putcopyend(); };
  399. $db->{AutoCommit} = $original_autocommit;
  400. $db->disconnect();
  401. return 0;
  402. };
  403. my $success = eval {
  404. if ($use_header_as_data) {
  405. $csv->combine(@columns);
  406. $db->pg_putcopydata($csv->string);
  407. }
  408. for my $row (@$data_rows) {
  409. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  410. my @vals = map { defined($_) ? $_ : undef } @$row;
  411. $csv->combine(@vals);
  412. $db->pg_putcopydata($csv->string);
  413. }
  414. $db->pg_putcopyend();
  415. 1;
  416. };
  417. if ($success) {
  418. $db->commit();
  419. } else {
  420. eval { $db->rollback(); };
  421. my $err = "CSV COPY failed: $@";
  422. log_error($err);
  423. eval { $db->pg_putcopyend(); };
  424. goto FALLBACK_INSERT_PG;
  425. }
  426. # ========================
  427. # Fallback для PostgreSQL
  428. # ========================
  429. FALLBACK_INSERT_PG:
  430. {
  431. my $quoted_cols = join(', ', map { $db->quote_identifier($_) } @columns);
  432. my $placeholders = join(',', ('?') x @columns);
  433. my $sql = "INSERT INTO $table ($quoted_cols) VALUES ($placeholders)";
  434. my $sth = $db->prepare($sql);
  435. my $success = eval {
  436. for my $row (@$data_rows) {
  437. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  438. my @vals = map { defined($_) ? $_ : undef } @$row;
  439. $sth->execute(@vals);
  440. }
  441. 1;
  442. };
  443. if ($success) {
  444. $db->commit();
  445. } else {
  446. eval { $db->rollback(); };
  447. my $err = "PostgreSQL bulk INSERT failed: $@";
  448. log_error($err);
  449. $db->{AutoCommit} = $original_autocommit;
  450. $db->disconnect();
  451. return 0;
  452. }
  453. }
  454. } else {
  455. my $err = "Unsupported DBTYPE: ". get_db_type($db);
  456. log_error($err);
  457. $db->{AutoCommit} = $original_autocommit;
  458. $db->disconnect();
  459. return 0;
  460. }
  461. $db->{AutoCommit} = $original_autocommit;
  462. $db->disconnect();
  463. return 1;
  464. }
  465. #---------------------------------------------------------------------------------------------------------------
  466. sub reconnect_db {
  467. my $db_ref = shift;
  468. # Если соединение активно — ничего не делаем
  469. if ($$db_ref && $$db_ref->ping) {
  470. return 1;
  471. }
  472. # Сохраняем AutoCommit из текущего соединения (если есть)
  473. my $original_autocommit = 1;
  474. if ($$db_ref) {
  475. $original_autocommit = $$db_ref->{AutoCommit};
  476. eval { $$db_ref->disconnect; };
  477. $$db_ref = undef;
  478. }
  479. # Пытаемся переподключиться
  480. eval {
  481. $$db_ref = init_db($original_autocommit);
  482. unless ($$db_ref && $$db_ref->ping) {
  483. log_die "Failed to establish database connection";
  484. }
  485. 1;
  486. } or do {
  487. my $error = $@ || 'Unknown error';
  488. $$db_ref = undef;
  489. log_die "Database reconnection failed: $error";
  490. return 0;
  491. };
  492. return 1;
  493. }
  494. #---------------------------------------------------------------------------------------------------------------
  495. sub write_db_log {
  496. my $db=shift;
  497. my $msg=shift;
  498. my $level = shift || $L_VERBOSE;
  499. my $auth_id = shift || 0;
  500. return if (!$db);
  501. return if (!$msg);
  502. $msg=~s/[\'\"]//g;
  503. my $db_log = 0;
  504. # Переподключение
  505. unless (reconnect_db(\$db)) {
  506. log_error("No database connection available");
  507. $db_log = 0;
  508. }
  509. if ($level eq $L_ERROR and $log_level >= $L_ERROR) { log_error($msg); $db_log = 1; }
  510. if ($level eq $L_WARNING and $log_level >= $L_WARNING) { log_warning($msg); $db_log = 1; }
  511. if ($level eq $L_INFO and $log_level >= $L_INFO) { log_info($msg); $db_log = 1; }
  512. if ($level eq $L_VERBOSE and $log_level >= $L_VERBOSE) { log_verbose($msg); $db_log = 1; }
  513. if ($level eq $L_DEBUG and $log_level >= $L_DEBUG) { log_debug($msg); return; }
  514. if ($db_log) {
  515. #my $new_id = do_sql($dbh, 'INSERT INTO user_list (login) VALUES (?)', 'Ivan');
  516. do_sql($db,'INSERT INTO worklog(customer,message,level,auth_id,ip) VALUES( ?, ?, ?, ?, ?)',$MY_NAME,$msg,$level,$auth_id,$config_ref{self_ip});
  517. }
  518. }
  519. #---------------------------------------------------------------------------------------------------------------
  520. sub db_log_debug {
  521. my $db = shift;
  522. my $msg = shift;
  523. my $id = shift;
  524. if ($debug) { log_debug($msg); }
  525. }
  526. #---------------------------------------------------------------------------------------------------------------
  527. sub db_log_error {
  528. my $db = shift;
  529. my $msg = shift;
  530. if ($log_level >= $L_ERROR) {
  531. sendEmail("ERROR! ".get_first_line($msg),$msg,1);
  532. write_db_log($db,$msg,$L_ERROR);
  533. }
  534. }
  535. #---------------------------------------------------------------------------------------------------------------
  536. sub db_log_info {
  537. my $db = shift;
  538. my $msg = shift;
  539. my $id = shift;
  540. if ($log_level >= $L_INFO) { write_db_log($db,$msg,$L_INFO,$id); }
  541. }
  542. #---------------------------------------------------------------------------------------------------------------
  543. sub db_log_verbose {
  544. my $db = shift;
  545. my $msg = shift;
  546. my $id = shift;
  547. if ($log_level >= $L_VERBOSE) { write_db_log($db,$msg,$L_VERBOSE,$id); }
  548. }
  549. #---------------------------------------------------------------------------------------------------------------
  550. sub db_log_warning {
  551. my $db = shift;
  552. my $msg = shift;
  553. my $id = shift;
  554. if ($log_level >= $L_WARNING) { write_db_log($db,$msg,$L_WARNING,$id); }
  555. }
  556. #---------------------------------------------------------------------------------------------------------------
  557. sub init_db {
  558. my $autocommit = shift;
  559. if (!defined $autocommit) { $autocommit = 1; }
  560. my $db;
  561. if ($config_ref{DBTYPE} eq 'mysql') {
  562. $db = DBI->connect(
  563. "dbi:mysql:database=$DBNAME;host=$DBHOST;port=3306;mysql_local_infile=1", $DBUSER, $DBPASS,
  564. { RaiseError => 0, AutoCommit => $autocommit, mysql_enable_utf8 => 1 }
  565. );
  566. if (!defined $db) {
  567. log_die "Cannot connect to MySQL server: $DBI::errstr\n";
  568. }
  569. $db->do('SET NAMES utf8mb4');
  570. } else {
  571. $db = DBI->connect(
  572. "dbi:Pg:dbname=$DBNAME;host=$DBHOST;port=5432", $DBUSER, $DBPASS,
  573. { RaiseError => 0, AutoCommit => $autocommit, pg_enable_utf8 => 1, pg_server_prepare => 0 }
  574. );
  575. if (!defined $db) {
  576. log_die "Cannot connect to PostgreSQL server: $DBI::errstr\n";
  577. }
  578. }
  579. return $db;
  580. }
  581. #---------------------------------------------------------------------------------------------------------------
  582. # Обновленная функция get_option с параметризованными запросами
  583. sub get_option {
  584. my $db = shift;
  585. my $option_id = shift;
  586. return if (!$option_id);
  587. return if (!$db);
  588. my $sql = q{
  589. SELECT
  590. COALESCE(c.value, co.default_value) AS value,
  591. co.option_type
  592. FROM config_options co
  593. LEFT JOIN config c ON c.option_id = co.id
  594. WHERE co.id = ?
  595. };
  596. my $record = get_record_sql($db, $sql, $option_id);
  597. unless ($record) {
  598. log_error("Option ID $option_id not found in config_options table");
  599. return;
  600. }
  601. return $record->{value};
  602. }
  603. #---------------------------------------------------------------------------------------------------------------
  604. sub get_records_sql {
  605. my ($db, $sql, @params) = @_;
  606. my @result;
  607. return @result if (!$db);
  608. return @result if (!$sql);
  609. unless (reconnect_db(\$db)) {
  610. log_error("No database connection available");
  611. return @result;
  612. }
  613. my $result_ref = _execute_param($db, $sql, \@params, { mode => 'array' });
  614. if (ref($result_ref) eq 'ARRAY') {
  615. @result = @$result_ref;
  616. }
  617. return @result;
  618. }
  619. #---------------------------------------------------------------------------------------------------------------
  620. sub get_record_sql {
  621. my ($db, $sql, @params) = @_;
  622. my @result;
  623. return @result if (!$db);
  624. return @result if (!$sql);
  625. # Добавляем LIMIT только если его еще нет в запросе
  626. if ($sql !~ /\bLIMIT\s+\d+/i && $sql !~ /\bFETCH\s+FIRST\s+\d+/i) {
  627. $sql .= ' LIMIT 1';
  628. }
  629. # Переподключение
  630. unless (reconnect_db(\$db)) {
  631. log_error("No database connection available");
  632. return;
  633. }
  634. return _execute_param($db, $sql, \@params, { mode => 'single' });
  635. }
  636. #---------------------------------------------------------------------------------------------------------------
  637. sub get_count_records {
  638. my ($db, $table, $filter, @params) = @_;
  639. my $result = 0;
  640. return $result if (!$db);
  641. return $result if (!$table);
  642. my $sSQL='SELECT COUNT(*) as rec_cnt FROM '.$table;
  643. if ($filter) { $sSQL=$sSQL." WHERE ".$filter; }
  644. my $record = get_record_sql($db,$sSQL, @params);
  645. if ($record->{rec_cnt}) { $result = $record->{rec_cnt}; }
  646. return $result;
  647. }
  648. #---------------------------------------------------------------------------------------------------------------
  649. sub get_id_record {
  650. my ($db, $table, $filter, @params) = @_;
  651. my $result = 0;
  652. return $result if (!$db);
  653. return $result if (!$table);
  654. my $record = get_record_sql($db,"SELECT id FROM $table WHERE $filter", @params);
  655. if ($record->{id}) { $result = $record->{id}; }
  656. return $result;
  657. }
  658. #---------------------------------------------------------------------------------------------------------------
  659. sub get_diff_rec {
  660. my ($db, $table, $record, $filter_sql, @filter_params) = @_;
  661. return unless $db && $table && $filter_sql;
  662. unless (reconnect_db(\$db)) {
  663. log_error("No database connection available");
  664. return;
  665. }
  666. my $old_record = get_record_sql($db,"SELECT * FROM $table WHERE $filter_sql",@filter_params);
  667. return unless $old_record;
  668. my $result;
  669. foreach my $field (keys %$record) {
  670. if (!$record->{$field}) { $record->{$field}=''; }
  671. if (!$old_record->{$field}) { $old_record->{$field}=''; }
  672. if ($record->{$field}!~/^$old_record->{$field}$/) { $result->{$field} = "$record->{$field} [ old: " . $old_record->{$field} . "]"; }
  673. }
  674. return hash_to_text($result);
  675. }
  676. #---------------------------------------------------------------------------------------------------------------
  677. sub get_db_type {
  678. my $db = shift;
  679. return lc($db->{Driver}->{Name});
  680. #'mysql', 'pg'
  681. }
  682. #---------------------------------------------------------------------------------------------------------------
  683. # Внутренняя функция для выполнения параметризованных запросов
  684. sub _execute_param {
  685. my ($db, $sql, $params, $options) = @_;
  686. return unless $db && $sql;
  687. my $mode = $options->{mode} || 'execute';
  688. # --- Автоматическая поддержка RETURNING для PostgreSQL ---
  689. my $was_modified = 0;
  690. my $original_sql = $sql;
  691. if ($mode eq 'id' && $sql =~ /^\s*INSERT\b/i) {
  692. if (get_db_type($db) eq 'pg') {
  693. unless ($sql =~ /\bRETURNING\b/i) {
  694. $sql .= ' RETURNING id';
  695. $was_modified = 1;
  696. $mode = 'scalar';
  697. }
  698. }
  699. }
  700. # Логируем не-SELECT
  701. unless ($original_sql =~ /^\s*SELECT/i) {
  702. log_debug($original_sql . ($params ? ' | params: [' . join(', ', map { defined $_ ? $_ : 'undef' } @$params) . ']' : ''));
  703. }
  704. # === не переподключаемся внутри транзакции ===
  705. my $autocommit_enabled = $db->{AutoCommit};
  706. unless ($autocommit_enabled) {
  707. # В транзакции: нельзя переподключаться!
  708. unless ($db->ping) {
  709. log_error("Database connection lost during transaction");
  710. return wantarray ? () : undef;
  711. }
  712. } else {
  713. # Вне транзакции: можно переподключиться
  714. unless (reconnect_db(\$db)) {
  715. log_error("No database connection available");
  716. return wantarray ? () : undef;
  717. }
  718. }
  719. my $sth = $db->prepare($sql) or do {
  720. log_error("Unable to prepare SQL [$original_sql]: " . $db->errstr);
  721. return wantarray ? () : undef;
  722. };
  723. my $rv = $params ? $sth->execute(@$params) : $sth->execute();
  724. unless ($rv) {
  725. log_error("Unable to execute SQL [$original_sql]" . ($params ? " with params: [" . join(', ', map { defined $_ ? $_ : 'undef' } @$params) . "]" : "") . ": " . $sth->errstr);
  726. $sth->finish();
  727. return wantarray ? () : undef;
  728. }
  729. # --- Обработка результатов ---
  730. if ($was_modified && $mode eq 'scalar') {
  731. my $row = $sth->fetchrow_arrayref();
  732. $sth->finish();
  733. my $id = $row ? $row->[0] : 0;
  734. return $id;
  735. }
  736. elsif ($mode eq 'single') {
  737. my $row = $sth->fetchrow_hashref();
  738. $sth->finish();
  739. return $row;
  740. }
  741. elsif ($mode eq 'array') {
  742. my @rows;
  743. while (my $row = $sth->fetchrow_hashref()) {
  744. push @rows, $row;
  745. }
  746. $sth->finish();
  747. return \@rows;
  748. }
  749. elsif ($mode eq 'arrayref') {
  750. my $rows = $sth->fetchall_arrayref({});
  751. $sth->finish();
  752. return $rows;
  753. }
  754. elsif ($mode eq 'scalar') {
  755. my $row = $sth->fetchrow_arrayref();
  756. $sth->finish();
  757. return $row ? $row->[0] : undef;
  758. }
  759. elsif ($mode eq 'id') {
  760. if ($original_sql =~ /^\s*INSERT/i) {
  761. my $id;
  762. if (get_db_type($db) eq 'mysql') {
  763. $id = $sth->{mysql_insertid};
  764. } else {
  765. ($id) = $db->selectrow_array("SELECT lastval()");
  766. }
  767. $sth->finish();
  768. return $id || 0;
  769. }
  770. $sth->finish();
  771. return 1;
  772. }
  773. else {
  774. $sth->finish();
  775. return 1;
  776. }
  777. }
  778. #---------------------------------------------------------------------------------------------------------------
  779. sub do_sql {
  780. my ($db, $sql, @bind_values) = @_;
  781. return unless $db && $sql; # Возвращаем undef при ошибке входных данных
  782. my $mode;
  783. if ($sql =~ /^\s*insert\b/i) {
  784. $mode = 'id';
  785. } elsif ($sql =~ /^\s*select\b/i) {
  786. $mode = 'arrayref';
  787. } else {
  788. $mode = 'execute';
  789. }
  790. my $result = _execute_param($db, $sql, \@bind_values, { mode => $mode });
  791. # Если _execute_param вернул undef/ложь — это ошибка
  792. unless (defined $result) {
  793. return; # Возвращаем undef (лучше, чем 0)
  794. }
  795. if ($mode eq 'id') {
  796. return $result; # число (возможно 0 — допустимо для ID)
  797. } elsif ($mode eq 'arrayref') {
  798. # _execute_param всегда возвращает ARRAYREF при успехе
  799. return $result;
  800. } else {
  801. # Для UPDATE/DELETE: возвращаем количество затронутых строк или 1
  802. return $result ? $result : 1;
  803. }
  804. }
  805. #---------------------------------------------------------------------------------------------------------------
  806. sub insert_record {
  807. my ($db, $table, $record) = @_;
  808. return unless $db && $table && ref($record) eq 'HASH' && %$record;
  809. # Переподключаемся ТОЛЬКО если не в транзакции
  810. if ($db->{AutoCommit}) {
  811. unless (reconnect_db(\$db)) {
  812. log_error("No database connection available");
  813. return;
  814. }
  815. } else {
  816. unless ($db->ping) {
  817. log_error("Database connection lost during transaction");
  818. return;
  819. }
  820. }
  821. my $db_info= build_db_schema($db);
  822. my $dns_changed = 0;
  823. my $rec_id = 0;
  824. if ($table eq "user_auth") {
  825. foreach my $field (keys %$record) {
  826. if (exists $acl_fields{$field}) { $record->{changed}="1"; }
  827. if (exists $dhcp_fields{$field}) { $record->{dhcp_changed}="1"; }
  828. if (exists $dns_fields{$field}) { $dns_changed=1; }
  829. }
  830. }
  831. my @insert_params;
  832. my $fields = '';
  833. my $values = '';
  834. my $new_str = '';
  835. foreach my $field (keys %$record) {
  836. my $val = normalize_value($record->{$field}, $db_schema{$db_info->{db_type}}{$db_info->{db_name}}{$table}{$field});
  837. # Экранируем имя поля в зависимости от СУБД
  838. my $quoted_field = get_db_type($db) eq 'mysql'
  839. ? '`' . $field . '`'
  840. : '"' . $field . '"';
  841. $fields .= "$quoted_field, ";
  842. $values .= "?, ";
  843. push @insert_params, $val;
  844. # Для лога — безопасное представление
  845. my $log_val = defined $val ? substr($val, 0, 200) : 'NULL';
  846. $log_val =~ s/[^[:print:]]/_/g;
  847. $new_str .= " $field => $log_val,";
  848. }
  849. $fields =~ s/,\s*$//;
  850. $values =~ s/,\s*$//;
  851. my $sSQL = "INSERT INTO $table($fields) VALUES($values)";
  852. my $result = do_sql($db,$sSQL,@insert_params);
  853. if ($result) {
  854. $rec_id = $result if ($table eq "user_auth");
  855. $new_str='id: '.$result.' '.$new_str;
  856. if ($table eq 'user_auth_alias' and $dns_changed) {
  857. if ($record->{'alias'} and $record->{'alias'}!~/\.$/) {
  858. my $add_dns;
  859. $add_dns->{'name_type'}='CNAME';
  860. $add_dns->{'name'}=$record->{'alias'};
  861. $add_dns->{'value'}=get_dns_name($db,$record->{'auth_id'});
  862. $add_dns->{'operation_type'}='add';
  863. $add_dns->{'auth_id'}=$record->{'auth_id'};
  864. insert_record($db,'dns_queue',$add_dns);
  865. }
  866. }
  867. if ($table eq 'user_auth' and $dns_changed) {
  868. if ($record->{'dns_name'} and $record->{'ip'} and !$record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  869. my $add_dns;
  870. $add_dns->{'name_type'}='A';
  871. $add_dns->{'name'}=$record->{'dns_name'};
  872. $add_dns->{'value'}=$record->{'ip'};
  873. $add_dns->{'operation_type'}='add';
  874. $add_dns->{'auth_id'}=$result;
  875. insert_record($db,'dns_queue',$add_dns);
  876. }
  877. if ($record->{'dns_name'} and $record->{'ip'} and $record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  878. my $add_dns;
  879. $add_dns->{'name_type'}='PTR';
  880. $add_dns->{'name'}=$record->{'dns_name'};
  881. $add_dns->{'value'}=$record->{'ip'};
  882. $add_dns->{'operation_type'}='add';
  883. $add_dns->{'auth_id'}=$result;
  884. insert_record($db,'dns_queue',$add_dns);
  885. }
  886. }
  887. }
  888. db_log_debug($db,'Add record to table '.$table.' '.$new_str,$rec_id);
  889. return $result;
  890. }
  891. #---------------------------------------------------------------------------------------------------------------
  892. sub update_record {
  893. my ($db, $table, $record, $filter_sql, @filter_params) = @_;
  894. return unless $db && $table && $filter_sql;
  895. # Переподключаемся ТОЛЬКО если не в транзакции
  896. if ($db->{AutoCommit}) {
  897. unless (reconnect_db(\$db)) {
  898. log_error("No database connection available");
  899. return;
  900. }
  901. } else {
  902. unless ($db->ping) {
  903. log_error("Database connection lost during transaction");
  904. return;
  905. }
  906. }
  907. my $db_info = build_db_schema($db);
  908. my $select_sql = "SELECT * FROM $table WHERE $filter_sql";
  909. my $old_record = get_record_sql($db, $select_sql, @filter_params);
  910. return unless $old_record;
  911. my @update_params;
  912. my $set_clause = '';
  913. my $dns_changed = 0;
  914. my $rec_id = $old_record->{id} || 0;
  915. if ($table eq "user_auth") {
  916. $rec_id = $old_record->{'id'} if ($old_record->{'id'});
  917. my $cur_ou_id = $old_record->{'ou_id'} if ($old_record->{'ou_id'});
  918. if (exists $record->{ou_id}) { $cur_ou_id = $record->{'ou_id'}; }
  919. #disable update field 'created_by'
  920. #if ($old_record->{'created_by'} and exists ($record->{'created_by'})) { delete $record->{'created_by'}; }
  921. foreach my $field (keys %$record) {
  922. if (exists $acl_fields{$field}) { $record->{changed}="1"; }
  923. if (exists $dhcp_fields{$field} and !is_system_ou($db,$cur_ou_id)) { $record->{dhcp_changed}="1"; }
  924. if (exists $dns_fields{$field}) { $dns_changed=1; }
  925. }
  926. }
  927. my $diff = '';
  928. for my $field (keys %$record) {
  929. my $old_val = defined $old_record->{$field} ? $old_record->{$field} : '';
  930. my $new_val = normalize_value($record->{$field}, $db_schema{$db_info->{db_type}}{$db_info->{db_name}}{$table}{$field});
  931. if ($new_val ne $old_val) {
  932. $diff .= " $field => $new_val (old: $old_val),";
  933. $set_clause .= " $field = ?, ";
  934. push @update_params, $new_val;
  935. }
  936. }
  937. return 1 unless $set_clause;
  938. # Добавляем служебные поля
  939. if ($table eq 'user_auth') {
  940. $set_clause .= "changed_time = ?, ";
  941. push @update_params, GetNowTime();
  942. }
  943. $set_clause =~ s/,\s*$//;
  944. $diff =~ s/,\s*$//;
  945. if ($table eq 'user_auth') {
  946. if ($dns_changed) {
  947. my $del_dns;
  948. if ($old_record->{'dns_name'} and $old_record->{'ip'} and !$old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  949. $del_dns->{'name_type'}='A';
  950. $del_dns->{'name'}=$old_record->{'dns_name'};
  951. $del_dns->{'value'}=$old_record->{'ip'};
  952. $del_dns->{'operation_type'}='del';
  953. if ($rec_id) { $del_dns->{'auth_id'}=$rec_id; }
  954. insert_record($db,'dns_queue',$del_dns);
  955. }
  956. if ($old_record->{'dns_name'} and $old_record->{'ip'} and $old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  957. $del_dns->{'name_type'}='PTR';
  958. $del_dns->{'name'}=$old_record->{'dns_name'};
  959. $del_dns->{'value'}=$old_record->{'ip'};
  960. $del_dns->{'operation_type'}='del';
  961. if ($rec_id) { $del_dns->{'auth_id'}=$rec_id; }
  962. insert_record($db,'dns_queue',$del_dns);
  963. }
  964. my $new_dns;
  965. my $dns_rec_ip = $old_record->{ip};
  966. my $dns_rec_name = $old_record->{dns_name};
  967. if ($record->{'dns_name'}) { $dns_rec_name = $record->{'dns_name'}; }
  968. if ($record->{'ip'}) { $dns_rec_ip = $record->{'ip'}; }
  969. if ($dns_rec_name and $dns_rec_ip and !$record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  970. $new_dns->{'name_type'}='A';
  971. $new_dns->{'name'}=$dns_rec_name;
  972. $new_dns->{'value'}=$dns_rec_ip;
  973. $new_dns->{'operation_type'}='add';
  974. if ($rec_id) { $new_dns->{'auth_id'}=$rec_id; }
  975. insert_record($db,'dns_queue',$new_dns);
  976. }
  977. if ($dns_rec_name and $dns_rec_ip and $record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  978. $new_dns->{'name_type'}='PTR';
  979. $new_dns->{'name'}=$dns_rec_name;
  980. $new_dns->{'value'}=$dns_rec_ip;
  981. $new_dns->{'operation_type'}='add';
  982. if ($rec_id) { $new_dns->{'auth_id'}=$rec_id; }
  983. insert_record($db,'dns_queue',$new_dns);
  984. }
  985. }
  986. }
  987. if ($table eq 'user_auth_alias') {
  988. if ($dns_changed) {
  989. my $del_dns;
  990. if ($old_record->{'alias'} and $old_record->{'alias'}!~/\.$/) {
  991. $del_dns->{'name_type'}='CNAME';
  992. $del_dns->{'name'}=$old_record->{'alias'};
  993. $del_dns->{'operation_type'}='del';
  994. $del_dns->{'value'}=get_dns_name($db,$old_record->{auth_id});
  995. $del_dns->{'auth_id'}=$old_record->{auth_id};
  996. insert_record($db,'dns_queue',$del_dns);
  997. }
  998. my $new_dns;
  999. my $dns_rec_name = $old_record->{alias};
  1000. if ($record->{'alias'}) { $dns_rec_name = $record->{'alias'}; }
  1001. if ($dns_rec_name and $record->{'alias'}!~/\.$/) {
  1002. $new_dns->{'name_type'}='CNAME';
  1003. $new_dns->{'name'}=$dns_rec_name;
  1004. $new_dns->{'operation_type'}='add';
  1005. $new_dns->{'value'}=get_dns_name($db,$old_record->{auth_id});
  1006. $new_dns->{'auth_id'}=$rec_id;
  1007. insert_record($db,'dns_queue',$new_dns);
  1008. }
  1009. }
  1010. }
  1011. # Формируем полный список параметров: сначала SET, потом WHERE
  1012. my @all_params = (@update_params, @filter_params);
  1013. my $update_sql = "UPDATE $table SET $set_clause WHERE $filter_sql";
  1014. db_log_debug($db, "Change table $table for $filter_sql set: $diff", $rec_id);
  1015. return do_sql($db, $update_sql, @all_params);
  1016. }
  1017. #---------------------------------------------------------------------------------------------------------------
  1018. sub delete_record {
  1019. my ($db, $table, $filter_sql, @filter_params) = @_;
  1020. return unless $db && $table && $filter_sql;
  1021. # Переподключаемся ТОЛЬКО если не в транзакции
  1022. if ($db->{AutoCommit}) {
  1023. unless (reconnect_db(\$db)) {
  1024. log_error("No database connection available");
  1025. return;
  1026. }
  1027. } else {
  1028. unless ($db->ping) {
  1029. log_error("Database connection lost during transaction");
  1030. return;
  1031. }
  1032. }
  1033. my $select_sql = "SELECT * FROM $table WHERE $filter_sql";
  1034. my $old_record = get_record_sql($db, $select_sql, @filter_params);
  1035. return unless $old_record;
  1036. my $rec_id = 0;
  1037. my $diff='';
  1038. foreach my $field (keys %$old_record) {
  1039. next if (!$old_record->{$field});
  1040. $diff = $diff." $field => $old_record->{$field},";
  1041. }
  1042. $diff=~s/,\s*$//;
  1043. if ($table eq 'user_auth') {
  1044. $rec_id = $old_record->{'id'} if ($old_record->{'id'});
  1045. }
  1046. db_log_debug($db,'Delete record from table '.$table.' value: '.$diff, $rec_id);
  1047. #never delete user ip record!
  1048. if ($table eq 'user_auth') {
  1049. my $sSQL = "UPDATE user_auth SET changed = 1, deleted = 1, changed_time = ? WHERE $filter_sql";
  1050. my $ret = do_sql($db, $sSQL, GetNowTime(), @filter_params);
  1051. if ($old_record->{'dns_name'} and $old_record->{'ip'} and !$old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1052. my $del_dns;
  1053. $del_dns->{'name_type'}='A';
  1054. $del_dns->{'name'}=$old_record->{'dns_name'};
  1055. $del_dns->{'value'}=$old_record->{'ip'};
  1056. $del_dns->{'operation_type'}='del';
  1057. $del_dns->{'auth_id'}=$old_record->{'id'};
  1058. insert_record($db,'dns_queue',$del_dns);
  1059. }
  1060. if ($old_record->{'dns_name'} and $old_record->{'ip'} and $old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1061. my $del_dns;
  1062. $del_dns->{'name_type'}='PTR';
  1063. $del_dns->{'name'}=$old_record->{'dns_name'};
  1064. $del_dns->{'value'}=$old_record->{'ip'};
  1065. $del_dns->{'operation_type'}='del';
  1066. $del_dns->{'auth_id'}=$old_record->{'id'};
  1067. insert_record($db,'dns_queue',$del_dns);
  1068. }
  1069. return $ret;
  1070. }
  1071. if ($table eq 'user_list' and $old_record->{'permanent'}) { return; }
  1072. if ($table eq 'user_auth_alias') {
  1073. if ($old_record->{'alias'} and $old_record->{'auth_id'} and $old_record->{'alias'}!~/\.$/) {
  1074. my $del_dns;
  1075. $del_dns->{'name_type'}='CNAME';
  1076. $del_dns->{'name'}=$old_record->{'alias'};
  1077. $del_dns->{'value'}=get_dns_name($db,$old_record->{'auth_id'});
  1078. $del_dns->{'operation_type'}='del';
  1079. $del_dns->{'auth_id'}=$old_record->{'auth_id'};
  1080. insert_record($db,'dns_queue',$del_dns);
  1081. }
  1082. }
  1083. my $sSQL = "DELETE FROM ".$table." WHERE ".$filter_sql;
  1084. return do_sql($db,$sSQL,@filter_params);
  1085. }
  1086. #---------------------------------------------------------------------------------------------------------------
  1087. sub is_system_ou {
  1088. my ($db, $ou_id) = @_;
  1089. return 0 if !defined $ou_id || $ou_id !~ /^\d+$/ || $ou_id <= 0;
  1090. my $sql = "SELECT 1 FROM ou WHERE id = ? AND (default_users = 1 OR default_hotspot = 1)";
  1091. my $record = get_record_sql($db, $sql, $ou_id);
  1092. return $record ? 1 : 0;
  1093. }
  1094. #---------------------------------------------------------------------------------------------------------------
  1095. sub init_option {
  1096. my $db=shift;
  1097. $last_refresh_config = time();
  1098. $config_ref{version}='';
  1099. my $version_record = get_record_sql($db,"SELECT version FROM version WHERE version is NOT NULL");
  1100. if ($version_record) { $config_ref{version}=$version_record->{version}; }
  1101. $config_ref{self_ip} = '127.0.0.1';
  1102. if ($DBHOST ne '127.0.0.1') {
  1103. my $ip_route = qx(ip r get $DBHOST 2>&1 | head -1);
  1104. if ($? == 0) {
  1105. if ($ip_route =~ /src\s+(\d+\.\d+\.\d+\.\d+)/) { $config_ref{self_ip} = $1; }
  1106. }
  1107. }
  1108. $config_ref{dbh}=$db;
  1109. $config_ref{save_detail}=get_option($db,23);
  1110. $config_ref{add_unknown_user}=get_option($db,22);
  1111. $config_ref{dhcp_server}=get_option($db,5);
  1112. $config_ref{snmp_default_version}=get_option($db,9);
  1113. $config_ref{snmp_default_community}=get_option($db,11);
  1114. $config_ref{KB}=get_option($db,1);
  1115. if ($config_ref{KB} ==0) { $config_ref{KB}=1000; }
  1116. if ($config_ref{KB} ==1) { $config_ref{KB}=1024; }
  1117. $config_ref{admin_email}=get_option($db,21);
  1118. $config_ref{sender_email}=get_option($db,52);
  1119. $config_ref{send_email}=get_option($db,51);
  1120. $config_ref{history}=get_option($db,26);
  1121. $config_ref{history_dhcp}=get_option($db,27);
  1122. $config_ref{router_login}=get_option($db,28);
  1123. $config_ref{router_password}=get_option($db,29);
  1124. $config_ref{router_port}=get_option($db,30);
  1125. $config_ref{org_name}=get_option($db,32);
  1126. $config_ref{domain_name}=get_option($db,33);
  1127. $config_ref{connections_history}=get_option($db,35);
  1128. $config_ref{debug}=get_option($db,34);
  1129. $config_ref{log_level} = get_option($db,53);
  1130. if ($config_ref{debug}) { $config_ref{log_level} = 255; }
  1131. $config_ref{urgent_sync}=get_option($db,50);
  1132. $config_ref{ignore_hotspot_dhcp_log} = get_option($db,44);
  1133. $config_ref{ignore_update_dhcp_event} = get_option($db,45);
  1134. $config_ref{update_hostname_from_dhcp} = get_option($db,46);
  1135. $config_ref{history_log_day}=get_option($db,47);
  1136. $config_ref{history_syslog_day} = get_option($db,48);
  1137. $config_ref{history_trafstat_day} = get_option($db,49);
  1138. $config_ref{enable_quotes} = get_option($db,54);
  1139. $config_ref{netflow_step} = get_option($db,55);
  1140. $config_ref{traffic_ipstat_history} = get_option($db,56);
  1141. $config_ref{nagios_url} = get_option($db,57);
  1142. $config_ref{cacti_url} = get_option($db,58);
  1143. $config_ref{torrus_url} = get_option($db,59);
  1144. $config_ref{wiki_url} = get_option($db,60);
  1145. $config_ref{stat_url} = get_option($db,62);
  1146. $config_ref{wiki_path} = get_option($db,61);
  1147. $config_ref{auto_mac_rule} = get_option($db,64);
  1148. #network configuration mode
  1149. $config_ref{config_mode}=get_option($db,68);
  1150. #auto clean old user record
  1151. $config_ref{clean_empty_user}=get_option($db,69);
  1152. #dns_server_type
  1153. $config_ref{dns_server}=get_option($db,3);
  1154. $config_ref{dns_server_type}=get_option($db,70);
  1155. $config_ref{enable_dns_updates}=get_option($db,71);
  1156. #$save_detail = 1; id=23
  1157. $save_detail=get_option($db,23);
  1158. #$add_unknown_user = 1; id=22
  1159. $add_unknown_user=get_option($db,22);
  1160. #$dns_server='192.168.2.12'; id=3
  1161. $dns_server=get_option($db,3);
  1162. #$dhcp_server='192.168.2.12'; id=5
  1163. $dhcp_server=get_option($db,5);
  1164. #$snmp_default_version='2'; id=9
  1165. $snmp_default_version=get_option($db,9);
  1166. #$snmp_default_community='public'; id=11
  1167. $snmp_default_community=get_option($db,11);
  1168. #$KB=1024; id=1
  1169. $KB=$config_ref{KB};
  1170. #$admin_email; id=21
  1171. $admin_email=get_option($db,21);
  1172. #sender email
  1173. $sender_email=get_option($db,52);
  1174. #send email
  1175. $send_email=get_option($db,51);
  1176. #$history=15; id=26
  1177. $history=get_option($db,26);
  1178. #$history_dhcp=7; id=27
  1179. $history_dhcp=get_option($db,27);
  1180. #$router_login="admin"; id=28
  1181. $router_login=get_option($db,28);
  1182. #$router_password="admin"; id=29
  1183. $router_password=get_option($db,29);
  1184. #$router_port=23; id=30
  1185. $router_port=get_option($db,30);
  1186. #32
  1187. $org_name=get_option($db,32);
  1188. #33
  1189. $domain_name=get_option($db,33);
  1190. #35
  1191. $connections_history=get_option($db,35);
  1192. #debug
  1193. $debug=get_option($db,34);
  1194. #log level
  1195. $log_level = get_option($db,53);
  1196. if ($debug) { $log_level = 255; }
  1197. #urgent sync access
  1198. $urgent_sync=get_option($db,50);
  1199. $ignore_hotspot_dhcp_log = get_option($db,44);
  1200. $ignore_update_dhcp_event = get_option($db,45);
  1201. $update_hostname_from_dhcp = get_option($db,46);
  1202. $history_log_day=get_option($db,47);
  1203. $history_syslog_day = get_option($db,48);
  1204. $history_trafstat_day = get_option($db,49);
  1205. my $ou = get_record_sql($db,"SELECT id FROM ou WHERE default_users = 1");
  1206. if (!$ou) { $default_user_ou_id = 0; } else { $default_user_ou_id = $ou->{'id'}; }
  1207. $ou = get_record_sql($db,"SELECT id FROM ou WHERE default_hotspot = 1 ");
  1208. if (!$ou) { $default_hotspot_ou_id = $default_user_ou_id; } else { $default_hotspot_ou_id = $ou->{'id'}; }
  1209. @subnets=get_records_sql($db,'SELECT * FROM subnets ORDER BY ip_int_start');
  1210. if (defined $office_networks) { undef $office_networks; }
  1211. if (defined $free_networks) { undef $free_networks; }
  1212. if (defined $vpn_networks) { undef $vpn_networks; }
  1213. if (defined $hotspot_networks) { undef $hotspot_networks; }
  1214. if (defined $all_networks) { undef $all_networks; }
  1215. $office_networks = new Net::Patricia;
  1216. $free_networks = new Net::Patricia;
  1217. $vpn_networks = new Net::Patricia;
  1218. $hotspot_networks = new Net::Patricia;
  1219. $all_networks = new Net::Patricia;
  1220. @office_network_list=();
  1221. @free_network_list=();
  1222. @free_network_list=();
  1223. @vpn_network_list=();
  1224. @hotspot_network_list=();
  1225. @all_network_list=();
  1226. foreach my $net (@subnets) {
  1227. next if (!$net->{subnet});
  1228. $subnets_ref{$net->{subnet}}=$net;
  1229. if ($net->{office}) {
  1230. push(@office_network_list,$net->{subnet});
  1231. $office_networks->add_string($net->{subnet},$net);
  1232. }
  1233. if ($net->{free}) {
  1234. push(@free_network_list,$net->{subnet});
  1235. $free_networks->add_string($net->{subnet},$net);
  1236. }
  1237. if ($net->{vpn}) {
  1238. push(@vpn_network_list,$net->{subnet});
  1239. $vpn_networks->add_string($net->{subnet},$net);
  1240. }
  1241. if ($net->{hotspot}) {
  1242. push(@hotspot_network_list,$net->{subnet});
  1243. push(@all_network_list,$net->{subnet});
  1244. $hotspot_networks->add_string($net->{subnet},$net);
  1245. }
  1246. push(@all_network_list,$net->{subnet});
  1247. $all_networks->add_string($net->{subnet},$net);
  1248. }
  1249. }
  1250. #---------------------------------------------------------------------------------------------------------------
  1251. sub Set_Variable {
  1252. my ($db, $name, $value, $timeshift) = @_;
  1253. $name //= $MY_NAME;
  1254. $value //= $$;
  1255. $timeshift //= 60;
  1256. Del_Variable($db, $name);
  1257. my $clean_time = time() + $timeshift;
  1258. my ($sec, $min, $hour, $day, $month, $year) = localtime($clean_time);
  1259. $month++;
  1260. $year += 1900;
  1261. my $clear_time_str = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $day, $hour, $min, $sec;
  1262. my $sql = "INSERT INTO variables (name, value, clear_time) VALUES (?, ?, ?)";
  1263. do_sql($db, $sql, $name, $value, $clear_time_str);
  1264. }
  1265. #---------------------------------------------------------------------------------------------------------------
  1266. sub Get_Variable {
  1267. my $db = shift;
  1268. my $name = shift || $MY_NAME;
  1269. my $variable = get_record_sql($db, 'SELECT value FROM variables WHERE name = ?', $name);
  1270. if ($variable and $variable->{'value'}) { return $variable->{'value'}; }
  1271. return;
  1272. }
  1273. #---------------------------------------------------------------------------------------------------------------
  1274. sub Del_Variable {
  1275. my ($db, $name) = @_;
  1276. $name //= $MY_NAME;
  1277. do_sql($db, "DELETE FROM variables WHERE name = ?", $name);
  1278. }
  1279. #---------------------------------------------------------------------------------------------------------------
  1280. sub clean_variables {
  1281. my ($db) = @_;
  1282. # 1. Clean temporary variables
  1283. my $now = time();
  1284. my ($sec, $min, $hour, $day, $month, $year) = localtime($now);
  1285. $month++;
  1286. $year += 1900;
  1287. my $now_str = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $day, $hour, $min, $sec;
  1288. do_sql($db, "DELETE FROM variables WHERE clear_time <= ?", $now_str);
  1289. # 2. Clean old AD computer cache
  1290. my $yesterday = DateTime->now(time_zone => 'local')->subtract(days => 1);
  1291. my $clean_str = $yesterday->strftime("%Y-%m-%d 00:00:00");
  1292. do_sql($db, "DELETE FROM ad_comp_cache WHERE last_found <= ?", $clean_str);
  1293. }
  1294. #---------------------------------------------------------------------------------------------------------------
  1295. #skip init for upgrade
  1296. if ($MY_NAME!~/upgrade.pl/) {
  1297. $dbh=init_db();
  1298. init_option($dbh);
  1299. clean_variables($dbh);
  1300. Set_Variable($dbh);
  1301. warn "DBI driver name: ", $dbh->{Driver}->{Name}, "\n" if ($debug);
  1302. warn "Full dbh class: ", ref($dbh), "\n" if ($debug);
  1303. }
  1304. 1;
  1305. }