PluginProbe
AL Pack / trunk
AL Pack vtrunk
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 trunk, at includes/protection.php

1,276 lines 45.8 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 .country-tag-container {
34 display: flex;
35 width: 100%;
36 flex-wrap: wrap;
37 gap: 8px;
38 flex: 1;
39 min-height: 100px;
40 padding: 5px;
41 border: 1px solid #ddd;
42 border-radius: 4px;
43 background: #fff;
44 align-items: flex-start;
45 box-sizing: border-box;
46 }
47
48 .country-tag {
49 display: inline-flex;
50 align-items: center;
51 background: #2196F3;
52 color: white;
53 padding: 4px 10px;
54 border-radius: 16px;
55 font-size: 13px;
56 gap: 6px;
57 }
58
59 .country-tag .country-code {
60 background: rgba(255, 255, 255, 0.2);
61 padding: 2px 6px;
62 border-radius: 10px;
63 font-size: 11px;
64 font-weight: bold;
65 }
66
67 .country-tag .remove-country {
68 cursor: pointer;
69 margin-left: 4px;
70 font-weight: bold;
71 opacity: 0.8;
72 transition: opacity 0.2s;
73 }
74
75 .country-tag .remove-country:hover {
76 opacity: 1;
77 }
78
79 .country-autocomplete {
80 position: absolute;
81 top: 100%;
82 left: 0;
83 right: 0;
84 background: white;
85 border: 1px solid #ddd;
86 border-top: none;
87 max-height: 200px;
88 overflow-y: auto;
89 box-shadow: 0 2px 5px rgba(0,0,0,0.1);
90 z-index: 1000;
91 display: none;
92 }
93
94 .country-autocomplete-item {
95 padding: 10px 15px;
96 cursor: pointer;
97 display: flex;
98 align-items: center;
99 justify-content: space-between;
100 }
101
102 .country-autocomplete-item:hover,
103 .country-autocomplete-item.active {
104 background: #f0f0f0;
105 }
106
107 .country-name {
108 flex: 1;
109 }
110
111 .country-code-badge {
112 background: #e0e0e0;
113 padding: 2px 8px;
114 border-radius: 10px;
115 font-size: 12px;
116 color: #666;
117 }
118
119 .ip-add-form {
120 position: relative;
121 }
122
123 #blocked-country-input {
124 width: 100%;
125 }
126
127 .presslearn-modal {
128 position: fixed;
129 z-index: 9999;
130 left: 0;
131 top: 0;
132 width: 100%;
133 height: 100%;
134 background-color: rgba(0, 0, 0, 0.5);
135 display: flex;
136 align-items: center;
137 justify-content: center;
138 }
139
140 .presslearn-modal-content {
141 background-color: #fff;
142 width: 90%;
143 max-width: 700px;
144 border-radius: 6px;
145 box-shadow: 0 3px 20px rgba(0, 0, 0, 0.2);
146 display: flex;
147 flex-direction: column;
148 max-height: 90vh;
149 }
150
151 .presslearn-modal-header {
152 padding: 20px;
153 border-bottom: 1px solid #ddd;
154 display: flex;
155 justify-content: space-between;
156 align-items: center;
157 }
158
159 .presslearn-modal-header h3 {
160 margin: 0;
161 font-size: 20px;
162 }
163
164 .presslearn-modal-close {
165 font-size: 28px;
166 font-weight: bold;
167 cursor: pointer;
168 color: #666;
169 }
170
171 .presslearn-modal-close:hover {
172 color: #000;
173 }
174
175 .presslearn-modal-body {
176 padding: 20px;
177 overflow-y: auto;
178 flex: 1;
179 }
180
181 .presslearn-modal-footer {
182 padding: 15px 20px;
183 border-top: 1px solid #ddd;
184 text-align: right;
185 display: flex;
186 gap: 10px;
187 justify-content: flex-end;
188 }
189
190 .settings-form-row {
191 margin-bottom: 20px;
192 }
193
194 .settings-form-row label {
195 display: block;
196 margin-bottom: 5px;
197 font-weight: bold;
198 }
199
200 .settings-form-row input {
201 width: 100%;
202 padding: 8px;
203 border: 1px solid #ddd;
204 border-radius: 4px;
205 }
206
207 .modal-preview {
208 margin-top: 30px;
209 border-top: 1px solid #eee;
210 padding-top: 20px;
211 }
212
213 .modal-preview h4 {
214 margin-top: 0;
215 margin-bottom: 15px;
216 }
217
218 .modal-preview-container {
219 border: 1px dashed #ccc;
220 padding: 20px;
221 border-radius: 5px;
222 }
223
224 .ip-add-form {
225 gap: 20px;
226 margin-bottom: 20px;
227 flex-direction: column;
228 }
229
230 .ip-input-group {
231 display: flex;
232 gap: 10px;
233 margin-bottom: 10px;
234 }
235
236 .ip-input-group input {
237 flex: 1;
238 }
239
240 .ip-description {
241 color: #666;
242 font-size: 13px;
243 margin: 5px 0 0;
244 }
245
246 .ip-list-table {
247 width: 100%;
248 border-collapse: collapse;
249 }
250
251 .ip-list-table th,
252 .ip-list-table td {
253 padding: 12px 15px;
254 text-align: left;
255 border-bottom: 1px solid #eee;
256 }
257
258 .ip-list-table th {
259 background-color: #f9f9f9;
260 font-weight: 600;
261 }
262
263 .ip-list-table tr:hover {
264 background-color: #f5f5f5;
265 }
266
267 .delete-ip,
268 .delete-blocked-ip {
269 color: #d32f2f;
270 cursor: pointer;
271 background: none;
272 border: none;
273 padding: 0;
274 }
275
276 .delete-ip:hover,
277 .delete-blocked-ip:hover {
278 text-decoration: underline;
279 }
280
281 .log-filter {
282 display: flex;
283 gap: 10px;
284 margin-bottom: 20px;
285 align-items: center;
286 }
287
288 .log-list-table {
289 width: 100%;
290 border-collapse: collapse;
291 }
292
293 .log-list-table th,
294 .log-list-table td {
295 padding: 12px 15px;
296 text-align: left;
297 border-bottom: 1px solid #eee;
298 }
299
300 .log-list-table th {
301 background-color: #f9f9f9;
302 font-weight: 600;
303 }
304
305 .log-list-table tr:hover {
306 background-color: #f5f5f5;
307 }
308
309 .spinner-container {
310 display: flex;
311 justify-content: center;
312 align-items: center;
313 height: 50px;
314 }
315
316 .log-expired {
317 color: #999;
318 text-decoration: line-through;
319 }
320
321 .log-active {
322 color: #d32f2f;
323 }
324 ";
325
326 wp_add_inline_style('presslearn-protection-admin-css', $protection_css);
327 }
328
329 /**
330 * Protection admin scripts
331 */
332 function presslearn_protection_admin_scripts() {
333 if (!is_admin()) {
334 return;
335 }
336
337 $screen = get_current_screen();
338 if (!$screen || strpos($screen->id, 'presslearn-click-protection') === false) {
339 return;
340 }
341
342 wp_register_script(
343 'presslearn-protection-ip-management-js',
344 false,
345 array('jquery'),
346 PRESSLEARN_PLUGIN_VERSION,
347 true
348 );
349 wp_enqueue_script('presslearn-protection-ip-management-js');
350
351 wp_localize_script('presslearn-protection-ip-management-js', 'presslearn_protection_ajax', array(
352 'ajax_url' => admin_url('admin-ajax.php'),
353 'nonce' => wp_create_nonce('presslearn_ip_nonce')
354 ));
355
356 $ip_management_js = "
357 document.addEventListener('DOMContentLoaded', function() {
358 let allowedIPs = [];
359 let blockedIPs = [];
360
361 const ipNonce = presslearn_protection_ajax.nonce;
362 const ajaxUrl = presslearn_protection_ajax.ajax_url;
363
364 const openAllowedBtn = document.getElementById('open-allowed-ip-editor');
365 const openBlockedBtn = document.getElementById('open-blocked-ip-editor');
366 const openBlockedCountriesBtn = document.getElementById('open-blocked-countries-editor');
367
368 if (openAllowedBtn) {
369 openAllowedBtn.addEventListener('click', function() {
370 loadAllowedIPs();
371 document.getElementById('allowed-ip-modal').style.display = 'flex';
372 });
373 }
374
375 if (openBlockedBtn) {
376 openBlockedBtn.addEventListener('click', function() {
377 loadBlockedIPs();
378 document.getElementById('blocked-ip-modal').style.display = 'flex';
379 });
380 }
381
382 if (openBlockedCountriesBtn) {
383 openBlockedCountriesBtn.addEventListener('click', function() {
384 loadBlockedCountries();
385 document.getElementById('blocked-countries-editor').style.display = 'flex';
386 });
387 }
388
389 document.addEventListener('click', function(e) {
390 if (e.target.classList.contains('presslearn-modal-close') ||
391 e.target.id === 'close-modal-button') {
392 document.getElementById('allowed-ip-modal').style.display = 'none';
393 }
394 if (e.target.classList.contains('presslearn-modal-close') ||
395 e.target.id === 'close-blocked-modal-button') {
396 document.getElementById('blocked-ip-modal').style.display = 'none';
397 }
398 if (e.target.id === 'allowed-ip-modal') {
399 document.getElementById('allowed-ip-modal').style.display = 'none';
400 }
401 if (e.target.id === 'blocked-ip-modal') {
402 document.getElementById('blocked-ip-modal').style.display = 'none';
403 }
404 });
405
406 const addIpBtn = document.getElementById('add-ip-button');
407 const addBlockedIpBtn = document.getElementById('add-blocked-ip-button');
408 const newIpInput = document.getElementById('new-ip-address');
409 const newBlockedIpInput = document.getElementById('new-blocked-ip');
410
411 if (addIpBtn) {
412 addIpBtn.addEventListener('click', addNewIP);
413 }
414
415 if (addBlockedIpBtn) {
416 addBlockedIpBtn.addEventListener('click', addNewBlockedIP);
417 }
418
419 if (newIpInput) {
420 newIpInput.addEventListener('keypress', function(e) {
421 if (e.which === 13) {
422 addNewIP();
423 e.preventDefault();
424 }
425 });
426 }
427
428 if (newBlockedIpInput) {
429 newBlockedIpInput.addEventListener('keypress', function(e) {
430 if (e.which === 13) {
431 addNewBlockedIP();
432 e.preventDefault();
433 }
434 });
435 }
436
437 function loadAllowedIPs() {
438 const tbody = document.getElementById('allowed-ip-list');
439 if (tbody) {
440 tbody.innerHTML = '<tr><td colspan=\"3\" style=\"text-align:center;\"><div class=\"spinner-container\"><span class=\"spinner is-active\"></span></div></td></tr>';
441 }
442
443 fetch(ajaxUrl, {
444 method: 'POST',
445 headers: {
446 'Content-Type': 'application/x-www-form-urlencoded',
447 },
448 body: new URLSearchParams({
449 action: 'presslearn_get_allowed_ips',
450 nonce: ipNonce
451 })
452 })
453 .then(response => response.json())
454 .then(data => {
455 if (data.success) {
456 allowedIPs = data.data.ips || [];
457 renderIPTable();
458 } else {
459 alert('허용 IP 목록을 불러오는데 실패했습니다.');
460 allowedIPs = [];
461 renderIPTable();
462 }
463 })
464 .catch(error => {
465 alert('서버 통신 오류가 발생했습니다.');
466 allowedIPs = [];
467 renderIPTable();
468 });
469 }
470
471 function loadBlockedIPs() {
472 const tbody = document.getElementById('blocked-ip-list');
473 if (tbody) {
474 tbody.innerHTML = '<tr><td colspan=\"3\" style=\"text-align:center;\"><div class=\"spinner-container\"><span class=\"spinner is-active\"></span></div></td></tr>';
475 }
476
477 fetch(ajaxUrl, {
478 method: 'POST',
479 headers: {
480 'Content-Type': 'application/x-www-form-urlencoded',
481 },
482 body: new URLSearchParams({
483 action: 'presslearn_get_blocked_ips',
484 nonce: ipNonce
485 })
486 })
487 .then(response => response.json())
488 .then(data => {
489 if (data.success) {
490 blockedIPs = data.data.ips || [];
491 renderBlockedIPTable();
492 } else {
493 alert('차단 IP 목록을 불러오는데 실패했습니다.');
494 blockedIPs = [];
495 renderBlockedIPTable();
496 }
497 })
498 .catch(error => {
499 alert('서버 통신 오류가 발생했습니다.');
500 blockedIPs = [];
501 renderBlockedIPTable();
502 });
503 }
504
505 function renderIPTable() {
506 const tbody = document.getElementById('allowed-ip-list');
507 const noIpsMessage = document.getElementById('no-ips-message');
508
509 if (!tbody) return;
510
511 tbody.innerHTML = '';
512
513 if (allowedIPs.length > 0) {
514 if (noIpsMessage) noIpsMessage.style.display = 'none';
515
516 allowedIPs.forEach((item) => {
517 const row = document.createElement('tr');
518 row.innerHTML = `
519 <td>\${item.ip}</td>
520 <td>\${item.date}</td>
521 <td>
522 <button type=\"button\" class=\"delete-ip\" data-ip=\"\${item.ip}\">삭제</button>
523 </td>
524 `;
525 tbody.appendChild(row);
526 });
527
528 document.querySelectorAll('.delete-ip').forEach(btn => {
529 btn.addEventListener('click', function() {
530 const ip = this.getAttribute('data-ip');
531 deleteIP(ip);
532 });
533 });
534 } else {
535 if (noIpsMessage) noIpsMessage.style.display = 'block';
536 }
537 }
538
539 function renderBlockedIPTable() {
540 const tbody = document.getElementById('blocked-ip-list');
541 const noBlockedIpsMessage = document.getElementById('no-blocked-ips-message');
542
543 if (!tbody) return;
544
545 tbody.innerHTML = '';
546
547 if (blockedIPs.length > 0) {
548 if (noBlockedIpsMessage) noBlockedIpsMessage.style.display = 'none';
549
550 blockedIPs.forEach((item) => {
551 const row = document.createElement('tr');
552 const expiryText = item.expires ? item.expires : '영구 차단';
553 const reasonText = item.reason || '수동 차단';
554
555 row.innerHTML = `
556 <td>\${item.ip}</td>
557 <td>\${item.date}</td>
558 <td>
559 <button type=\"button\" class=\"delete-blocked-ip\" data-ip=\"\${item.ip}\" title=\"이 IP를 차단 목록에서 제거하고 로컬 스토리지를 리�
560 �합니다\">삭제</button>
561 </td>
562 `;
563 tbody.appendChild(row);
564 });
565
566 document.querySelectorAll('.delete-blocked-ip').forEach(btn => {
567 btn.addEventListener('click', function() {
568 const ip = this.getAttribute('data-ip');
569 deleteBlockedIP(ip);
570 });
571 });
572 } else {
573 if (noBlockedIpsMessage) noBlockedIpsMessage.style.display = 'block';
574 }
575 }
576
577 function addNewIP() {
578 const newIpInput = document.getElementById('new-ip-address');
579 if (!newIpInput) return;
580
581 const newIP = newIpInput.value.trim();
582
583 if (!isValidIP(newIP)) {
584 alert('유효한 IP 주소를 �
585 력해주세요.');
586 return;
587 }
588
589 fetch(ajaxUrl, {
590 method: 'POST',
591 headers: {
592 'Content-Type': 'application/x-www-form-urlencoded',
593 },
594 body: new URLSearchParams({
595 action: 'presslearn_add_allowed_ip',
596 nonce: ipNonce,
597 ip: newIP
598 })
599 })
600 .then(response => response.json())
601 .then(data => {
602 if (data.success) {
603 alert('IP가 성공적으로 허용 목록에 추가되었습니다.');
604 allowedIPs = data.data.ips;
605 renderIPTable();
606 newIpInput.value = '';
607 newIpInput.focus();
608 } else {
609 alert(data.data.message || '허용 IP 추가에 실패했습니다.');
610 }
611 })
612 .catch(error => {
613 alert('서버 통신 오류가 발생했습니다.');
614 });
615 }
616
617 function addNewBlockedIP() {
618 const newBlockedIpInput = document.getElementById('new-blocked-ip');
619 const permanentBlockCheckbox = document.getElementById('permanent-block-checkbox');
620 if (!newBlockedIpInput) return;
621
622 const newIP = newBlockedIpInput.value.trim();
623 const isPermanent = permanentBlockCheckbox ? permanentBlockCheckbox.checked : false;
624
625 if (!isValidIP(newIP)) {
626 alert('유효한 IP 주소를 �
627 력해주세요.');
628 return;
629 }
630
631 fetch(ajaxUrl, {
632 method: 'POST',
633 headers: {
634 'Content-Type': 'application/x-www-form-urlencoded',
635 },
636 body: new URLSearchParams({
637 action: 'presslearn_add_blocked_ip',
638 nonce: ipNonce,
639 ip: newIP,
640 permanent: isPermanent ? '1' : '0'
641 })
642 })
643 .then(response => response.json())
644 .then(data => {
645 if (data.success) {
646 alert('IP가 성공적으로 차단되었습니다.');
647 blockedIPs = data.data.ips;
648 renderBlockedIPTable();
649 newBlockedIpInput.value = '';
650 if (permanentBlockCheckbox) permanentBlockCheckbox.checked = false;
651 newBlockedIpInput.focus();
652 } else {
653 alert(data.data.message || '차단 IP 추가에 실패했습니다.');
654 }
655 })
656 .catch(error => {
657 alert('서버 통신 오류가 발생했습니다.');
658 });
659 }
660
661 function deleteIP(ip) {
662 if (confirm('이 IP를 허용 목록에서 삭제하시겠습니까?')) {
663 fetch(ajaxUrl, {
664 method: 'POST',
665 headers: {
666 'Content-Type': 'application/x-www-form-urlencoded',
667 },
668 body: new URLSearchParams({
669 action: 'presslearn_delete_allowed_ip',
670 nonce: ipNonce,
671 ip: ip
672 })
673 })
674 .then(response => response.json())
675 .then(data => {
676 if (data.success) {
677 alert('IP가 성공적으로 허용 목록에서 삭제되었습니다.');
678 allowedIPs = data.data.ips;
679 renderIPTable();
680 } else {
681 alert(data.data.message || '허용 IP 삭제에 실패했습니다.');
682 }
683 })
684 .catch(error => {
685 alert('서버 통신 오류가 발생했습니다.');
686 });
687 }
688 }
689
690 function deleteBlockedIP(ip) {
691 if (confirm('이 IP를 차단 목록에서 삭제하시겠습니까?')) {
692 fetch(ajaxUrl, {
693 method: 'POST',
694 headers: {
695 'Content-Type': 'application/x-www-form-urlencoded',
696 },
697 body: new URLSearchParams({
698 action: 'presslearn_delete_blocked_ip',
699 nonce: ipNonce,
700 ip: ip
701 })
702 })
703 .then(response => response.json())
704 .then(data => {
705 if (data.success) {
706 alert('IP가 성공적으로 차단 목록에서 삭제되었습니다.');
707 blockedIPs = data.data.ips;
708 renderBlockedIPTable();
709 } else {
710 alert(data.data.message || '차단 IP 삭제에 실패했습니다.');
711 }
712 })
713 .catch(error => {
714 alert('서버 통신 오류가 발생했습니다.');
715 });
716 }
717 }
718
719 function isValidIP(ip) {
720 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]?)$/;
721 return regex.test(ip);
722 }
723
724 // 국가별 차단 관련 코드
725 const countries = [
726 { name: '대한민국', code: 'KR' },
727 { name: '북한', code: 'KP' },
728 { name: '중국', code: 'CN' },
729 { name: '일본', code: 'JP' },
730 { name: '미국', code: 'US' },
731 { name: '러시아', code: 'RU' },
732 { name: '베트남', code: 'VN' },
733 { name: '태국', code: 'TH' },
734 { name: '인도네시아', code: 'ID' },
735 { name: '필리핀', code: 'PH' },
736 { name: '말레이시아', code: 'MY' },
737 { name: '싱가포르', code: 'SG' },
738 { name: '인도', code: 'IN' },
739 { name: '파키스탄', code: 'PK' },
740 { name: '방글라데시', code: 'BD' },
741 { name: '네팔', code: 'NP' },
742 { name: '스리랑카', code: 'LK' },
743 { name: '몽골', code: 'MN' },
744 { name: '대만', code: 'TW' },
745 { name: '홍콩', code: 'HK' },
746 { name: '마카오', code: 'MO' },
747 { name: '캐나다', code: 'CA' },
748 { name: '멕시코', code: 'MX' },
749 { name: '브라질', code: 'BR' },
750 { name: '아르헨티나', code: 'AR' },
751 { name: '칠레', code: 'CL' },
752 { name: '페루', code: 'PE' },
753 { name: '콜롬비아', code: 'CO' },
754 { name: '베네수엘라', code: 'VE' },
755 { name: '영국', code: 'GB' },
756 { name: '프랑스', code: 'FR' },
757 { name: '�
758 일', code: 'DE' },
759 { name: '이탈리아', code: 'IT' },
760 { name: '스페인', code: 'ES' },
761 { name: '네덜란드', code: 'NL' },
762 { name: '벨기에', code: 'BE' },
763 { name: '스위스', code: 'CH' },
764 { name: '스웨덴', code: 'SE' },
765 { name: '�
766 �르웨이', code: 'NO' },
767 { name: '덴마크', code: 'DK' },
768 { name: '핀란드', code: 'FI' },
769 { name: '폴란드', code: 'PL' },
770 { name: '체코', code: 'CZ' },
771 { name: '헝가리', code: 'HU' },
772 { name: '루마니아', code: 'RO' },
773 { name: '불가리아', code: 'BG' },
774 { name: '그리스', code: 'GR' },
775 { name: '터키', code: 'TR' },
776 { name: '우크라이나', code: 'UA' },
777 { name: '벨라루스', code: 'BY' },
778 { name: '카자흐스탄', code: 'KZ' },
779 { name: '우즈베키스탄', code: 'UZ' },
780 { name: '이란', code: 'IR' },
781 { name: '이라크', code: 'IQ' },
782 { name: '사우디아라비아', code: 'SA' },
783 { name: '아랍에미리트', code: 'AE' },
784 { name: '이스라엘', code: 'IL' },
785 { name: '이집트', code: 'EG' },
786 { name: '남아프리카', code: 'ZA' },
787 { name: '나이지리아', code: 'NG' },
788 { name: '케냐', code: 'KE' },
789 { name: '에티오피아', code: 'ET' },
790 { name: '가나', code: 'GH' },
791 { name: '모로코', code: 'MA' },
792 { name: '알제리', code: 'DZ' },
793 { name: '튀니지', code: 'TN' },
794 { name: '호주', code: 'AU' },
795 { name: '뉴질랜드', code: 'NZ' },
796 { name: '피지', code: 'FJ' }
797 ];
798
799 let blockedCountries = [];
800 let activeAutocompleteIndex = -1;
801
802 const countryInput = document.getElementById('blocked-country-input');
803 const countryAutocomplete = document.getElementById('country-autocomplete');
804 const countryTagsContainer = document.getElementById('blocked-countries-tags');
805 const saveBlockedCountriesBtn = document.getElementById('save-blocked-countries');
806 const closeCountriesModalBtn = document.getElementById('close-blocked-countries-modal');
807 const blockedCountriesModal = document.getElementById('blocked-countries-editor');
808
809 if (closeCountriesModalBtn) {
810 closeCountriesModalBtn.addEventListener('click', function() {
811 blockedCountriesModal.style.display = 'none';
812 });
813 }
814
815 if (blockedCountriesModal) {
816 blockedCountriesModal.addEventListener('click', function(e) {
817 if (e.target === blockedCountriesModal || e.target.classList.contains('presslearn-modal-close')) {
818 blockedCountriesModal.style.display = 'none';
819 }
820 });
821 }
822
823 function loadBlockedCountries() {
824 fetch(ajaxUrl, {
825 method: 'POST',
826 headers: {
827 'Content-Type': 'application/x-www-form-urlencoded',
828 },
829 body: new URLSearchParams({
830 action: 'presslearn_get_blocked_countries',
831 nonce: ipNonce
832 })
833 })
834 .then(response => response.json())
835 .then(data => {
836 if (data.success && data.data.countries) {
837 blockedCountries = data.data.countries;
838 renderCountryTags();
839 }
840 });
841 }
842
843 function renderCountryTags() {
844 countryTagsContainer.innerHTML = '';
845 blockedCountries.forEach(country => {
846 const tag = document.createElement('span');
847 tag.className = 'country-tag';
848 tag.innerHTML =
849 country.name +
850 '<span class=\"country-code\">' + country.code + '</span>' +
851 '<span class=\"remove-country\" data-code=\"' + country.code + '\">×</span>';
852 countryTagsContainer.appendChild(tag);
853 });
854 }
855
856 function showAutocomplete(searchTerm) {
857 const filtered = countries.filter(country =>
858 country.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
859 country.code.toLowerCase().includes(searchTerm.toLowerCase())
860 ).filter(country =>
861 !blockedCountries.some(blocked => blocked.code === country.code)
862 );
863
864 countryAutocomplete.innerHTML = '';
865 activeAutocompleteIndex = -1;
866
867 if (filtered.length > 0 && searchTerm.length > 0) {
868 countryAutocomplete.style.display = 'block';
869 filtered.forEach((country, index) => {
870 const item = document.createElement('div');
871 item.className = 'country-autocomplete-item';
872 item.setAttribute('data-index', index);
873 item.innerHTML =
874 '<span class=\"country-name\">' + country.name + '</span>' +
875 '<span class=\"country-code-badge\">' + country.code + '</span>';
876 item.addEventListener('click', () => selectCountry(country));
877 countryAutocomplete.appendChild(item);
878 });
879 } else {
880 countryAutocomplete.style.display = 'none';
881 }
882 }
883
884 function selectCountry(country) {
885 if (!blockedCountries.some(blocked => blocked.code === country.code)) {
886 blockedCountries.push(country);
887 renderCountryTags();
888 countryInput.value = '';
889 countryAutocomplete.style.display = 'none';
890 }
891 }
892
893 if (countryInput) {
894 countryInput.addEventListener('input', (e) => {
895 showAutocomplete(e.target.value);
896 });
897
898 countryInput.addEventListener('keydown', (e) => {
899 const items = countryAutocomplete.querySelectorAll('.country-autocomplete-item');
900
901 if (e.key === 'ArrowDown') {
902 e.preventDefault();
903 activeAutocompleteIndex = Math.min(activeAutocompleteIndex + 1, items.length - 1);
904 updateActiveItem(items);
905 } else if (e.key === 'ArrowUp') {
906 e.preventDefault();
907 activeAutocompleteIndex = Math.max(activeAutocompleteIndex - 1, -1);
908 updateActiveItem(items);
909 } else if (e.key === 'Enter') {
910 e.preventDefault();
911 if (activeAutocompleteIndex >= 0 && items[activeAutocompleteIndex]) {
912 items[activeAutocompleteIndex].click();
913 }
914 } else if (e.key === 'Escape') {
915 countryAutocomplete.style.display = 'none';
916 activeAutocompleteIndex = -1;
917 }
918 });
919
920 countryInput.addEventListener('blur', () => {
921 setTimeout(() => {
922 countryAutocomplete.style.display = 'none';
923 }, 200);
924 });
925 }
926
927 function updateActiveItem(items) {
928 items.forEach((item, index) => {
929 if (index === activeAutocompleteIndex) {
930 item.classList.add('active');
931 } else {
932 item.classList.remove('active');
933 }
934 });
935 }
936
937 if (countryTagsContainer) {
938 countryTagsContainer.addEventListener('click', (e) => {
939 if (e.target.classList.contains('remove-country')) {
940 const code = e.target.getAttribute('data-code');
941 blockedCountries = blockedCountries.filter(country => country.code !== code);
942 renderCountryTags();
943 }
944 });
945 }
946
947 if (saveBlockedCountriesBtn) {
948 saveBlockedCountriesBtn.addEventListener('click', () => {
949 fetch(ajaxUrl, {
950 method: 'POST',
951 headers: {
952 'Content-Type': 'application/x-www-form-urlencoded',
953 },
954 body: new URLSearchParams({
955 action: 'presslearn_save_blocked_countries',
956 nonce: ipNonce,
957 countries: JSON.stringify(blockedCountries)
958 })
959 })
960 .then(response => response.json())
961 .then(data => {
962 if (data.success) {
963 alert('차단 국가 목록이 저장되었습니다.');
964 blockedCountriesModal.style.display = 'none';
965 } else {
966 alert(data.data.message || '저장에 실패했습니다.');
967 }
968 });
969 });
970 }
971 });
972 ";
973
974 wp_add_inline_script('presslearn-protection-ip-management-js', $ip_management_js);
975
976 wp_register_script(
977 'presslearn-protection-modal-settings-js',
978 false,
979 array('jquery'),
980 PRESSLEARN_PLUGIN_VERSION,
981 true
982 );
983 wp_enqueue_script('presslearn-protection-modal-settings-js');
984
985 $modal_settings_js = "
986 document.addEventListener('DOMContentLoaded', function() {
987 const openModalSettingsBtn = document.getElementById('open-modal-settings');
988 const closeModalSettingsBtn = document.getElementById('close-modal-settings');
989 const saveModalSettingsBtn = document.getElementById('save-modal-settings');
990
991 if (openModalSettingsBtn) {
992 openModalSettingsBtn.addEventListener('click', function() {
993 document.getElementById('modal-settings-editor').style.display = 'flex';
994 });
995 }
996
997 if (closeModalSettingsBtn) {
998 closeModalSettingsBtn.addEventListener('click', function() {
999 document.getElementById('modal-settings-editor').style.display = 'none';
1000 });
1001 }
1002
1003 document.addEventListener('click', function(e) {
1004 if (e.target.classList.contains('presslearn-modal-close')) {
1005 document.getElementById('modal-settings-editor').style.display = 'none';
1006 }
1007 if (e.target.id === 'modal-settings-editor') {
1008 document.getElementById('modal-settings-editor').style.display = 'none';
1009 }
1010 });
1011
1012 const modalTitleInput = document.getElementById('modal_title');
1013 const modalMessageInput = document.getElementById('modal_message');
1014 const modalSubmessageInput = document.getElementById('modal_submessage');
1015 const modalButtonTextInput = document.getElementById('modal_button_text');
1016
1017 if (modalTitleInput) {
1018 modalTitleInput.addEventListener('input', function() {
1019 const previewTitle = document.getElementById('preview-title');
1020 if (previewTitle) previewTitle.textContent = this.value;
1021 const form = document.getElementById('click-protection-settings-form');
1022 if (form) {
1023 const hiddenInput = form.querySelector('input[name=\"modal_title\"]');
1024 if (hiddenInput) hiddenInput.value = this.value;
1025 }
1026 });
1027 }
1028
1029 if (modalMessageInput) {
1030 modalMessageInput.addEventListener('input', function() {
1031 const previewMessage = document.getElementById('preview-message');
1032 if (previewMessage) previewMessage.textContent = this.value;
1033 const form = document.getElementById('click-protection-settings-form');
1034 if (form) {
1035 const hiddenInput = form.querySelector('input[name=\"modal_message\"]');
1036 if (hiddenInput) hiddenInput.value = this.value;
1037 }
1038 });
1039 }
1040
1041 if (modalSubmessageInput) {
1042 modalSubmessageInput.addEventListener('input', function() {
1043 const previewSubmessage = document.getElementById('preview-submessage');
1044 if (previewSubmessage) previewSubmessage.textContent = this.value;
1045 const form = document.getElementById('click-protection-settings-form');
1046 if (form) {
1047 const hiddenInput = form.querySelector('input[name=\"modal_submessage\"]');
1048 if (hiddenInput) hiddenInput.value = this.value;
1049 }
1050 });
1051 }
1052
1053 if (modalButtonTextInput) {
1054 modalButtonTextInput.addEventListener('input', function() {
1055 const previewButton = document.getElementById('preview-button');
1056 if (previewButton) previewButton.textContent = this.value;
1057 const form = document.getElementById('click-protection-settings-form');
1058 if (form) {
1059 const hiddenInput = form.querySelector('input[name=\"modal_button_text\"]');
1060 if (hiddenInput) hiddenInput.value = this.value;
1061 }
1062 });
1063 }
1064
1065 if (saveModalSettingsBtn) {
1066 saveModalSettingsBtn.addEventListener('click', function() {
1067 const modalTitle = modalTitleInput ? modalTitleInput.value : '';
1068 const modalMessage = modalMessageInput ? modalMessageInput.value : '';
1069 const modalSubmessage = modalSubmessageInput ? modalSubmessageInput.value : '';
1070 const modalButtonText = modalButtonTextInput ? modalButtonTextInput.value : '';
1071
1072 const form = document.getElementById('click-protection-settings-form');
1073 if (form) {
1074 const titleInput = form.querySelector('input[name=\"modal_title\"]');
1075 const messageInput = form.querySelector('input[name=\"modal_message\"]');
1076 const submessageInput = form.querySelector('input[name=\"modal_submessage\"]');
1077 const buttonTextInput = form.querySelector('input[name=\"modal_button_text\"]');
1078
1079 if (titleInput) titleInput.value = modalTitle;
1080 if (messageInput) messageInput.value = modalMessage;
1081 if (submessageInput) submessageInput.value = modalSubmessage;
1082 if (buttonTextInput) buttonTextInput.value = modalButtonText;
1083 }
1084
1085 document.getElementById('modal-settings-editor').style.display = 'none';
1086 alert('모달 설정이 적용되었습니다. 변경사항을 저장하려면 \"저장하기\" 버튼을 클릭하세요.');
1087 });
1088 }
1089 });
1090 ";
1091
1092 wp_add_inline_script('presslearn-protection-modal-settings-js', $modal_settings_js);
1093
1094 wp_register_script(
1095 'presslearn-protection-blocked-logs-js',
1096 false,
1097 array('jquery'),
1098 PRESSLEARN_PLUGIN_VERSION,
1099 true
1100 );
1101 wp_enqueue_script('presslearn-protection-blocked-logs-js');
1102
1103 wp_localize_script('presslearn-protection-blocked-logs-js', 'presslearn_logs_ajax', array(
1104 'ajax_url' => admin_url('admin-ajax.php'),
1105 'nonce' => wp_create_nonce('presslearn_ip_nonce')
1106 ));
1107
1108 $blocked_logs_js = "
1109 document.addEventListener('DOMContentLoaded', function() {
1110 const ajaxUrl = presslearn_logs_ajax.ajax_url;
1111 const ipNonce = presslearn_logs_ajax.nonce;
1112
1113 const openLogModalBtn = document.getElementById('open-blocked-log-modal');
1114 const closeLogModalBtn = document.getElementById('close-log-modal-button');
1115 const refreshLogBtn = document.getElementById('refresh-log');
1116
1117 if (openLogModalBtn) {
1118 openLogModalBtn.addEventListener('click', function() {
1119 loadBlockedLogs();
1120 document.getElementById('blocked-log-modal').style.display = 'flex';
1121 });
1122 }
1123
1124 if (closeLogModalBtn) {
1125 closeLogModalBtn.addEventListener('click', function() {
1126 document.getElementById('blocked-log-modal').style.display = 'none';
1127 });
1128 }
1129
1130 if (refreshLogBtn) {
1131 refreshLogBtn.addEventListener('click', loadBlockedLogs);
1132 }
1133
1134 document.addEventListener('click', function(e) {
1135 if (e.target.classList.contains('presslearn-modal-close')) {
1136 document.getElementById('blocked-log-modal').style.display = 'none';
1137 }
1138 if (e.target.id === 'blocked-log-modal') {
1139 document.getElementById('blocked-log-modal').style.display = 'none';
1140 }
1141 });
1142
1143 function loadBlockedLogs() {
1144 const logList = document.getElementById('blocked-log-list');
1145 const noLogsMessage = document.getElementById('no-blocked-logs-message');
1146 const logLoading = document.getElementById('log-loading');
1147 const periodSelect = document.getElementById('log-filter-period');
1148
1149 if (logList) logList.innerHTML = '';
1150 if (noLogsMessage) noLogsMessage.style.display = 'none';
1151 if (logLoading) logLoading.style.display = 'block';
1152
1153 const period = periodSelect ? periodSelect.value : '30';
1154
1155 fetch(presslearn_logs_ajax.ajax_url, {
1156 method: 'POST',
1157 headers: {
1158 'Content-Type': 'application/x-www-form-urlencoded',
1159 },
1160 body: new URLSearchParams({
1161 action: 'presslearn_get_blocked_logs',
1162 nonce: presslearn_logs_ajax.nonce,
1163 period: period
1164 })
1165 })
1166 .then(response => response.json())
1167 .then(data => {
1168 if (logLoading) logLoading.style.display = 'none';
1169
1170 if (data.success) {
1171 const logs = data.data.logs || [];
1172 renderLogTable(logs);
1173 } else {
1174 alert('차단 로그를 불러오는데 실패했습니다.');
1175 if (noLogsMessage) noLogsMessage.style.display = 'block';
1176 }
1177 })
1178 .catch(error => {
1179 if (logLoading) logLoading.style.display = 'none';
1180 alert('서버 통신 오류가 발생했습니다.');
1181 if (noLogsMessage) noLogsMessage.style.display = 'block';
1182 });
1183 }
1184
1185 function renderLogTable(logs) {
1186 const tbody = document.getElementById('blocked-log-list');
1187 const noLogsMessage = document.getElementById('no-blocked-logs-message');
1188
1189 if (!tbody) return;
1190
1191 tbody.innerHTML = '';
1192
1193 if (logs.length > 0) {
1194 if (noLogsMessage) noLogsMessage.style.display = 'none';
1195
1196 const today = new Date();
1197
1198 logs.forEach((item) => {
1199 let statusClass = 'log-active';
1200 let expiryText = item.expires || '영구 차단';
1201 let actionButton = '';
1202
1203 if (item.expires) {
1204 const expiryDate = new Date(item.expires);
1205 if (expiryDate < today) {
1206 statusClass = 'log-expired';
1207 expiryText += ' (만료됨)';
1208 } else {
1209 actionButton = `<button type=\"button\" class=\"unblock-ip-from-log\" data-ip=\"\${item.ip}\" style=\"color: #d32f2f; background: none; border: none; cursor: pointer; text-decoration: underline;\">해지</button>`;
1210 }
1211 } else {
1212 actionButton = `<button type=\"button\" class=\"unblock-ip-from-log\" data-ip=\"\${item.ip}\" style=\"color: #d32f2f; background: none; border: none; cursor: pointer; text-decoration: underline;\">해지</button>`;
1213 }
1214
1215 const row = document.createElement('tr');
1216 row.className = statusClass;
1217 row.innerHTML = `
1218 <td>\${item.ip}</td>
1219 <td>\${item.block_date || item.date}</td>
1220 <td>\${item.reason || '수동 차단'}</td>
1221 <td>\${expiryText}</td>
1222 <td>\${actionButton}</td>
1223 `;
1224 tbody.appendChild(row);
1225 });
1226
1227 document.querySelectorAll('.unblock-ip-from-log').forEach(btn => {
1228 btn.addEventListener('click', function() {
1229 const ip = this.getAttribute('data-ip');
1230 unblockIPFromLog(ip);
1231 });
1232 });
1233 } else {
1234 if (noLogsMessage) noLogsMessage.style.display = 'block';
1235 }
1236 }
1237
1238 function unblockIPFromLog(ip) {
1239 if (confirm('이 IP의 차단을 해제하시겠습니까? 해당 IP의 브라우저 제한도 함께 해제됩니다.')) {
1240 fetch(ajaxUrl, {
1241 method: 'POST',
1242 headers: {
1243 'Content-Type': 'application/x-www-form-urlencoded',
1244 },
1245 body: new URLSearchParams({
1246 action: 'presslearn_unblock_ip_with_reset',
1247 nonce: ipNonce,
1248 ip: ip
1249 })
1250 })
1251 .then(response => response.json())
1252 .then(data => {
1253 if (data.success) {
1254 alert('IP 차단이 성공적으로 해제되었습니다.');
1255 loadBlockedLogs();
1256 if (document.getElementById('blocked-ip-modal').style.display === 'flex') {
1257 loadBlockedIPs();
1258 }
1259 } else {
1260 alert(data.data.message || 'IP 차단 해제에 실패했습니다.');
1261 }
1262 })
1263 .catch(error => {
1264 alert('서버 통신 오류가 발생했습니다.');
1265 });
1266 }
1267 }
1268 });
1269 ";
1270
1271 wp_add_inline_script('presslearn-protection-blocked-logs-js', $blocked_logs_js);
1272 }
1273
1274 add_action('admin_enqueue_scripts', 'presslearn_protection_admin_styles');
1275 add_action('admin_enqueue_scripts', 'presslearn_protection_admin_scripts');
1276