database.pm 69 KB

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