1
0

database.pm 68 KB

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