PluginProbe
AL Pack / 1.0.0
AL Pack v1.0.0
1.3.13 trunk 1.0.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.11 1.3.12
alpack / includes / protection.php

protection.php in AL Pack 1.0.0, at includes/protection.php

859 lines 29.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 function presslearn_is_plugin_active_for_protection() {
7 $click_protection_enabled = get_option('presslearn_click_protection_enabled', 'no');
8 return $click_protection_enabled === 'yes';
9 }
10
11 /**
12 * Protection admin styles
13 */
14 function presslearn_protection_admin_styles() {
15 if (!is_admin()) {
16 return;
17 }
18
19 $screen = get_current_screen();
20 if (!$screen || strpos($screen->id, 'presslearn-click-protection') === false) {
21 return;
22 }
23
24 wp_register_style(
25 'presslearn-protection-admin-css',
26 false,
27 array(),
28 PRESSLEARN_PLUGIN_VERSION
29 );
30 wp_enqueue_style('presslearn-protection-admin-css');
31
32 $protection_css = "
33 .presslearn-modal {
34 position: fixed;
35 z-index: 9999;
36 left: 0;
37 top: 0;
38 width: 100%;
39 height: 100%;
40 background-color: rgba(0, 0, 0, 0.5);
41 display: flex;
42 align-items: center;
43 justify-content: center;
44 }
45
46 .presslearn-modal-content {
47 background-color: #fff;
48 width: 90%;
49 max-width: 700px;
50 border-radius: 6px;
51 box-shadow: 0 3px 20px rgba(0, 0, 0, 0.2);
52 display: flex;
53 flex-direction: column;
54 max-height: 90vh;
55 }
56
57 .presslearn-modal-header {
58 padding: 20px;
59 border-bottom: 1px solid #ddd;
60 display: flex;
61 justify-content: space-between;
62 align-items: center;
63 }
64
65 .presslearn-modal-header h3 {
66 margin: 0;
67 font-size: 20px;
68 }
69
70 .presslearn-modal-close {
71 font-size: 28px;
72 font-weight: bold;
73 cursor: pointer;
74 color: #666;
75 }
76
77 .presslearn-modal-close:hover {
78 color: #000;
79 }
80
81 .presslearn-modal-body {
82 padding: 20px;
83 overflow-y: auto;
84 flex: 1;
85 }
86
87 .presslearn-modal-footer {
88 padding: 15px 20px;
89 border-top: 1px solid #ddd;
90 text-align: right;
91 display: flex;
92 gap: 10px;
93 justify-content: flex-end;
94 }
95
96 .settings-form-row {
97 margin-bottom: 20px;
98 }
99
100 .settings-form-row label {
101 display: block;
102 margin-bottom: 5px;
103 font-weight: bold;
104 }
105
106 .settings-form-row input {
107 width: 100%;
108 padding: 8px;
109 border: 1px solid #ddd;
110 border-radius: 4px;
111 }
112
113 .modal-preview {
114 margin-top: 30px;
115 border-top: 1px solid #eee;
116 padding-top: 20px;
117 }
118
119 .modal-preview h4 {
120 margin-top: 0;
121 margin-bottom: 15px;
122 }
123
124 .modal-preview-container {
125 border: 1px dashed #ccc;
126 padding: 20px;
127 border-radius: 5px;
128 }
129
130 .ip-add-form {
131 margin-bottom: 20px;
132 padding-bottom: 20px;
133 border-bottom: 1px solid #eee;
134 }
135
136 .ip-input-group {
137 display: flex;
138 gap: 10px;
139 margin-bottom: 10px;
140 }
141
142 .ip-input-group input {
143 flex: 1;
144 }
145
146 .ip-description {
147 color: #666;
148 font-size: 13px;
149 margin: 5px 0 0;
150 }
151
152 .ip-list-table {
153 width: 100%;
154 border-collapse: collapse;
155 }
156
157 .ip-list-table th,
158 .ip-list-table td {
159 padding: 12px 15px;
160 text-align: left;
161 border-bottom: 1px solid #eee;
162 }
163
164 .ip-list-table th {
165 background-color: #f9f9f9;
166 font-weight: 600;
167 }
168
169 .ip-list-table tr:hover {
170 background-color: #f5f5f5;
171 }
172
173 .delete-ip,
174 .delete-blocked-ip {
175 color: #d32f2f;
176 cursor: pointer;
177 background: none;
178 border: none;
179 padding: 0;
180 }
181
182 .delete-ip:hover,
183 .delete-blocked-ip:hover {
184 text-decoration: underline;
185 }
186
187 .log-filter {
188 display: flex;
189 gap: 10px;
190 margin-bottom: 20px;
191 align-items: center;
192 }
193
194 .log-list-table {
195 width: 100%;
196 border-collapse: collapse;
197 }
198
199 .log-list-table th,
200 .log-list-table td {
201 padding: 12px 15px;
202 text-align: left;
203 border-bottom: 1px solid #eee;
204 }
205
206 .log-list-table th {
207 background-color: #f9f9f9;
208 font-weight: 600;
209 }
210
211 .log-list-table tr:hover {
212 background-color: #f5f5f5;
213 }
214
215 .spinner-container {
216 display: flex;
217 justify-content: center;
218 align-items: center;
219 height: 50px;
220 }
221
222 .log-expired {
223 color: #999;
224 text-decoration: line-through;
225 }
226
227 .log-active {
228 color: #d32f2f;
229 }
230 ";
231
232 wp_add_inline_style('presslearn-protection-admin-css', $protection_css);
233 }
234
235 /**
236 * Protection admin scripts
237 */
238 function presslearn_protection_admin_scripts() {
239 if (!is_admin()) {
240 return;
241 }
242
243 $screen = get_current_screen();
244 if (!$screen || strpos($screen->id, 'presslearn-click-protection') === false) {
245 return;
246 }
247
248 wp_register_script(
249 'presslearn-protection-ip-management-js',
250 false,
251 array('jquery'),
252 PRESSLEARN_PLUGIN_VERSION,
253 true
254 );
255 wp_enqueue_script('presslearn-protection-ip-management-js');
256
257 wp_localize_script('presslearn-protection-ip-management-js', 'presslearn_protection_ajax', array(
258 'ajax_url' => admin_url('admin-ajax.php'),
259 'nonce' => wp_create_nonce('presslearn_ip_nonce')
260 ));
261
262 $ip_management_js = "
263 document.addEventListener('DOMContentLoaded', function() {
264 let allowedIPs = [];
265 let blockedIPs = [];
266
267 const ipNonce = presslearn_protection_ajax.nonce;
268 const ajaxUrl = presslearn_protection_ajax.ajax_url;
269
270 const openAllowedBtn = document.getElementById('open-allowed-ip-editor');
271 const openBlockedBtn = document.getElementById('open-blocked-ip-editor');
272
273 if (openAllowedBtn) {
274 openAllowedBtn.addEventListener('click', function() {
275 loadAllowedIPs();
276 document.getElementById('allowed-ip-modal').style.display = 'flex';
277 });
278 }
279
280 if (openBlockedBtn) {
281 openBlockedBtn.addEventListener('click', function() {
282 loadBlockedIPs();
283 document.getElementById('blocked-ip-modal').style.display = 'flex';
284 });
285 }
286
287 document.addEventListener('click', function(e) {
288 if (e.target.classList.contains('presslearn-modal-close') ||
289 e.target.id === 'close-modal-button') {
290 document.getElementById('allowed-ip-modal').style.display = 'none';
291 }
292 if (e.target.classList.contains('presslearn-modal-close') ||
293 e.target.id === 'close-blocked-modal-button') {
294 document.getElementById('blocked-ip-modal').style.display = 'none';
295 }
296 if (e.target.id === 'allowed-ip-modal') {
297 document.getElementById('allowed-ip-modal').style.display = 'none';
298 }
299 if (e.target.id === 'blocked-ip-modal') {
300 document.getElementById('blocked-ip-modal').style.display = 'none';
301 }
302 });
303
304 const addIpBtn = document.getElementById('add-ip-button');
305 const addBlockedIpBtn = document.getElementById('add-blocked-ip-button');
306 const newIpInput = document.getElementById('new-ip-address');
307 const newBlockedIpInput = document.getElementById('new-blocked-ip');
308
309 if (addIpBtn) {
310 addIpBtn.addEventListener('click', addNewIP);
311 }
312
313 if (addBlockedIpBtn) {
314 addBlockedIpBtn.addEventListener('click', addNewBlockedIP);
315 }
316
317 if (newIpInput) {
318 newIpInput.addEventListener('keypress', function(e) {
319 if (e.which === 13) {
320 addNewIP();
321 e.preventDefault();
322 }
323 });
324 }
325
326 if (newBlockedIpInput) {
327 newBlockedIpInput.addEventListener('keypress', function(e) {
328 if (e.which === 13) {
329 addNewBlockedIP();
330 e.preventDefault();
331 }
332 });
333 }
334
335 function loadAllowedIPs() {
336 const tbody = document.getElementById('allowed-ip-list');
337 if (tbody) {
338 tbody.innerHTML = '<tr><td colspan=\"3\" style=\"text-align:center;\"><div class=\"spinner-container\"><span class=\"spinner is-active\"></span></div></td></tr>';
339 }
340
341 fetch(ajaxUrl, {
342 method: 'POST',
343 headers: {
344 'Content-Type': 'application/x-www-form-urlencoded',
345 },
346 body: new URLSearchParams({
347 action: 'presslearn_get_allowed_ips',
348 nonce: ipNonce
349 })
350 })
351 .then(response => response.json())
352 .then(data => {
353 if (data.success) {
354 allowedIPs = data.data.ips || [];
355 renderIPTable();
356 } else {
357 alert('허용 IP 목록을 불러오는데 실패했습니다.');
358 allowedIPs = [];
359 renderIPTable();
360 }
361 })
362 .catch(error => {
363 alert('서버 통신 오류가 발생했습니다.');
364 allowedIPs = [];
365 renderIPTable();
366 });
367 }
368
369 function loadBlockedIPs() {
370 const tbody = document.getElementById('blocked-ip-list');
371 if (tbody) {
372 tbody.innerHTML = '<tr><td colspan=\"3\" style=\"text-align:center;\"><div class=\"spinner-container\"><span class=\"spinner is-active\"></span></div></td></tr>';
373 }
374
375 fetch(ajaxUrl, {
376 method: 'POST',
377 headers: {
378 'Content-Type': 'application/x-www-form-urlencoded',
379 },
380 body: new URLSearchParams({
381 action: 'presslearn_get_blocked_ips',
382 nonce: ipNonce
383 })
384 })
385 .then(response => response.json())
386 .then(data => {
387 if (data.success) {
388 blockedIPs = data.data.ips || [];
389 renderBlockedIPTable();
390 } else {
391 alert('차단 IP 목록을 불러오는데 실패했습니다.');
392 blockedIPs = [];
393 renderBlockedIPTable();
394 }
395 })
396 .catch(error => {
397 alert('서버 통신 오류가 발생했습니다.');
398 blockedIPs = [];
399 renderBlockedIPTable();
400 });
401 }
402
403 function renderIPTable() {
404 const tbody = document.getElementById('allowed-ip-list');
405 const noIpsMessage = document.getElementById('no-ips-message');
406
407 if (!tbody) return;
408
409 tbody.innerHTML = '';
410
411 if (allowedIPs.length > 0) {
412 if (noIpsMessage) noIpsMessage.style.display = 'none';
413
414 allowedIPs.forEach((item) => {
415 const row = document.createElement('tr');
416 row.innerHTML = `
417 <td>\${item.ip}</td>
418 <td>\${item.date}</td>
419 <td>
420 <button type=\"button\" class=\"delete-ip\" data-ip=\"\${item.ip}\">삭제</button>
421 </td>
422 `;
423 tbody.appendChild(row);
424 });
425
426 document.querySelectorAll('.delete-ip').forEach(btn => {
427 btn.addEventListener('click', function() {
428 const ip = this.getAttribute('data-ip');
429 deleteIP(ip);
430 });
431 });
432 } else {
433 if (noIpsMessage) noIpsMessage.style.display = 'block';
434 }
435 }
436
437 function renderBlockedIPTable() {
438 const tbody = document.getElementById('blocked-ip-list');
439 const noBlockedIpsMessage = document.getElementById('no-blocked-ips-message');
440
441 if (!tbody) return;
442
443 tbody.innerHTML = '';
444
445 if (blockedIPs.length > 0) {
446 if (noBlockedIpsMessage) noBlockedIpsMessage.style.display = 'none';
447
448 blockedIPs.forEach((item) => {
449 const row = document.createElement('tr');
450 row.innerHTML = `
451 <td>\${item.ip}</td>
452 <td>\${item.date}</td>
453 <td>
454 <button type=\"button\" class=\"delete-blocked-ip\" data-ip=\"\${item.ip}\">삭제</button>
455 </td>
456 `;
457 tbody.appendChild(row);
458 });
459
460 document.querySelectorAll('.delete-blocked-ip').forEach(btn => {
461 btn.addEventListener('click', function() {
462 const ip = this.getAttribute('data-ip');
463 deleteBlockedIP(ip);
464 });
465 });
466 } else {
467 if (noBlockedIpsMessage) noBlockedIpsMessage.style.display = 'block';
468 }
469 }
470
471 function addNewIP() {
472 const newIpInput = document.getElementById('new-ip-address');
473 if (!newIpInput) return;
474
475 const newIP = newIpInput.value.trim();
476
477 if (!isValidIP(newIP)) {
478 alert('유효한 IP 주소를 �
479 력해주세요.');
480 return;
481 }
482
483 fetch(ajaxUrl, {
484 method: 'POST',
485 headers: {
486 'Content-Type': 'application/x-www-form-urlencoded',
487 },
488 body: new URLSearchParams({
489 action: 'presslearn_add_allowed_ip',
490 nonce: ipNonce,
491 ip: newIP
492 })
493 })
494 .then(response => response.json())
495 .then(data => {
496 if (data.success) {
497 alert('IP가 성공적으로 허용 목록에 추가되었습니다.');
498 allowedIPs = data.data.ips;
499 renderIPTable();
500 newIpInput.value = '';
501 newIpInput.focus();
502 } else {
503 alert(data.data.message || '허용 IP 추가에 실패했습니다.');
504 }
505 })
506 .catch(error => {
507 alert('서버 통신 오류가 발생했습니다.');
508 });
509 }
510
511 function addNewBlockedIP() {
512 const newBlockedIpInput = document.getElementById('new-blocked-ip');
513 if (!newBlockedIpInput) return;
514
515 const newIP = newBlockedIpInput.value.trim();
516
517 if (!isValidIP(newIP)) {
518 alert('유효한 IP 주소를 �
519 력해주세요.');
520 return;
521 }
522
523 fetch(ajaxUrl, {
524 method: 'POST',
525 headers: {
526 'Content-Type': 'application/x-www-form-urlencoded',
527 },
528 body: new URLSearchParams({
529 action: 'presslearn_add_blocked_ip',
530 nonce: ipNonce,
531 ip: newIP
532 })
533 })
534 .then(response => response.json())
535 .then(data => {
536 if (data.success) {
537 alert('IP가 성공적으로 차단되었습니다.');
538 blockedIPs = data.data.ips;
539 renderBlockedIPTable();
540 newBlockedIpInput.value = '';
541 newBlockedIpInput.focus();
542 } else {
543 alert(data.data.message || '차단 IP 추가에 실패했습니다.');
544 }
545 })
546 .catch(error => {
547 alert('서버 통신 오류가 발생했습니다.');
548 });
549 }
550
551 function deleteIP(ip) {
552 if (confirm('이 IP를 허용 목록에서 삭제하시겠습니까?')) {
553 fetch(ajaxUrl, {
554 method: 'POST',
555 headers: {
556 'Content-Type': 'application/x-www-form-urlencoded',
557 },
558 body: new URLSearchParams({
559 action: 'presslearn_delete_allowed_ip',
560 nonce: ipNonce,
561 ip: ip
562 })
563 })
564 .then(response => response.json())
565 .then(data => {
566 if (data.success) {
567 alert('IP가 성공적으로 허용 목록에서 삭제되었습니다.');
568 allowedIPs = data.data.ips;
569 renderIPTable();
570 } else {
571 alert(data.data.message || '허용 IP 삭제에 실패했습니다.');
572 }
573 })
574 .catch(error => {
575 alert('서버 통신 오류가 발생했습니다.');
576 });
577 }
578 }
579
580 function deleteBlockedIP(ip) {
581 if (confirm('이 IP를 차단 목록에서 삭제하시겠습니까?')) {
582 fetch(ajaxUrl, {
583 method: 'POST',
584 headers: {
585 'Content-Type': 'application/x-www-form-urlencoded',
586 },
587 body: new URLSearchParams({
588 action: 'presslearn_delete_blocked_ip',
589 nonce: ipNonce,
590 ip: ip
591 })
592 })
593 .then(response => response.json())
594 .then(data => {
595 if (data.success) {
596 alert('IP가 성공적으로 차단 목록에서 삭제되었습니다.');
597 blockedIPs = data.data.ips;
598 renderBlockedIPTable();
599 } else {
600 alert(data.data.message || '차단 IP 삭제에 실패했습니다.');
601 }
602 })
603 .catch(error => {
604 alert('서버 통신 오류가 발생했습니다.');
605 });
606 }
607 }
608
609 function isValidIP(ip) {
610 const regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
611 return regex.test(ip);
612 }
613 });
614 ";
615
616 wp_add_inline_script('presslearn-protection-ip-management-js', $ip_management_js);
617
618 wp_register_script(
619 'presslearn-protection-modal-settings-js',
620 false,
621 array('jquery'),
622 PRESSLEARN_PLUGIN_VERSION,
623 true
624 );
625 wp_enqueue_script('presslearn-protection-modal-settings-js');
626
627 $modal_settings_js = "
628 document.addEventListener('DOMContentLoaded', function() {
629 const openModalSettingsBtn = document.getElementById('open-modal-settings');
630 const closeModalSettingsBtn = document.getElementById('close-modal-settings');
631 const saveModalSettingsBtn = document.getElementById('save-modal-settings');
632
633 if (openModalSettingsBtn) {
634 openModalSettingsBtn.addEventListener('click', function() {
635 document.getElementById('modal-settings-editor').style.display = 'flex';
636 });
637 }
638
639 if (closeModalSettingsBtn) {
640 closeModalSettingsBtn.addEventListener('click', function() {
641 document.getElementById('modal-settings-editor').style.display = 'none';
642 });
643 }
644
645 document.addEventListener('click', function(e) {
646 if (e.target.classList.contains('presslearn-modal-close')) {
647 document.getElementById('modal-settings-editor').style.display = 'none';
648 }
649 if (e.target.id === 'modal-settings-editor') {
650 document.getElementById('modal-settings-editor').style.display = 'none';
651 }
652 });
653
654 const modalTitleInput = document.getElementById('modal_title');
655 const modalMessageInput = document.getElementById('modal_message');
656 const modalSubmessageInput = document.getElementById('modal_submessage');
657 const modalButtonTextInput = document.getElementById('modal_button_text');
658
659 if (modalTitleInput) {
660 modalTitleInput.addEventListener('input', function() {
661 const previewTitle = document.getElementById('preview-title');
662 if (previewTitle) previewTitle.textContent = this.value;
663 });
664 }
665
666 if (modalMessageInput) {
667 modalMessageInput.addEventListener('input', function() {
668 const previewMessage = document.getElementById('preview-message');
669 if (previewMessage) previewMessage.textContent = this.value;
670 });
671 }
672
673 if (modalSubmessageInput) {
674 modalSubmessageInput.addEventListener('input', function() {
675 const previewSubmessage = document.getElementById('preview-submessage');
676 if (previewSubmessage) previewSubmessage.textContent = this.value;
677 });
678 }
679
680 if (modalButtonTextInput) {
681 modalButtonTextInput.addEventListener('input', function() {
682 const previewButton = document.getElementById('preview-button');
683 if (previewButton) previewButton.textContent = this.value;
684 });
685 }
686
687 if (saveModalSettingsBtn) {
688 saveModalSettingsBtn.addEventListener('click', function() {
689 const modalTitle = modalTitleInput ? modalTitleInput.value : '';
690 const modalMessage = modalMessageInput ? modalMessageInput.value : '';
691 const modalSubmessage = modalSubmessageInput ? modalSubmessageInput.value : '';
692 const modalButtonText = modalButtonTextInput ? modalButtonTextInput.value : '';
693
694 const form = document.getElementById('click-protection-settings-form');
695 if (form) {
696 const existingInputs = form.querySelectorAll('input[name^=\"modal_\"]');
697 existingInputs.forEach(input => input.remove());
698
699 const inputs = [
700 {name: 'modal_title', value: modalTitle},
701 {name: 'modal_message', value: modalMessage},
702 {name: 'modal_submessage', value: modalSubmessage},
703 {name: 'modal_button_text', value: modalButtonText}
704 ];
705
706 inputs.forEach(inputData => {
707 const input = document.createElement('input');
708 input.type = 'hidden';
709 input.name = inputData.name;
710 input.value = inputData.value;
711 form.appendChild(input);
712 });
713 }
714
715 document.getElementById('modal-settings-editor').style.display = 'none';
716 alert('모달 설정이 적용되었습니다. 변경사항을 저장하려면 \"저장하기\" 버튼을 클릭하세요.');
717 });
718 }
719 });
720 ";
721
722 wp_add_inline_script('presslearn-protection-modal-settings-js', $modal_settings_js);
723
724 wp_register_script(
725 'presslearn-protection-blocked-logs-js',
726 false,
727 array('jquery'),
728 PRESSLEARN_PLUGIN_VERSION,
729 true
730 );
731 wp_enqueue_script('presslearn-protection-blocked-logs-js');
732
733 wp_localize_script('presslearn-protection-blocked-logs-js', 'presslearn_logs_ajax', array(
734 'ajax_url' => admin_url('admin-ajax.php'),
735 'nonce' => wp_create_nonce('presslearn_ip_nonce')
736 ));
737
738 $blocked_logs_js = "
739 document.addEventListener('DOMContentLoaded', function() {
740 const openLogModalBtn = document.getElementById('open-blocked-log-modal');
741 const closeLogModalBtn = document.getElementById('close-log-modal-button');
742 const refreshLogBtn = document.getElementById('refresh-log');
743
744 if (openLogModalBtn) {
745 openLogModalBtn.addEventListener('click', function() {
746 loadBlockedLogs();
747 document.getElementById('blocked-log-modal').style.display = 'flex';
748 });
749 }
750
751 if (closeLogModalBtn) {
752 closeLogModalBtn.addEventListener('click', function() {
753 document.getElementById('blocked-log-modal').style.display = 'none';
754 });
755 }
756
757 if (refreshLogBtn) {
758 refreshLogBtn.addEventListener('click', loadBlockedLogs);
759 }
760
761 document.addEventListener('click', function(e) {
762 if (e.target.classList.contains('presslearn-modal-close')) {
763 document.getElementById('blocked-log-modal').style.display = 'none';
764 }
765 if (e.target.id === 'blocked-log-modal') {
766 document.getElementById('blocked-log-modal').style.display = 'none';
767 }
768 });
769
770 function loadBlockedLogs() {
771 const logList = document.getElementById('blocked-log-list');
772 const noLogsMessage = document.getElementById('no-blocked-logs-message');
773 const logLoading = document.getElementById('log-loading');
774 const periodSelect = document.getElementById('log-filter-period');
775
776 if (logList) logList.innerHTML = '';
777 if (noLogsMessage) noLogsMessage.style.display = 'none';
778 if (logLoading) logLoading.style.display = 'block';
779
780 const period = periodSelect ? periodSelect.value : '30';
781
782 fetch(presslearn_logs_ajax.ajax_url, {
783 method: 'POST',
784 headers: {
785 'Content-Type': 'application/x-www-form-urlencoded',
786 },
787 body: new URLSearchParams({
788 action: 'presslearn_get_blocked_logs',
789 nonce: presslearn_logs_ajax.nonce,
790 period: period
791 })
792 })
793 .then(response => response.json())
794 .then(data => {
795 if (logLoading) logLoading.style.display = 'none';
796
797 if (data.success) {
798 const logs = data.data.logs || [];
799 renderLogTable(logs);
800 } else {
801 alert('차단 로그를 불러오는데 실패했습니다.');
802 if (noLogsMessage) noLogsMessage.style.display = 'block';
803 }
804 })
805 .catch(error => {
806 if (logLoading) logLoading.style.display = 'none';
807 alert('서버 통신 오류가 발생했습니다.');
808 if (noLogsMessage) noLogsMessage.style.display = 'block';
809 });
810 }
811
812 function renderLogTable(logs) {
813 const tbody = document.getElementById('blocked-log-list');
814 const noLogsMessage = document.getElementById('no-blocked-logs-message');
815
816 if (!tbody) return;
817
818 tbody.innerHTML = '';
819
820 if (logs.length > 0) {
821 if (noLogsMessage) noLogsMessage.style.display = 'none';
822
823 const today = new Date();
824
825 logs.forEach((item) => {
826 let statusClass = 'log-active';
827 let expiryText = item.expires || '영구 차단';
828
829 if (item.expires) {
830 const expiryDate = new Date(item.expires);
831 if (expiryDate < today) {
832 statusClass = 'log-expired';
833 expiryText += ' (만료됨)';
834 }
835 }
836
837 const row = document.createElement('tr');
838 row.className = statusClass;
839 row.innerHTML = `
840 <td>\${item.ip}</td>
841 <td>\${item.block_date || item.date}</td>
842 <td>\${item.reason || '수동 차단'}</td>
843 <td>\${expiryText}</td>
844 `;
845 tbody.appendChild(row);
846 });
847 } else {
848 if (noLogsMessage) noLogsMessage.style.display = 'block';
849 }
850 }
851 });
852 ";
853
854 wp_add_inline_script('presslearn-protection-blocked-logs-js', $blocked_logs_js);
855 }
856
857 add_action('admin_enqueue_scripts', 'presslearn_protection_admin_styles');
858 add_action('admin_enqueue_scripts', 'presslearn_protection_admin_scripts');
859