database.pm 70 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087
  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. update_records
  44. get_office_subnet
  45. get_notify_subnet
  46. is_hotspot
  47. get_queue
  48. get_group
  49. get_subnet_description
  50. get_filter_instance_description
  51. get_vendor_name
  52. get_ou
  53. get_device_name
  54. get_device_model
  55. get_device_model_name
  56. get_building
  57. get_filter
  58. get_login
  59. StrToIp
  60. IpToStr
  61. prepare_audit_message
  62. batch_db_sql_cached
  63. batch_db_sql_csv
  64. reconnect_db
  65. write_db_log
  66. db_log_debug
  67. db_log_error
  68. db_log_info
  69. db_log_verbose
  70. db_log_warning
  71. normalize_value
  72. get_table_columns
  73. init_db
  74. do_sql
  75. _execute_param
  76. do_sql_param
  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` by %s:\n\n",
  448. $op_label,
  449. ucfirst($table),
  450. $summary_label,
  451. $table,
  452. $MY_NAME
  453. );
  454. for my $field (sort keys %resolved_changes) {
  455. my $change = $resolved_changes{$field};
  456. if ($operation eq 'insert') {
  457. if (defined $change->{new}) {
  458. $message .= sprintf(" %s: %s\n", $field, $change->{new});
  459. }
  460. } elsif ($operation eq 'delete') {
  461. if (defined $change->{old}) {
  462. $message .= sprintf(" %s: %s\n", $field, $change->{old});
  463. }
  464. } else { # update
  465. my $old_display = !defined($change->{old}) ? '[NULL]' : $change->{old};
  466. my $new_display = !defined($change->{new}) ? '[NULL]' : $change->{new};
  467. $message .= sprintf(" %s: \"%s\" → \"%s\"\n", $field, $old_display, $new_display);
  468. }
  469. }
  470. chomp $message;
  471. return $message;
  472. }
  473. #---------------------------------------------------------------------------------------------------------------
  474. sub resolve_reference_value {
  475. my ($dbh, $field, $value) = @_;
  476. return undef if !defined $value || $value eq '';
  477. # Проверка на целое число (как в PHP)
  478. if ($value !~ /^[+-]?\d+$/) {
  479. return "$value";
  480. }
  481. my $as_int = int($value);
  482. if ("$as_int" ne "$value") {
  483. return "$value";
  484. }
  485. my $id = $as_int;
  486. if ($field eq 'device_id') {
  487. return get_device_name($dbh, $id) // "Device#$id";
  488. }
  489. elsif ($field eq 'building_id') {
  490. return get_building($dbh, $id) // "Building#$id";
  491. }
  492. elsif ($field eq 'user_id') {
  493. return get_login($dbh, $id) // "User#$id";
  494. }
  495. elsif ($field eq 'ou_id') {
  496. return get_ou($dbh, $id) // "OU#$id";
  497. }
  498. elsif ($field eq 'vendor_id') {
  499. return get_vendor_name($dbh, $id) // "Vendor#$id";
  500. }
  501. elsif ($field eq 'device_model_id') {
  502. return get_device_model_name($dbh, $id) // "Model#$id";
  503. }
  504. elsif ($field eq 'instance_id') {
  505. return get_filter_instance_description($dbh, $id) // "FilterInstance#$id";
  506. }
  507. elsif ($field eq 'subnet_id') {
  508. return get_subnet_description($dbh, $id) // "Subnet#$id";
  509. }
  510. elsif ($field eq 'group_id') {
  511. return get_group($dbh, $id) // "FilterGroup#$id";
  512. }
  513. elsif ($field eq 'filter_id') {
  514. return get_filter($dbh, $id) // "Filter#$id";
  515. }
  516. elsif ($field eq 'filter_group_id') {
  517. return get_group($dbh, $id) // "FilterGroup#$id";
  518. }
  519. elsif ($field eq 'queue_id') {
  520. return get_queue($dbh, $id) // "Queue#$id";
  521. }
  522. elsif ($field eq 'auth_id') {
  523. return 'None' if $id <= 0;
  524. my $sql = "
  525. SELECT
  526. COALESCE(ul.login, CONCAT('User#', ua.user_id)) AS login,
  527. ua.ip,
  528. ua.dns_name
  529. FROM user_auth ua
  530. LEFT JOIN user_list ul ON ul.id = ua.user_id
  531. WHERE ua.id = ?
  532. ";
  533. my $row = get_record_sql($dbh, $sql, $id);
  534. return "Auth#$id" unless $row;
  535. my @parts;
  536. push @parts, "login: $row->{login}" if $row->{login} && $row->{login} ne '';
  537. push @parts, "IP: $row->{ip}" if $row->{ip} && $row->{ip} ne '';
  538. push @parts, "DNS: $row->{dns_name}" if $row->{dns_name} && $row->{dns_name} ne '';
  539. return @parts ? join(', ', @parts) : "Auth#$id";
  540. }
  541. elsif ($field eq 'target_port_id') {
  542. return 'None' if $id == 0;
  543. my $sql = "
  544. SELECT CONCAT(d.device_name, '[', dp.port, ']')
  545. FROM device_ports dp
  546. JOIN devices d ON d.id = dp.device_id
  547. WHERE dp.id = ?
  548. ";
  549. my $name = $dbh->selectrow_array($sql, undef, $id);
  550. return $name // "Port#$id";
  551. }
  552. else {
  553. return "$value";
  554. }
  555. }
  556. #---------------------------------------------------------------------------------------------------------------
  557. sub build_db_schema {
  558. my ($dbh) = @_;
  559. # Определяем тип СУБД
  560. my $db_type = lc($dbh->{Driver}->{Name});
  561. die "Unsupported database driver: $db_type"
  562. unless $db_type eq 'mysql' || $db_type eq 'pg';
  563. # Получаем имя базы данных
  564. my $db_name;
  565. if ($db_type eq 'mysql') {
  566. ($db_name) = $dbh->selectrow_array("SELECT DATABASE()");
  567. } elsif ($db_type eq 'pg') {
  568. ($db_name) = $dbh->selectrow_array("SELECT current_database()");
  569. }
  570. my $db_info;
  571. $db_info->{db_type}=$db_type;
  572. $db_info->{db_name}=$db_name;
  573. return $db_info if (exists $db_schema{$db_type}{$db_name});
  574. # Получаем список таблиц
  575. my @tables;
  576. if ($db_type eq 'mysql') {
  577. my $sth = $dbh->prepare("SHOW TABLES");
  578. $sth->execute();
  579. @tables = map { $_->[0] } @{$sth->fetchall_arrayref()};
  580. } elsif ($db_type eq 'pg') {
  581. my $sql = q{
  582. SELECT tablename
  583. FROM pg_tables
  584. WHERE schemaname = 'public'
  585. };
  586. my $sth = $dbh->prepare($sql);
  587. $sth->execute();
  588. @tables = map { $_->[0] } @{$sth->fetchall_arrayref()};
  589. }
  590. # Собираем схему
  591. for my $table (@tables) {
  592. my $sth = $dbh->column_info(undef, undef, $table, '%');
  593. while (my $col = $sth->fetchrow_hashref) {
  594. my $col_name = lc($col->{COLUMN_NAME});
  595. $db_schema{$db_type}{$db_name}{$table}{$col_name} = {
  596. type => $col->{TYPE_NAME} // '',
  597. nullable => $col->{NULLABLE} // 1,
  598. default => $col->{COLUMN_DEF} // undef,
  599. };
  600. }
  601. }
  602. return $db_info;
  603. }
  604. #---------------------------------------------------------------------------------------------------------------
  605. sub normalize_value {
  606. my ($value, $col_info) = @_;
  607. # Если значение пустое — обрабатываем по правилам колонки
  608. if (!defined $value || $value eq '' || $value =~ /^(?:NULL|\\N)$/i) {
  609. return $col_info->{nullable} ? undef : _default_for_type($col_info);
  610. }
  611. my $type = lc($col_info->{type});
  612. # --- Числовые типы: приводим к числу, если выглядит как число ---
  613. if ($type =~ /^(?:tinyint|smallint|mediumint|int|integer|bigint|serial|bigserial)$/i) {
  614. # Просто конвертируем строку в число (Perl сам обрежет мусор)
  615. # Например: "123abc" → 123, "abc" → 0
  616. return 0 + $value;
  617. }
  618. # --- Булевы: приводим к 0/1 ---
  619. if ($type =~ /^(?:bool|boolean|bit)$/i) {
  620. return $value ? 1 : 0;
  621. }
  622. # --- Временные типы: оставляем как есть, но фильтруем "нулевые" даты MySQL ---
  623. if ($type =~ /^(?:timestamp|datetime|date|time)$/i) {
  624. # Это частая проблема при миграции — '0000-00-00' ломает PostgreSQL
  625. return undef if $value =~ /^0000-00-00/;
  626. return $value;
  627. }
  628. # --- Все остальные типы (строки, inet, json и т.д.) — передаём как есть ---
  629. return $value;
  630. }
  631. # Вспомогательная: безопасное значение по умолчанию
  632. sub _default_for_type {
  633. my ($col) = @_;
  634. # Используем DEFAULT, только если он простой литерал (не выражение)
  635. if (defined $col->{default}) {
  636. my $def = $col->{default};
  637. # Пропускаем выражения: nextval(), CURRENT_TIMESTAMP, NOW(), uuid() и т.п.
  638. if ($def !~ /(nextval|current_timestamp|now|uuid|auto_increment|::)/i) {
  639. # Убираем одинарные кавычки, если строка: 'value' → value
  640. if ($def =~ /^'(.*)'$/) {
  641. return $1;
  642. }
  643. # Если похоже на число — вернём как число
  644. if ($def =~ /^[+-]?\d+$/) {
  645. return 0 + $def;
  646. }
  647. return $def;
  648. }
  649. }
  650. # Фолбэк по типу
  651. my $type = lc($col->{type});
  652. if ($type =~ /^(?:tinyint|smallint|int|integer|bigint)/i) { return 0; }
  653. if ($type =~ /^(?:char|varchar|text)/i) { return ''; }
  654. if ($type =~ /^(?:timestamp|datetime)/i) { return GetNowTime(); }
  655. return undef;
  656. }
  657. #---------------------------------------------------------------------------------------------------------------
  658. sub get_table_columns {
  659. my ($db, $table) = @_;
  660. my %columns;
  661. my $sth = $db->column_info(undef, undef, $table, '%');
  662. while (my $row = $sth->fetchrow_hashref) {
  663. my $name = lc($row->{COLUMN_NAME}); # ← приводим к нижнему регистру сразу!
  664. $columns{$name} = {
  665. type => $row->{TYPE_NAME} // '',
  666. nullable => $row->{NULLABLE} // 1,
  667. default => $row->{COLUMN_DEF} // undef,
  668. };
  669. }
  670. return %columns; # возвращает список: key1, val1, key2, val2...
  671. }
  672. #---------------------------------------------------------------------------------------------------------------
  673. sub StrToIp {
  674. return unpack('N',pack('C4',split(/\./,$_[0])));
  675. }
  676. #---------------------------------------------------------------------------------------------------------------
  677. sub IpToStr {
  678. my $nIP = shift;
  679. my $res = (($nIP>>24) & 255) .".". (($nIP>>16) & 255) .".". (($nIP>>8) & 255) .".". ($nIP & 255);
  680. return $res;
  681. }
  682. #---------------------------------------------------------------------------------------------------------------
  683. sub batch_db_sql_cached {
  684. my ( $sql, $data) = @_;
  685. my $db=init_db();
  686. # Запоминаем исходное состояние AutoCommit
  687. my $original_autocommit = $db->{AutoCommit};
  688. eval {
  689. # Выключаем AutoCommit для транзакции
  690. $db->{AutoCommit} = 0;
  691. my $sth = $db->prepare_cached($sql) or die "Unable to prepare SQL: " . $db->errstr;
  692. for my $params (@$data) {
  693. next unless @$params;
  694. $sth->execute(@$params) or die "Unable to execute with params [" . join(',', @$params) . "]: " . $sth->errstr;
  695. }
  696. $db->commit();
  697. 1;
  698. } or do {
  699. my $err = $@ || 'Unknown error';
  700. eval { $db->rollback() };
  701. warn "batch_sql_cached failed: $err";
  702. # Восстанавливаем AutoCommit даже при ошибке
  703. $db->{AutoCommit} = $original_autocommit;
  704. return 0;
  705. };
  706. # Восстанавливаем исходный режим AutoCommit
  707. $db->{AutoCommit} = $original_autocommit;
  708. $db->disconnect();
  709. return 1;
  710. }
  711. #---------------------------------------------------------------------------------------------------------------
  712. sub batch_db_sql_csv {
  713. my ($table, $data) = @_;
  714. return 0 unless @$data;
  715. # Первая строка — заголовки (имена столбцов)
  716. my $header_row = shift @$data;
  717. unless ($header_row && ref($header_row) eq 'ARRAY' && @$header_row) {
  718. log_error("First row must be column names (array reference)");
  719. return 0;
  720. }
  721. my @columns = @$header_row;
  722. # Теперь @$data содержит только строки данных
  723. my $data_rows = $data;
  724. # Если нет данных — только заголовок
  725. unless (@$data_rows) {
  726. log_debug("No data rows to insert, only header");
  727. return 1;
  728. }
  729. my $db = init_db();
  730. my $original_autocommit = $db->{AutoCommit};
  731. $db->{AutoCommit} = 0;
  732. if (get_db_type($db) eq 'mysql') {
  733. # --- MySQL: попытка LOAD DATA, fallback на INSERT ---
  734. log_debug("Using LOAD DATA LOCAL INFILE for MySQL");
  735. my $fh = File::Temp->new(UNLINK => 1);
  736. my $fname = $fh->filename;
  737. binmode($fh, ':utf8');
  738. my $csv = Text::CSV->new({
  739. binary => 1,
  740. quote_char => '"',
  741. escape_char => '"',
  742. sep_char => ',',
  743. eol => "\r\n",
  744. always_quote => 1,
  745. }) or do {
  746. my $err = "Cannot create Text::CSV: " . Text::CSV->error_diag();
  747. log_error($err);
  748. $db->{AutoCommit} = $original_autocommit;
  749. $db->disconnect();
  750. return 0;
  751. };
  752. # Пишем заголовок
  753. $csv->print($fh, \@columns);
  754. # Пишем данные
  755. for my $row (@$data_rows) {
  756. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  757. my @vals = map { defined($_) ? $_ : 'NULL' } @$row;
  758. $csv->print($fh, \@vals);
  759. }
  760. close $fh;
  761. my $col_list = join(', ', map { $db->quote_identifier($_) } @columns);
  762. 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)};
  763. my $load_ok = eval { $db->do($query); 1 };
  764. if (!$load_ok) {
  765. my $err = "MySQL LOAD DATA failed: $@";
  766. log_error($err);
  767. log_debug("Falling back to bulk INSERT for MySQL");
  768. goto FALLBACK_INSERT_MYSQL;
  769. }
  770. $db->commit();
  771. $db->{AutoCommit} = $original_autocommit;
  772. $db->disconnect();
  773. return 1;
  774. # ========================
  775. # Fallback для MySQL
  776. # ========================
  777. FALLBACK_INSERT_MYSQL:
  778. {
  779. my $quoted_cols = join(', ', map { $db->quote_identifier($_) } @columns);
  780. my $placeholders = join(',', ('?') x @columns);
  781. my $sql = "INSERT INTO $table ($quoted_cols) VALUES ($placeholders)";
  782. my $sth = $db->prepare($sql);
  783. my $success = eval {
  784. for my $row (@$data_rows) {
  785. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  786. my @vals = map { defined($_) ? $_ : undef } @$row;
  787. $sth->execute(@vals);
  788. }
  789. 1;
  790. };
  791. if ($success) {
  792. $db->commit();
  793. } else {
  794. eval { $db->rollback(); };
  795. my $err = "MySQL bulk INSERT failed: $@";
  796. log_error($err);
  797. $db->{AutoCommit} = $original_autocommit;
  798. $db->disconnect();
  799. return 0;
  800. }
  801. $db->{AutoCommit} = $original_autocommit;
  802. }
  803. } elsif (get_db_type($db) eq 'pg') {
  804. if (!$db->can('pg_putcopydata') || !$db->can('pg_putcopyend')) {
  805. log_debug("pg_putcopydata/pg_putcopyend not available — falling back to bulk INSERT");
  806. goto FALLBACK_INSERT_PG;
  807. }
  808. my $col_list = join(', ', map { $db->quote_identifier($_) } @columns);
  809. my $copy_sql = "COPY $table ($col_list) FROM STDIN WITH (FORMAT CSV, HEADER true)";
  810. my $use_header_as_data;
  811. my $start_ok = eval { $db->do($copy_sql); 1 };
  812. if (!$start_ok) {
  813. log_debug("COPY with HEADER failed: $@ — trying without HEADER");
  814. $copy_sql = "COPY $table ($col_list) FROM STDIN WITH (FORMAT CSV)";
  815. $start_ok = eval { $db->do($copy_sql); 1 };
  816. if (!$start_ok) {
  817. log_debug("COPY failed entirely: $@ — falling back to bulk INSERT");
  818. goto FALLBACK_INSERT_PG;
  819. }
  820. $use_header_as_data = 1;
  821. } else {
  822. $use_header_as_data = 0;
  823. }
  824. log_debug("Using CSV COPY for PostgreSQL");
  825. my $csv = Text::CSV->new({
  826. binary => 1,
  827. quote_char => '"',
  828. escape_char => '"',
  829. sep_char => ',',
  830. eol => "\n",
  831. always_quote => 1,
  832. }) or do {
  833. my $err = "Cannot create Text::CSV: " . Text::CSV->error_diag();
  834. log_error($err);
  835. eval { $db->pg_putcopyend(); };
  836. $db->{AutoCommit} = $original_autocommit;
  837. $db->disconnect();
  838. return 0;
  839. };
  840. my $success = eval {
  841. if ($use_header_as_data) {
  842. $csv->combine(@columns);
  843. $db->pg_putcopydata($csv->string);
  844. }
  845. for my $row (@$data_rows) {
  846. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  847. my @vals = map { defined($_) ? $_ : undef } @$row;
  848. $csv->combine(@vals);
  849. $db->pg_putcopydata($csv->string);
  850. }
  851. $db->pg_putcopyend();
  852. 1;
  853. };
  854. if ($success) {
  855. $db->commit();
  856. } else {
  857. eval { $db->rollback(); };
  858. my $err = "CSV COPY failed: $@";
  859. log_error($err);
  860. eval { $db->pg_putcopyend(); };
  861. goto FALLBACK_INSERT_PG;
  862. }
  863. # ========================
  864. # Fallback для PostgreSQL
  865. # ========================
  866. FALLBACK_INSERT_PG:
  867. {
  868. my $quoted_cols = join(', ', map { $db->quote_identifier($_) } @columns);
  869. my $placeholders = join(',', ('?') x @columns);
  870. my $sql = "INSERT INTO $table ($quoted_cols) VALUES ($placeholders)";
  871. my $sth = $db->prepare($sql);
  872. my $success = eval {
  873. for my $row (@$data_rows) {
  874. next unless $row && ref($row) eq 'ARRAY' && @$row == @columns;
  875. my @vals = map { defined($_) ? $_ : undef } @$row;
  876. $sth->execute(@vals);
  877. }
  878. 1;
  879. };
  880. if ($success) {
  881. $db->commit();
  882. } else {
  883. eval { $db->rollback(); };
  884. my $err = "PostgreSQL bulk INSERT failed: $@";
  885. log_error($err);
  886. $db->{AutoCommit} = $original_autocommit;
  887. $db->disconnect();
  888. return 0;
  889. }
  890. }
  891. } else {
  892. my $err = "Unsupported DBTYPE: ". get_db_type($db);
  893. log_error($err);
  894. $db->{AutoCommit} = $original_autocommit;
  895. $db->disconnect();
  896. return 0;
  897. }
  898. $db->{AutoCommit} = $original_autocommit;
  899. $db->disconnect();
  900. return 1;
  901. }
  902. #---------------------------------------------------------------------------------------------------------------
  903. sub reconnect_db {
  904. my $db_ref = shift;
  905. # Если соединение активно — ничего не делаем
  906. if ($$db_ref && $$db_ref->ping) {
  907. return 1;
  908. }
  909. # Сохраняем AutoCommit из текущего соединения (если есть)
  910. my $original_autocommit = 1;
  911. if ($$db_ref) {
  912. $original_autocommit = $$db_ref->{AutoCommit};
  913. eval { $$db_ref->disconnect; };
  914. $$db_ref = undef;
  915. }
  916. # Пытаемся переподключиться
  917. eval {
  918. $$db_ref = init_db($original_autocommit);
  919. unless ($$db_ref && $$db_ref->ping) {
  920. log_die "Failed to establish database connection";
  921. }
  922. 1;
  923. } or do {
  924. my $error = $@ || 'Unknown error';
  925. $$db_ref = undef;
  926. log_die "Database reconnection failed: $error";
  927. return 0;
  928. };
  929. return 1;
  930. }
  931. #---------------------------------------------------------------------------------------------------------------
  932. sub write_db_log {
  933. my $db=shift;
  934. my $msg=shift;
  935. my $level = shift || $L_VERBOSE;
  936. my $auth_id = shift || 0;
  937. return if (!$db);
  938. return if (!$msg);
  939. $msg=~s/[\'\"]//g;
  940. my $db_log = 0;
  941. # Переподключение
  942. unless (reconnect_db(\$db)) {
  943. log_error("No database connection available");
  944. $db_log = 0;
  945. }
  946. if ($level eq $L_ERROR and $log_level >= $L_ERROR) { log_error($msg); $db_log = 1; }
  947. if ($level eq $L_WARNING and $log_level >= $L_WARNING) { log_warning($msg); $db_log = 1; }
  948. if ($level eq $L_INFO and $log_level >= $L_INFO) { log_info($msg); $db_log = 1; }
  949. if ($level eq $L_VERBOSE and $log_level >= $L_VERBOSE) { log_verbose($msg); $db_log = 1; }
  950. if ($level eq $L_DEBUG and $log_level >= $L_DEBUG) { log_debug($msg); return; }
  951. if ($db_log) {
  952. #my $new_id = do_sql($dbh, 'INSERT INTO user_list (login) VALUES (?)', 'Ivan');
  953. do_sql($db,'INSERT INTO worklog(customer,message,level,auth_id,ip) VALUES( ?, ?, ?, ?, ?)',$MY_NAME,$msg,$level,$auth_id,$config_ref{self_ip});
  954. }
  955. }
  956. #---------------------------------------------------------------------------------------------------------------
  957. sub db_log_debug {
  958. my $db = shift;
  959. my $msg = shift;
  960. my $id = shift;
  961. if ($debug) { log_debug($msg); }
  962. }
  963. #---------------------------------------------------------------------------------------------------------------
  964. sub db_log_error {
  965. my $db = shift;
  966. my $msg = shift;
  967. if ($log_level >= $L_ERROR) {
  968. sendEmail("ERROR! ".get_first_line($msg),$msg,1);
  969. write_db_log($db,$msg,$L_ERROR);
  970. }
  971. }
  972. #---------------------------------------------------------------------------------------------------------------
  973. sub db_log_info {
  974. my $db = shift;
  975. my $msg = shift;
  976. my $id = shift;
  977. if ($log_level >= $L_INFO) { write_db_log($db,$msg,$L_INFO,$id); }
  978. }
  979. #---------------------------------------------------------------------------------------------------------------
  980. sub db_log_verbose {
  981. my $db = shift;
  982. my $msg = shift;
  983. my $id = shift;
  984. if ($log_level >= $L_VERBOSE) { write_db_log($db,$msg,$L_VERBOSE,$id); }
  985. }
  986. #---------------------------------------------------------------------------------------------------------------
  987. sub db_log_warning {
  988. my $db = shift;
  989. my $msg = shift;
  990. my $id = shift;
  991. if ($log_level >= $L_WARNING) { write_db_log($db,$msg,$L_WARNING,$id); }
  992. }
  993. #---------------------------------------------------------------------------------------------------------------
  994. sub init_db {
  995. my $autocommit = shift;
  996. if (!defined $autocommit) { $autocommit = 1; }
  997. my $db;
  998. if ($config_ref{DBTYPE} eq 'mysql') {
  999. $db = DBI->connect(
  1000. "dbi:mysql:database=$DBNAME;host=$DBHOST;port=3306;mysql_local_infile=1", $DBUSER, $DBPASS,
  1001. { RaiseError => 0, AutoCommit => $autocommit, mysql_enable_utf8 => 1 }
  1002. );
  1003. if (!defined $db) {
  1004. log_die "Cannot connect to MySQL server: $DBI::errstr\n";
  1005. }
  1006. $db->do('SET NAMES utf8mb4');
  1007. } else {
  1008. $db = DBI->connect(
  1009. "dbi:Pg:dbname=$DBNAME;host=$DBHOST;port=5432", $DBUSER, $DBPASS,
  1010. { RaiseError => 0, AutoCommit => $autocommit, pg_enable_utf8 => 1, pg_server_prepare => 0 }
  1011. );
  1012. if (!defined $db) {
  1013. log_die "Cannot connect to PostgreSQL server: $DBI::errstr\n";
  1014. }
  1015. }
  1016. return $db;
  1017. }
  1018. #---------------------------------------------------------------------------------------------------------------
  1019. # Обновленная функция get_option с параметризованными запросами
  1020. sub get_option {
  1021. my $db = shift;
  1022. my $option_id = shift;
  1023. return if (!$option_id);
  1024. return if (!$db);
  1025. my $sql = q{
  1026. SELECT
  1027. COALESCE(c.value, co.default_value) AS value,
  1028. co.option_type
  1029. FROM config_options co
  1030. LEFT JOIN config c ON c.option_id = co.id
  1031. WHERE co.id = ?
  1032. };
  1033. my $record = get_record_sql($db, $sql, $option_id);
  1034. unless ($record) {
  1035. log_error("Option ID $option_id not found in config_options table");
  1036. return;
  1037. }
  1038. return $record->{value};
  1039. }
  1040. #---------------------------------------------------------------------------------------------------------------
  1041. sub get_records_sql {
  1042. my ($db, $sql, @params) = @_;
  1043. my @result;
  1044. return @result if (!$db);
  1045. return @result if (!$sql);
  1046. unless (reconnect_db(\$db)) {
  1047. log_error("No database connection available");
  1048. return @result;
  1049. }
  1050. my $result_ref = _execute_param($db, $sql, \@params, { mode => 'array' });
  1051. if (ref($result_ref) eq 'ARRAY') {
  1052. @result = @$result_ref;
  1053. }
  1054. return @result;
  1055. }
  1056. #---------------------------------------------------------------------------------------------------------------
  1057. sub get_record_sql {
  1058. my ($db, $sql, @params) = @_;
  1059. my @result;
  1060. return @result if (!$db);
  1061. return @result if (!$sql);
  1062. # Добавляем LIMIT только если его еще нет в запросе
  1063. if ($sql !~ /\bLIMIT\s+\d+/i && $sql !~ /\bFETCH\s+FIRST\s+\d+/i) {
  1064. $sql .= ' LIMIT 1';
  1065. }
  1066. # Переподключение
  1067. unless (reconnect_db(\$db)) {
  1068. log_error("No database connection available");
  1069. return;
  1070. }
  1071. return _execute_param($db, $sql, \@params, { mode => 'single' });
  1072. }
  1073. #---------------------------------------------------------------------------------------------------------------
  1074. sub get_count_records {
  1075. my ($db, $table, $filter, @params) = @_;
  1076. my $result = 0;
  1077. return $result if (!$db);
  1078. return $result if (!$table);
  1079. my $sSQL='SELECT COUNT(*) as rec_cnt FROM '.$table;
  1080. if ($filter) { $sSQL=$sSQL." WHERE ".$filter; }
  1081. my $record = get_record_sql($db,$sSQL, @params);
  1082. if ($record->{rec_cnt}) { $result = $record->{rec_cnt}; }
  1083. return $result;
  1084. }
  1085. #---------------------------------------------------------------------------------------------------------------
  1086. sub get_id_record {
  1087. my ($db, $table, $filter, @params) = @_;
  1088. my $result = 0;
  1089. return $result if (!$db);
  1090. return $result if (!$table);
  1091. my $record = get_record_sql($db,"SELECT id FROM $table WHERE $filter", @params);
  1092. if ($record->{id}) { $result = $record->{id}; }
  1093. return $result;
  1094. }
  1095. #---------------------------------------------------------------------------------------------------------------
  1096. sub get_diff_rec {
  1097. my ($db, $table, $record, $filter_sql, @filter_params) = @_;
  1098. return unless $db && $table && $filter_sql;
  1099. unless (reconnect_db(\$db)) {
  1100. log_error("No database connection available");
  1101. return;
  1102. }
  1103. my $old_record = get_record_sql($db,"SELECT * FROM $table WHERE $filter_sql",@filter_params);
  1104. return unless $old_record;
  1105. my $result;
  1106. foreach my $field (keys %$record) {
  1107. if (!$record->{$field}) { $record->{$field}=''; }
  1108. if (!$old_record->{$field}) { $old_record->{$field}=''; }
  1109. if ($record->{$field}!~/^$old_record->{$field}$/) { $result->{$field} = "$record->{$field} [ old: " . $old_record->{$field} . "]"; }
  1110. }
  1111. return hash_to_text($result);
  1112. }
  1113. #---------------------------------------------------------------------------------------------------------------
  1114. sub get_db_type {
  1115. my $db = shift;
  1116. return lc($db->{Driver}->{Name});
  1117. #'mysql', 'pg'
  1118. }
  1119. #---------------------------------------------------------------------------------------------------------------
  1120. # Внутренняя функция для выполнения параметризованных запросов
  1121. sub _execute_param {
  1122. my ($db, $sql, $params, $options) = @_;
  1123. return unless $db && $sql;
  1124. my $mode = $options->{mode} || 'execute';
  1125. # --- Автоматическая поддержка RETURNING для PostgreSQL ---
  1126. my $was_modified = 0;
  1127. my $original_sql = $sql;
  1128. if ($mode eq 'id' && $sql =~ /^\s*INSERT\b/i) {
  1129. if (get_db_type($db) eq 'pg') {
  1130. unless ($sql =~ /\bRETURNING\b/i) {
  1131. $sql .= ' RETURNING id';
  1132. $was_modified = 1;
  1133. $mode = 'scalar';
  1134. }
  1135. }
  1136. }
  1137. # Логируем не-SELECT
  1138. unless ($original_sql =~ /^\s*SELECT/i) {
  1139. log_debug($original_sql . ($params ? ' | params: [' . join(', ', map { defined $_ ? $_ : 'undef' } @$params) . ']' : ''));
  1140. }
  1141. # === не переподключаемся внутри транзакции ===
  1142. my $autocommit_enabled = $db->{AutoCommit};
  1143. unless ($autocommit_enabled) {
  1144. # В транзакции: нельзя переподключаться!
  1145. unless ($db->ping) {
  1146. log_error("Database connection lost during transaction");
  1147. return wantarray ? () : undef;
  1148. }
  1149. } else {
  1150. # Вне транзакции: можно переподключиться
  1151. unless (reconnect_db(\$db)) {
  1152. log_error("No database connection available");
  1153. return wantarray ? () : undef;
  1154. }
  1155. }
  1156. my $sth = $db->prepare($sql) or do {
  1157. log_error("Unable to prepare SQL [$original_sql]: " . $db->errstr);
  1158. return wantarray ? () : undef;
  1159. };
  1160. my $rv = $params ? $sth->execute(@$params) : $sth->execute();
  1161. unless ($rv) {
  1162. log_error("Unable to execute SQL [$original_sql]" . ($params ? " with params: [" . join(', ', map { defined $_ ? $_ : 'undef' } @$params) . "]" : "") . ": " . $sth->errstr);
  1163. $sth->finish();
  1164. return wantarray ? () : undef;
  1165. }
  1166. # --- Обработка результатов ---
  1167. if ($was_modified && $mode eq 'scalar') {
  1168. my $row = $sth->fetchrow_arrayref();
  1169. $sth->finish();
  1170. my $id = $row ? $row->[0] : 0;
  1171. return $id;
  1172. }
  1173. elsif ($mode eq 'single') {
  1174. my $row = $sth->fetchrow_hashref();
  1175. $sth->finish();
  1176. return $row;
  1177. }
  1178. elsif ($mode eq 'array') {
  1179. my @rows;
  1180. while (my $row = $sth->fetchrow_hashref()) {
  1181. push @rows, $row;
  1182. }
  1183. $sth->finish();
  1184. return \@rows;
  1185. }
  1186. elsif ($mode eq 'arrayref') {
  1187. my $rows = $sth->fetchall_arrayref({});
  1188. $sth->finish();
  1189. return $rows;
  1190. }
  1191. elsif ($mode eq 'scalar') {
  1192. my $row = $sth->fetchrow_arrayref();
  1193. $sth->finish();
  1194. return $row ? $row->[0] : undef;
  1195. }
  1196. elsif ($mode eq 'id') {
  1197. if ($original_sql =~ /^\s*INSERT/i) {
  1198. my $id;
  1199. if (get_db_type($db) eq 'mysql') {
  1200. $id = $sth->{mysql_insertid};
  1201. } else {
  1202. ($id) = $db->selectrow_array("SELECT lastval()");
  1203. }
  1204. $sth->finish();
  1205. return $id || 0;
  1206. }
  1207. $sth->finish();
  1208. return 1;
  1209. }
  1210. else {
  1211. $sth->finish();
  1212. return 1;
  1213. }
  1214. }
  1215. #---------------------------------------------------------------------------------------------------------------
  1216. sub do_sql {
  1217. my ($db, $sql, @bind_values) = @_;
  1218. return unless $db && $sql; # Возвращаем undef при ошибке входных данных
  1219. my $mode;
  1220. if ($sql =~ /^\s*insert\b/i) {
  1221. $mode = 'id';
  1222. } elsif ($sql =~ /^\s*select\b/i) {
  1223. $mode = 'arrayref';
  1224. } else {
  1225. $mode = 'execute';
  1226. }
  1227. my $result = _execute_param($db, $sql, \@bind_values, { mode => $mode });
  1228. # Если _execute_param вернул undef/ложь — это ошибка
  1229. unless (defined $result) {
  1230. return; # Возвращаем undef (лучше, чем 0)
  1231. }
  1232. if ($mode eq 'id') {
  1233. return $result; # число (возможно 0 — допустимо для ID)
  1234. } elsif ($mode eq 'arrayref') {
  1235. # _execute_param всегда возвращает ARRAYREF при успехе
  1236. return $result;
  1237. } else {
  1238. # Для UPDATE/DELETE: возвращаем количество затронутых строк или 1
  1239. return $result ? $result : 1;
  1240. }
  1241. }
  1242. #---------------------------------------------------------------------------------------------------------------
  1243. sub insert_record {
  1244. my ($db, $table, $record) = @_;
  1245. return unless $db && $table && ref($record) eq 'HASH' && %$record;
  1246. # Переподключаемся ТОЛЬКО если не в транзакции
  1247. if ($db->{AutoCommit}) {
  1248. unless (reconnect_db(\$db)) {
  1249. log_error("No database connection available");
  1250. return;
  1251. }
  1252. } else {
  1253. unless ($db->ping) {
  1254. log_error("Database connection lost during transaction");
  1255. return;
  1256. }
  1257. }
  1258. my $db_info= build_db_schema($db);
  1259. my $dns_changed = 0;
  1260. my $rec_id = 0;
  1261. if ($table eq "user_auth") {
  1262. foreach my $field (keys %$record) {
  1263. if (exists $acl_fields{$field}) { $record->{changed}="1"; }
  1264. if (exists $dhcp_fields{$field}) { $record->{dhcp_changed}="1"; }
  1265. if (exists $dns_fields{$field}) { $dns_changed=1; }
  1266. }
  1267. }
  1268. my @insert_params;
  1269. my $fields = '';
  1270. my $values = '';
  1271. foreach my $field (keys %$record) {
  1272. my $val = normalize_value($record->{$field}, $db_schema{$db_info->{db_type}}{$db_info->{db_name}}{$table}{$field});
  1273. # Экранируем имя поля в зависимости от СУБД
  1274. my $quoted_field = get_db_type($db) eq 'mysql'
  1275. ? '`' . $field . '`'
  1276. : '"' . $field . '"';
  1277. $fields .= "$quoted_field, ";
  1278. $values .= "?, ";
  1279. push @insert_params, $val;
  1280. }
  1281. $fields =~ s/,\s*$//;
  1282. $values =~ s/,\s*$//;
  1283. my $sSQL = "INSERT INTO $table($fields) VALUES($values)";
  1284. my $result = do_sql($db,$sSQL,@insert_params);
  1285. if ($result) {
  1286. $rec_id = $result;
  1287. my $changed_msg = prepare_audit_message($db, $table, undef, $record, $rec_id, 'insert');
  1288. if ($table !~ /session/i) {
  1289. if (defined $changed_msg && $changed_msg ne '') {
  1290. if ($table !~ /user/i) {
  1291. db_log_info($db, $changed_msg);
  1292. } else {
  1293. if ($table eq 'user_auth' && defined $record->{ip} && $record->{ip} ne '') {
  1294. if (is_hotspot($db, $record->{ip})) {
  1295. db_log_info($db, $changed_msg, $rec_id);
  1296. } else {
  1297. db_log_warning($db, $changed_msg, $rec_id);
  1298. my $send_alert_create = isNotifyCreate(get_notify_subnet($db, $record->{ip}));
  1299. sendEmail("WARN! " . get_first_line($changed_msg), $changed_msg, 1) if $send_alert_create;
  1300. }
  1301. } else {
  1302. db_log_warning($db, $changed_msg);
  1303. }
  1304. }
  1305. }
  1306. if ($table eq 'user_auth_alias' and $dns_changed) {
  1307. if ($record->{'alias'} and $record->{'alias'}!~/\.$/) {
  1308. my $add_dns;
  1309. $add_dns->{'name_type'}='CNAME';
  1310. $add_dns->{'name'}=$record->{'alias'};
  1311. $add_dns->{'value'}=get_dns_name($db,$record->{'auth_id'});
  1312. $add_dns->{'operation_type'}='add';
  1313. $add_dns->{'auth_id'}=$record->{'auth_id'};
  1314. insert_record($db,'dns_queue',$add_dns);
  1315. }
  1316. }
  1317. if ($table eq 'user_auth' and $dns_changed) {
  1318. if ($record->{'dns_name'} and $record->{'ip'} and !$record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  1319. my $add_dns;
  1320. $add_dns->{'name_type'}='A';
  1321. $add_dns->{'name'}=$record->{'dns_name'};
  1322. $add_dns->{'value'}=$record->{'ip'};
  1323. $add_dns->{'operation_type'}='add';
  1324. $add_dns->{'auth_id'}=$result;
  1325. insert_record($db,'dns_queue',$add_dns);
  1326. }
  1327. if ($record->{'dns_name'} and $record->{'ip'} and $record->{'dns_ptr_only'} and $record->{'dns_name'}!~/\.$/) {
  1328. my $add_dns;
  1329. $add_dns->{'name_type'}='PTR';
  1330. $add_dns->{'name'}=$record->{'dns_name'};
  1331. $add_dns->{'value'}=$record->{'ip'};
  1332. $add_dns->{'operation_type'}='add';
  1333. $add_dns->{'auth_id'}=$result;
  1334. insert_record($db,'dns_queue',$add_dns);
  1335. }
  1336. }
  1337. }
  1338. }
  1339. return $result;
  1340. }
  1341. #---------------------------------------------------------------------------------------------------------------
  1342. sub update_records {
  1343. my ($db, $table, $filter, $newvalue, @filter_params) = @_;
  1344. # Получаем ID всех записей, подходящих под фильтр
  1345. my $uSQL = "SELECT id FROM $table WHERE $filter";
  1346. my @ids = get_records_sql($db, $uSQL, @filter_params);
  1347. # Если ничего не найдено — считаем успехом
  1348. return 1 unless @ids;
  1349. # Обновляем каждую запись по отдельности
  1350. for my $record (@ids) {
  1351. next unless ref $record eq 'HASH' && defined $record->{id};
  1352. update_record($db, $table, $newvalue, "id = ?", $record->{id});
  1353. }
  1354. return 1;
  1355. }
  1356. #---------------------------------------------------------------------------------------------------------------
  1357. sub update_record {
  1358. my ($db, $table, $record, $filter_sql, @filter_params) = @_;
  1359. return unless $db && $table && $filter_sql;
  1360. # Переподключаемся ТОЛЬКО если не в транзакции
  1361. if ($db->{AutoCommit}) {
  1362. unless (reconnect_db(\$db)) {
  1363. log_error("No database connection available");
  1364. return;
  1365. }
  1366. } else {
  1367. unless ($db->ping) {
  1368. log_error("Database connection lost during transaction");
  1369. return;
  1370. }
  1371. }
  1372. my $db_info = build_db_schema($db);
  1373. my $select_sql = "SELECT * FROM $table WHERE $filter_sql";
  1374. my $old_record = get_record_sql($db, $select_sql, @filter_params);
  1375. return unless $old_record;
  1376. my @update_params;
  1377. my $set_clause = '';
  1378. my $dns_changed = 0;
  1379. my $rec_id = $old_record->{id} || 0;
  1380. if ($table eq "user_auth") {
  1381. $rec_id = $old_record->{'id'} if ($old_record->{'id'});
  1382. my $cur_ou_id = $old_record->{'ou_id'} if ($old_record->{'ou_id'});
  1383. if (exists $record->{ou_id}) { $cur_ou_id = $record->{'ou_id'}; }
  1384. #disable update field 'created_by'
  1385. #if ($old_record->{'created_by'} and exists ($record->{'created_by'})) { delete $record->{'created_by'}; }
  1386. foreach my $field (keys %$record) {
  1387. if (exists $acl_fields{$field}) { $record->{changed}="1"; }
  1388. if (exists $dhcp_fields{$field} and !is_system_ou($db,$cur_ou_id)) { $record->{dhcp_changed}="1"; }
  1389. if (exists $dns_fields{$field}) { $dns_changed=1; }
  1390. }
  1391. }
  1392. for my $field (keys %$record) {
  1393. next if (!$field);
  1394. next if (!exists $record->{$field});
  1395. next if (!defined $record->{$field});
  1396. my $old_val = defined $old_record->{$field} ? $old_record->{$field} : '';
  1397. my $new_val = normalize_value( $record->{$field}, $db_schema{$db_info->{db_type}}{$db_info->{db_name}}{$table}{$field});
  1398. $new_val = defined $new_val ? $new_val : '';
  1399. if ($new_val ne $old_val) {
  1400. $set_clause .= " $field = ?, ";
  1401. push @update_params, $new_val;
  1402. }
  1403. }
  1404. return unless $set_clause;
  1405. # Добавляем служебные поля
  1406. if ($table eq 'user_auth') {
  1407. if ($record->{changed} || $record->{dhcp_changed} || $dns_changed ) {
  1408. $set_clause .= "changed_time = ?, ";
  1409. push @update_params, GetNowTime();
  1410. }
  1411. }
  1412. $set_clause =~ s/,\s*$//;
  1413. if ($table eq 'user_auth') {
  1414. if ($dns_changed) {
  1415. my $del_dns;
  1416. if ($old_record->{'dns_name'} and $old_record->{'ip'} and !$old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1417. $del_dns->{'name_type'}='A';
  1418. $del_dns->{'name'}=$old_record->{'dns_name'};
  1419. $del_dns->{'value'}=$old_record->{'ip'};
  1420. $del_dns->{'operation_type'}='del';
  1421. if ($rec_id) { $del_dns->{'auth_id'}=$rec_id; }
  1422. insert_record($db,'dns_queue',$del_dns);
  1423. }
  1424. if ($old_record->{'dns_name'} and $old_record->{'ip'} and $old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1425. $del_dns->{'name_type'}='PTR';
  1426. $del_dns->{'name'}=$old_record->{'dns_name'};
  1427. $del_dns->{'value'}=$old_record->{'ip'};
  1428. $del_dns->{'operation_type'}='del';
  1429. if ($rec_id) { $del_dns->{'auth_id'}=$rec_id; }
  1430. insert_record($db,'dns_queue',$del_dns);
  1431. }
  1432. my $new_dns;
  1433. my $dns_rec_ip = $old_record->{ip};
  1434. my $dns_rec_name = $old_record->{dns_name};
  1435. if ($record->{'dns_name'}) { $dns_rec_name = $record->{'dns_name'}; }
  1436. if ($record->{'ip'}) { $dns_rec_ip = $record->{'ip'}; }
  1437. if ($dns_rec_name and $dns_rec_ip and !$record->{'dns_ptr_only'} and $dns_rec_name !~ /\.$/) {
  1438. $new_dns->{'name_type'}='A';
  1439. $new_dns->{'name'}=$dns_rec_name;
  1440. $new_dns->{'value'}=$dns_rec_ip;
  1441. $new_dns->{'operation_type'}='add';
  1442. if ($rec_id) { $new_dns->{'auth_id'}=$rec_id; }
  1443. insert_record($db,'dns_queue',$new_dns);
  1444. }
  1445. if ($dns_rec_name and $dns_rec_ip and $record->{'dns_ptr_only'} and $dns_rec_name !~ /\.$/) {
  1446. $new_dns->{'name_type'}='PTR';
  1447. $new_dns->{'name'}=$dns_rec_name;
  1448. $new_dns->{'value'}=$dns_rec_ip;
  1449. $new_dns->{'operation_type'}='add';
  1450. if ($rec_id) { $new_dns->{'auth_id'}=$rec_id; }
  1451. insert_record($db,'dns_queue',$new_dns);
  1452. }
  1453. }
  1454. }
  1455. if ($table eq 'user_auth_alias') {
  1456. if ($dns_changed) {
  1457. my $del_dns;
  1458. if ($old_record->{'alias'} and $old_record->{'alias'}!~/\.$/) {
  1459. $del_dns->{'name_type'}='CNAME';
  1460. $del_dns->{'name'}=$old_record->{'alias'};
  1461. $del_dns->{'operation_type'}='del';
  1462. $del_dns->{'value'}=get_dns_name($db,$old_record->{auth_id});
  1463. $del_dns->{'auth_id'}=$old_record->{auth_id};
  1464. insert_record($db,'dns_queue',$del_dns);
  1465. }
  1466. my $new_dns;
  1467. my $dns_rec_name = $old_record->{alias};
  1468. if ($record->{'alias'}) { $dns_rec_name = $record->{'alias'}; }
  1469. if ($dns_rec_name and $record->{'alias'}!~/\.$/) {
  1470. $new_dns->{'name_type'}='CNAME';
  1471. $new_dns->{'name'}=$dns_rec_name;
  1472. $new_dns->{'operation_type'}='add';
  1473. $new_dns->{'value'}=get_dns_name($db,$old_record->{auth_id});
  1474. $new_dns->{'auth_id'}=$rec_id;
  1475. insert_record($db,'dns_queue',$new_dns);
  1476. }
  1477. }
  1478. }
  1479. my @all_params = (@update_params, @filter_params);
  1480. my $update_sql = "UPDATE $table SET $set_clause WHERE $filter_sql";
  1481. my $result = do_sql($db, $update_sql, @all_params);
  1482. if ($result) {
  1483. my $changed_msg = prepare_audit_message($db, $table, $old_record, $record , $rec_id, 'update');
  1484. if ($table !~ /session/i) {
  1485. if (defined $changed_msg && $changed_msg ne '') {
  1486. if ($table !~ /user/i) {
  1487. db_log_info($db, $changed_msg);
  1488. } else {
  1489. if (is_hotspot($db, $old_record->{ip})) {
  1490. db_log_info($db, $changed_msg, $rec_id);
  1491. } else {
  1492. db_log_warning($db, $changed_msg, $rec_id);
  1493. if ($table eq 'user_auth' && defined $old_record->{ip} && $old_record->{ip} ne '') {
  1494. my $send_alert_update = isNotifyUpdate(get_notify_subnet($db, $old_record->{ip}));
  1495. sendEmail("WARN! " . get_first_line($changed_msg), $changed_msg, 1) if $send_alert_update;
  1496. }
  1497. }
  1498. }
  1499. }
  1500. }
  1501. }
  1502. return $result;
  1503. }
  1504. #---------------------------------------------------------------------------------------------------------------
  1505. sub delete_record {
  1506. my ($db, $table, $filter_sql, @filter_params) = @_;
  1507. return unless $db && $table && $filter_sql;
  1508. # Переподключаемся ТОЛЬКО если не в транзакции
  1509. if ($db->{AutoCommit}) {
  1510. unless (reconnect_db(\$db)) {
  1511. log_error("No database connection available");
  1512. return;
  1513. }
  1514. } else {
  1515. unless ($db->ping) {
  1516. log_error("Database connection lost during transaction");
  1517. return;
  1518. }
  1519. }
  1520. my $select_sql = "SELECT * FROM $table WHERE $filter_sql";
  1521. my $old_record = get_record_sql($db, $select_sql, @filter_params);
  1522. return unless $old_record;
  1523. my $rec_id = $old_record->{'id'};
  1524. #never delete user ip record!
  1525. if ($table eq 'user_auth') {
  1526. my $sSQL = "UPDATE user_auth SET changed = 1, deleted = 1, changed_time = ? WHERE $filter_sql";
  1527. my $ret = do_sql($db, $sSQL, GetNowTime(), @filter_params);
  1528. if ($old_record->{'dns_name'} and $old_record->{'ip'} and !$old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1529. my $del_dns;
  1530. $del_dns->{'name_type'}='A';
  1531. $del_dns->{'name'}=$old_record->{'dns_name'};
  1532. $del_dns->{'value'}=$old_record->{'ip'};
  1533. $del_dns->{'operation_type'}='del';
  1534. $del_dns->{'auth_id'}=$old_record->{'id'};
  1535. insert_record($db,'dns_queue',$del_dns);
  1536. }
  1537. if ($old_record->{'dns_name'} and $old_record->{'ip'} and $old_record->{'dns_ptr_only'} and $old_record->{'dns_name'}!~/\.$/) {
  1538. my $del_dns;
  1539. $del_dns->{'name_type'}='PTR';
  1540. $del_dns->{'name'}=$old_record->{'dns_name'};
  1541. $del_dns->{'value'}=$old_record->{'ip'};
  1542. $del_dns->{'operation_type'}='del';
  1543. $del_dns->{'auth_id'}=$old_record->{'id'};
  1544. insert_record($db,'dns_queue',$del_dns);
  1545. }
  1546. my $changed_msg = prepare_audit_message($db, $table, $old_record, undef , $rec_id, 'delete');
  1547. if ($ret) {
  1548. if (defined $changed_msg && $changed_msg ne '') {
  1549. if (defined $old_record->{ip} && $old_record->{ip} ne '') {
  1550. if (is_hotspot($db, $old_record->{ip})) {
  1551. db_log_info($db, $changed_msg, $rec_id);
  1552. } else {
  1553. db_log_warning($db, $changed_msg, $rec_id);
  1554. my $send_alert_delete = isNotifyDelete(get_notify_subnet($db, $old_record->{ip}));
  1555. sendEmail("WARN! " . get_first_line($changed_msg), $changed_msg, 1) if $send_alert_delete;
  1556. }
  1557. }
  1558. }
  1559. }
  1560. return $ret;
  1561. }
  1562. if ($table eq 'user_list' and $old_record->{'permanent'}) { return; }
  1563. if ($table eq 'user_auth_alias') {
  1564. if ($old_record->{'alias'} and $old_record->{'auth_id'} and $old_record->{'alias'}!~/\.$/) {
  1565. my $del_dns;
  1566. $del_dns->{'name_type'}='CNAME';
  1567. $del_dns->{'name'}=$old_record->{'alias'};
  1568. $del_dns->{'value'}=get_dns_name($db,$old_record->{'auth_id'});
  1569. $del_dns->{'operation_type'}='del';
  1570. $del_dns->{'auth_id'}=$old_record->{'auth_id'};
  1571. insert_record($db,'dns_queue',$del_dns);
  1572. }
  1573. }
  1574. my $sSQL = "DELETE FROM ".$table." WHERE ".$filter_sql;
  1575. my $result = do_sql($db,$sSQL,@filter_params);
  1576. my $changed_msg = prepare_audit_message($db, $table, $old_record, undef , $rec_id, 'delete');
  1577. if ($result && $table !~ /session/i) {
  1578. if (defined $changed_msg && $changed_msg ne '') {
  1579. if ($table !~ /user/i) {
  1580. db_log_info($db, $changed_msg);
  1581. } else {
  1582. db_log_warning($db, $changed_msg);
  1583. }
  1584. }
  1585. }
  1586. return $result;
  1587. }
  1588. #---------------------------------------------------------------------------------------------------------------
  1589. sub is_system_ou {
  1590. my ($db, $ou_id) = @_;
  1591. return 0 if !defined $ou_id || $ou_id !~ /^\d+$/ || $ou_id <= 0;
  1592. my $sql = "SELECT 1 FROM ou WHERE id = ? AND (default_users = 1 OR default_hotspot = 1)";
  1593. my $record = get_record_sql($db, $sql, $ou_id);
  1594. return $record ? 1 : 0;
  1595. }
  1596. #---------------------------------------------------------------------------------------------------------------
  1597. sub init_option {
  1598. my $db=shift;
  1599. $last_refresh_config = time();
  1600. $config_ref{version}='';
  1601. my $version_record = get_record_sql($db,"SELECT version FROM version WHERE version is NOT NULL");
  1602. if ($version_record) { $config_ref{version}=$version_record->{version}; }
  1603. $config_ref{self_ip} = '127.0.0.1';
  1604. if ($DBHOST ne '127.0.0.1') {
  1605. my $ip_route = qx(ip r get $DBHOST 2>&1 | head -1);
  1606. if ($? == 0) {
  1607. if ($ip_route =~ /src\s+(\d+\.\d+\.\d+\.\d+)/) { $config_ref{self_ip} = $1; }
  1608. }
  1609. }
  1610. $config_ref{dbh}=$db;
  1611. $config_ref{save_detail}=get_option($db,23);
  1612. $config_ref{add_unknown_user}=get_option($db,22);
  1613. $config_ref{dhcp_server}=get_option($db,5);
  1614. $config_ref{snmp_default_version}=get_option($db,9);
  1615. $config_ref{snmp_default_community}=get_option($db,11);
  1616. $config_ref{KB}=get_option($db,1);
  1617. if ($config_ref{KB} ==0) { $config_ref{KB}=1000; }
  1618. if ($config_ref{KB} ==1) { $config_ref{KB}=1024; }
  1619. $config_ref{admin_email}=get_option($db,21);
  1620. $config_ref{sender_email}=get_option($db,52);
  1621. $config_ref{send_email}=get_option($db,51);
  1622. $config_ref{history}=get_option($db,26);
  1623. $config_ref{history_dhcp}=get_option($db,27);
  1624. $config_ref{router_login}=get_option($db,28);
  1625. $config_ref{router_password}=get_option($db,29);
  1626. $config_ref{router_port}=get_option($db,30);
  1627. $config_ref{org_name}=get_option($db,32);
  1628. $config_ref{domain_name}=get_option($db,33);
  1629. $config_ref{connections_history}=get_option($db,35);
  1630. $config_ref{debug}=get_option($db,34);
  1631. $config_ref{log_level} = get_option($db,53);
  1632. if ($config_ref{debug}) { $config_ref{log_level} = 255; }
  1633. $config_ref{urgent_sync}=get_option($db,50);
  1634. $config_ref{ignore_hotspot_dhcp_log} = get_option($db,44);
  1635. $config_ref{ignore_update_dhcp_event} = get_option($db,45);
  1636. $config_ref{update_hostname_from_dhcp} = get_option($db,46);
  1637. $config_ref{history_log_day}=get_option($db,47);
  1638. $config_ref{history_syslog_day} = get_option($db,48);
  1639. $config_ref{history_trafstat_day} = get_option($db,49);
  1640. $config_ref{enable_quotes} = get_option($db,54);
  1641. $config_ref{netflow_step} = get_option($db,55);
  1642. $config_ref{traffic_ipstat_history} = get_option($db,56);
  1643. $config_ref{nagios_url} = get_option($db,57);
  1644. $config_ref{cacti_url} = get_option($db,58);
  1645. $config_ref{torrus_url} = get_option($db,59);
  1646. $config_ref{wiki_url} = get_option($db,60);
  1647. $config_ref{stat_url} = get_option($db,62);
  1648. $config_ref{wiki_path} = get_option($db,61);
  1649. $config_ref{auto_mac_rule} = get_option($db,64);
  1650. #network configuration mode
  1651. $config_ref{config_mode}=get_option($db,68);
  1652. #auto clean old user record
  1653. $config_ref{clean_empty_user}=get_option($db,69);
  1654. #dns_server_type
  1655. $config_ref{dns_server}=get_option($db,3);
  1656. $config_ref{dns_server_type}=get_option($db,70);
  1657. $config_ref{enable_dns_updates}=get_option($db,71);
  1658. #$save_detail = 1; id=23
  1659. $save_detail=get_option($db,23);
  1660. #$add_unknown_user = 1; id=22
  1661. $add_unknown_user=get_option($db,22);
  1662. #$dns_server='192.168.2.12'; id=3
  1663. $dns_server=get_option($db,3);
  1664. #$dhcp_server='192.168.2.12'; id=5
  1665. $dhcp_server=get_option($db,5);
  1666. #$snmp_default_version='2'; id=9
  1667. $snmp_default_version=get_option($db,9);
  1668. #$snmp_default_community='public'; id=11
  1669. $snmp_default_community=get_option($db,11);
  1670. #$KB=1024; id=1
  1671. $KB=$config_ref{KB};
  1672. #$admin_email; id=21
  1673. $admin_email=get_option($db,21);
  1674. #sender email
  1675. $sender_email=get_option($db,52);
  1676. #send email
  1677. $send_email=get_option($db,51);
  1678. #$history=15; id=26
  1679. $history=get_option($db,26);
  1680. #$history_dhcp=7; id=27
  1681. $history_dhcp=get_option($db,27);
  1682. #$router_login="admin"; id=28
  1683. $router_login=get_option($db,28);
  1684. #$router_password="admin"; id=29
  1685. $router_password=get_option($db,29);
  1686. #$router_port=23; id=30
  1687. $router_port=get_option($db,30);
  1688. #32
  1689. $org_name=get_option($db,32);
  1690. #33
  1691. $domain_name=get_option($db,33);
  1692. #35
  1693. $connections_history=get_option($db,35);
  1694. #debug
  1695. $debug=get_option($db,34);
  1696. #log level
  1697. $log_level = get_option($db,53);
  1698. if ($debug) { $log_level = 255; }
  1699. #urgent sync access
  1700. $urgent_sync=get_option($db,50);
  1701. $ignore_hotspot_dhcp_log = get_option($db,44);
  1702. $ignore_update_dhcp_event = get_option($db,45);
  1703. $update_hostname_from_dhcp = get_option($db,46);
  1704. $history_log_day=get_option($db,47);
  1705. $history_syslog_day = get_option($db,48);
  1706. $history_trafstat_day = get_option($db,49);
  1707. my $ou = get_record_sql($db,"SELECT id FROM ou WHERE default_users = 1");
  1708. if (!$ou) { $default_user_ou_id = 0; } else { $default_user_ou_id = $ou->{'id'}; }
  1709. $ou = get_record_sql($db,"SELECT id FROM ou WHERE default_hotspot = 1 ");
  1710. if (!$ou) { $default_hotspot_ou_id = $default_user_ou_id; } else { $default_hotspot_ou_id = $ou->{'id'}; }
  1711. @subnets=get_records_sql($db,'SELECT * FROM subnets ORDER BY ip_int_start');
  1712. if (defined $office_networks) { undef $office_networks; }
  1713. if (defined $free_networks) { undef $free_networks; }
  1714. if (defined $vpn_networks) { undef $vpn_networks; }
  1715. if (defined $hotspot_networks) { undef $hotspot_networks; }
  1716. if (defined $all_networks) { undef $all_networks; }
  1717. $office_networks = new Net::Patricia;
  1718. $free_networks = new Net::Patricia;
  1719. $vpn_networks = new Net::Patricia;
  1720. $hotspot_networks = new Net::Patricia;
  1721. $all_networks = new Net::Patricia;
  1722. @office_network_list=();
  1723. @free_network_list=();
  1724. @free_network_list=();
  1725. @vpn_network_list=();
  1726. @hotspot_network_list=();
  1727. @all_network_list=();
  1728. foreach my $net (@subnets) {
  1729. next if (!$net->{subnet});
  1730. $subnets_ref{$net->{subnet}}=$net;
  1731. if ($net->{office}) {
  1732. push(@office_network_list,$net->{subnet});
  1733. $office_networks->add_string($net->{subnet},$net);
  1734. }
  1735. if ($net->{free}) {
  1736. push(@free_network_list,$net->{subnet});
  1737. $free_networks->add_string($net->{subnet},$net);
  1738. }
  1739. if ($net->{vpn}) {
  1740. push(@vpn_network_list,$net->{subnet});
  1741. $vpn_networks->add_string($net->{subnet},$net);
  1742. }
  1743. if ($net->{hotspot}) {
  1744. push(@hotspot_network_list,$net->{subnet});
  1745. push(@all_network_list,$net->{subnet});
  1746. $hotspot_networks->add_string($net->{subnet},$net);
  1747. }
  1748. push(@all_network_list,$net->{subnet});
  1749. $all_networks->add_string($net->{subnet},$net);
  1750. }
  1751. }
  1752. #---------------------------------------------------------------------------------------------------------------
  1753. sub Set_Variable {
  1754. my ($db, $name, $value, $timeshift) = @_;
  1755. $name //= $MY_NAME;
  1756. $value //= $$;
  1757. $timeshift //= 60;
  1758. Del_Variable($db, $name);
  1759. my $clean_time = time() + $timeshift;
  1760. my ($sec, $min, $hour, $day, $month, $year) = localtime($clean_time);
  1761. $month++;
  1762. $year += 1900;
  1763. my $clear_time_str = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $day, $hour, $min, $sec;
  1764. my $sql = "INSERT INTO variables (name, value, clear_time) VALUES (?, ?, ?)";
  1765. do_sql($db, $sql, $name, $value, $clear_time_str);
  1766. }
  1767. #---------------------------------------------------------------------------------------------------------------
  1768. sub Get_Variable {
  1769. my $db = shift;
  1770. my $name = shift || $MY_NAME;
  1771. my $variable = get_record_sql($db, 'SELECT value FROM variables WHERE name = ?', $name);
  1772. if ($variable and $variable->{'value'}) { return $variable->{'value'}; }
  1773. return;
  1774. }
  1775. #---------------------------------------------------------------------------------------------------------------
  1776. sub Del_Variable {
  1777. my ($db, $name) = @_;
  1778. $name //= $MY_NAME;
  1779. do_sql($db, "DELETE FROM variables WHERE name = ?", $name);
  1780. }
  1781. #---------------------------------------------------------------------------------------------------------------
  1782. sub clean_variables {
  1783. my ($db) = @_;
  1784. # 1. Clean temporary variables
  1785. my $now = time();
  1786. my ($sec, $min, $hour, $day, $month, $year) = localtime($now);
  1787. $month++;
  1788. $year += 1900;
  1789. my $now_str = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $day, $hour, $min, $sec;
  1790. do_sql($db, "DELETE FROM variables WHERE clear_time <= ?", $now_str);
  1791. # 2. Clean old AD computer cache
  1792. my $yesterday = DateTime->now(time_zone => 'local')->subtract(days => 1);
  1793. my $clean_str = $yesterday->strftime("%Y-%m-%d 00:00:00");
  1794. do_sql($db, "DELETE FROM ad_comp_cache WHERE last_found <= ?", $clean_str);
  1795. }
  1796. #---------------------------------------------------------------------------------------------------------------
  1797. #skip init for upgrade
  1798. if ($MY_NAME!~/upgrade.pl/) {
  1799. $dbh=init_db();
  1800. init_option($dbh);
  1801. clean_variables($dbh);
  1802. Set_Variable($dbh);
  1803. # warn "DBI driver name: ", $dbh->{Driver}->{Name}, "\n" if ($debug);
  1804. # warn "Full dbh class: ", ref($dbh), "\n" if ($debug);
  1805. }
  1806. 1;
  1807. }