database.pm 69 KB

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