diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..81182c3 --- /dev/null +++ b/.htaccess @@ -0,0 +1,3 @@ + + Deny from all + diff --git a/api-comments.php b/api-comments.php index 6eda297..e78d2d4 100644 --- a/api-comments.php +++ b/api-comments.php @@ -5,8 +5,41 @@ header('Content-Type: application/json'); // Dołączamy połączenie do bazy (upewnij się, że plik db_connect.php istnieje i ma poprawne dane) require_once 'db_connect.php'; +session_start(); + $action = $_GET['action'] ?? ''; +// 0. AUTORYZACJA (LOGOWANIE) +if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'auth') { + $input = json_decode(file_get_contents('php://input'), true); + $pass = $input['password'] ?? ''; + + // $PROTOTYPE_PASSWORD jest zdefiniowane w db_connect.php + if (isset($PROTOTYPE_PASSWORD) && $pass === $PROTOTYPE_PASSWORD) { + $_SESSION['prototype_auth'] = true; + echo json_encode(['status' => 'success']); + } else { + echo json_encode(['status' => 'error', 'message' => 'Nieprawidłowe hasło']); + } + exit; +} + +// 0. SPRAWDZENIE SESJI (Opcjonalne dla list, wymagane dla add) +function isAuthorized() +{ + return !empty($_SESSION['prototype_auth']); +} + +// 0.5 SPRAWDZENIE STANU AUTORYZACJI (GET) - dla frontendowego odtworzenia sesji +if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'check_auth') { + if (isAuthorized()) { + echo json_encode(['status' => 'success']); + } else { + echo json_encode(['status' => 'error', 'message' => 'Brak sesji']); + } + exit; +} + // 1. POBIERANIE KOMENTARZY (GET) if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'list') { $pagePath = $_GET['page_path'] ?? ''; @@ -20,6 +53,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'list') { $stmt = $pdo->prepare("SELECT * FROM prototype_comments WHERE page_path = ? ORDER BY created_at DESC"); $stmt->execute([$pagePath]); $comments = $stmt->fetchAll(); + + // XSS PROTECTION: Escaping danych przed wysłaniem + foreach ($comments as &$c) { + $c['author'] = htmlspecialchars($c['author'] ?? '', ENT_QUOTES, 'UTF-8'); + $c['comment'] = htmlspecialchars($c['comment'] ?? '', ENT_QUOTES, 'UTF-8'); + // Selector musi zostać oryginalny, bo JS go używa do querySelector! + // $c['dom_selector'] = htmlspecialchars($c['dom_selector'] ?? '', ENT_QUOTES, 'UTF-8'); + } + echo json_encode(['status' => 'success', 'data' => $comments]); } catch (PDOException $e) { echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); @@ -29,6 +71,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'list') { // 2. DODAWANIE KOMENTARZA (POST) if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'add') { + // Wymagana autoryzacja + if (!isAuthorized()) { + echo json_encode(['status' => 'error', 'message' => 'Brak autoryzacji. Odśwież stronę i podaj hasło.']); + exit; + } + // Odczyt danych JSON z body requestu $input = json_decode(file_get_contents('php://input'), true); @@ -52,4 +100,55 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'add') { } exit; } + +// 3. ROZWIĄZYWANIE KOMENTARZA (POST) +if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'resolve') { + if (!isAuthorized()) { + echo json_encode(['status' => 'error', 'message' => 'Brak autoryzacji']); + exit; + } + + $input = json_decode(file_get_contents('php://input'), true); + $id = $input['id'] ?? 0; + + if (!$id) { + echo json_encode(['status' => 'error', 'message' => 'Brak ID']); + exit; + } + + try { + // Zmieniamy flagę is_resolved na 1 + $stmt = $pdo->prepare("UPDATE prototype_comments SET is_resolved = 1 WHERE id = ?"); + $stmt->execute([$id]); + echo json_encode(['status' => 'success']); + } catch (PDOException $e) { + echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); + } + exit; +} + +// 4. USUWANIE KOMENTARZA (POST) +if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'delete') { + if (!isAuthorized()) { + echo json_encode(['status' => 'error', 'message' => 'Brak autoryzacji']); + exit; + } + + $input = json_decode(file_get_contents('php://input'), true); + $id = $input['id'] ?? 0; + + if (!$id) { + echo json_encode(['status' => 'error', 'message' => 'Brak ID']); + exit; + } + + try { + $stmt = $pdo->prepare("DELETE FROM prototype_comments WHERE id = ?"); + $stmt->execute([$id]); + echo json_encode(['status' => 'success']); + } catch (PDOException $e) { + echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); + } + exit; +} ?> \ No newline at end of file diff --git a/assets/css/comments.css b/assets/css/comments.css index f6eeef3..43abfda 100644 --- a/assets/css/comments.css +++ b/assets/css/comments.css @@ -87,7 +87,7 @@ input:checked+.slider:before { .comment-marker:hover { z-index: 10001; - transform: rotate(-45deg) scale(1.2); + transform: rotate(-45deg) translate(-50%, -100%) scale(1.2); } .comment-marker.resolved { diff --git a/assets/js/comments.js b/assets/js/comments.js index aecba26..f1446a5 100644 --- a/assets/js/comments.js +++ b/assets/js/comments.js @@ -45,11 +45,43 @@ document.body.insertAdjacentHTML('beforeend', modalHtml); // Listenery UI - document.getElementById('comment-toggle').addEventListener('change', function (e) { - isCommentMode = e.target.checked; - toggleCommentMode(isCommentMode); + // Listenery UI + const toggle = document.getElementById('comment-toggle'); + + toggle.addEventListener('change', function (e) { + if (e.target.checked) { + // Próba włączenia - autoryzacja + checkAuth().then(ok => { + if (ok) { + isCommentMode = true; + localStorage.setItem('magico_comment_mode', 'true'); + toggleCommentMode(true); + } else { + e.target.checked = false; // Cofnij switch + } + }); + } else { + isCommentMode = false; + localStorage.removeItem('magico_comment_mode'); + toggleCommentMode(false); + } }); + // --- RESTORE SESSION --- + const savedMode = localStorage.getItem('magico_comment_mode'); + if (savedMode === 'true') { + // Sprawdź cicho czy sesja PHP jest aktywna + checkAuth(true).then(ok => { + if (ok) { + toggle.checked = true; + isCommentMode = true; + toggleCommentMode(true); + } else { + localStorage.removeItem('magico_comment_mode'); // Sesja wygasła + } + }); + } + document.getElementById('cancel-comment').addEventListener('click', closeInputBox); document.getElementById('save-comment').addEventListener('click', saveComment); @@ -57,6 +89,51 @@ window.addEventListener('scroll', updateMarkerPositions, true); } + // --- 1.5 AUTORYZACJA --- + // Prosta weryfikacja - czy mamy flagę w sesji JS? + // Lepiej: przy włączeniu zapytać o hasło jeśli nie mamy, i wysłać do API. + // API ustawi sesję PHP. + + let isAuthorized = false; // Lokalna flaga, aby nie pytać co chwilę + + async function checkAuth(silent = false) { + if (isAuthorized) return true; + + // Najpierw zapytaj API czy już jesteśmy zalogowani (sesja PHP) + try { + const check = await fetch(API_URL + '?action=check_auth'); + const checkData = await check.json(); + if (checkData.status === 'success') { + isAuthorized = true; + return true; + } + } catch (e) { } + + if (silent) return false; + + const pass = prompt("Podaj hasło do trybu komentowania:"); + if (!pass) return false; + + try { + const res = await fetch(API_URL + '?action=auth', { + method: 'POST', + body: JSON.stringify({ password: pass }) + }); + const data = await res.json(); + + if (data.status === 'success') { + isAuthorized = true; + return true; + } else { + alert("Błąd: " + (data.message || "Nieprawidłowe hasło")); + return false; + } + } catch (e) { + alert("Błąd połączenia z API"); + return false; + } + } + // --- 2. LOGIKA TRYBU (AGRESYWNE BLOKOWANIE) --- function toggleCommentMode(active) { if (active) { @@ -220,18 +297,58 @@ document.querySelectorAll('.comment-marker').forEach(el => el.remove()); if (activePopover) { activePopover.remove(); activePopover = null; } - markersData.forEach((c, index) => { + // Grupowanie komentarzy po selektorze + const grouped = {}; + markersData.forEach((c) => { + // Pomiń rozwiązane, jeśli chcemy je ukrywać (na razie pokazujemy wszystkie, albo tylko nierozwiązane?) + // User: "archiwum zmienia flagę is_resolved". Zakładamy że archiwum = ukryte? + // "Archiwum" zwykle oznacza ukryte. Ukryjmy rozwiązane. + if (c.is_resolved == 1) return; + + if (!grouped[c.dom_selector]) grouped[c.dom_selector] = []; + grouped[c.dom_selector].push(c); + }); + + Object.keys(grouped).forEach((selector, index) => { + const commentsList = grouped[selector]; + if (!commentsList.length) return; + try { - const el = document.querySelector(c.dom_selector); + const el = document.querySelector(selector); if (el && el.offsetParent !== null) { // Tylko widoczne elementy const marker = document.createElement('div'); marker.className = 'comment-marker'; - marker.dataset.index = index; // Żeby łatwo znaleźć dane + marker.dataset.selector = selector; - // Zdarzenie kliknięcia w marker - marker.addEventListener('click', (e) => { - e.preventDefault(); e.stopPropagation(); - showPopover(marker, c); + // Jeśli więcej niż 1 komentarz, pokaż licznik + if (commentsList.length > 1) { + marker.textContent = commentsList.length; + marker.style.display = 'flex'; + marker.style.alignItems = 'center'; + marker.style.justifyContent = 'center'; + marker.style.color = 'white'; + marker.style.fontSize = '12px'; + marker.style.fontWeight = 'bold'; + // Resetuejmu transform dla tekstu żeby był czytelny? Nie, rotacja jest na pinezce. + // Tekst też się obróci. Trzeba by go odkręcić. + // Prościej: dodajmy span w środku który odkręcimy. + marker.innerHTML = `${commentsList.length}`; + } + + // Zdarzenie HOVER (najazd myszką) + marker.addEventListener('mouseenter', (e) => { + showPopover(marker, commentsList); + }); + + marker.addEventListener('mouseleave', (e) => { + marker._leaveTimeout = setTimeout(() => { + if (activePopover && activePopover._associatedMarker === marker) { + if (!activePopover.matches(':hover')) { + activePopover.remove(); + activePopover = null; + } + } + }, 300); }); document.body.appendChild(marker); @@ -243,48 +360,96 @@ updateMarkerPositions(); } - function showPopover(marker, data) { + function showPopover(marker, commentsList) { if (activePopover) activePopover.remove(); const pop = document.createElement('div'); pop.className = 'comment-popover'; - // Escape HTML - XSS Protection - const safeAuthor = document.createElement('div'); - safeAuthor.textContent = data.author || 'Anonim'; - const safeComment = document.createElement('div'); - safeComment.textContent = data.comment || ''; + // Budowanie listy komentarzy + let html = '
'; - pop.innerHTML = ` -
${safeAuthor.innerHTML}
-

${safeComment.innerHTML}

- `; + commentsList.forEach(data => { + // Escape (chociaż API to robi, warto mieć warstwę w JS w razie czego, ale API już robi htmlspecialchars) + // Skoro API robi htmlspecialchars, to tutaj możemy bezpiecznie wstawić + // Ale uwaga: JS textContent jest bezpieczniejszy. + // Hack na szybkie budowanie bezpiecznego HTML w pętli stringów jest trudny. + // Zbudujmy to jako elementy DOM potem? Albo zaufajmy API + textContent buildera. + + // Zrobimy placeholder i podstawimy wartości. + + html += ` +
+
+
${data.author || 'Anonim'}
+ ${data.created_at} +
+

${data.comment}

+
+ + +
+
+ `; + }); + html += '
'; + + pop.innerHTML = html; document.body.appendChild(pop); activePopover = pop; pop._associatedMarker = marker; + // Bindowanie akcji + pop.querySelectorAll('.btn-resolve').forEach(btn => { + btn.addEventListener('click', () => resolveComment(btn.dataset.id)); + }); + pop.querySelectorAll('.btn-delete').forEach(btn => { + btn.addEventListener('click', () => deleteComment(btn.dataset.id)); + }); + + // Obsługa zamykania przy wyjechaniu z popovera + pop.addEventListener('mouseleave', () => { + if (activePopover === pop) { + activePopover.remove(); + activePopover = null; + } + }); + // Pozycjonowanie const rect = marker.getBoundingClientRect(); const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - pop.style.top = (rect.top + scrollTop - 10) + 'px'; // Trochę wyżej - pop.style.left = (rect.right + scrollLeft + 10) + 'px'; // Obok markera + pop.style.top = (rect.top + scrollTop - 5) + 'px'; + pop.style.left = (rect.right + scrollLeft + 2) + 'px'; - // Jeśli wychodzi poza ekran z prawej, daj na lewo if (rect.right + 260 > window.innerWidth) { pop.style.left = (rect.left + scrollLeft - 260) + 'px'; } } - // Zamknij popover przy kliknięciu w tło - window.addEventListener('click', (e) => { - if (activePopover && !e.target.closest('.comment-popover') && !e.target.closest('.comment-marker')) { - activePopover.remove(); - activePopover = null; - } - }); + function resolveComment(id) { + if (!confirm('Czy na pewno chcesz rozwiązać/zarchiwizować ten komentarz?')) return; + fetch(API_URL + '?action=resolve', { + method: 'POST', body: JSON.stringify({ id: id }) + }).then(res => res.json()).then(res => { + if (res.status === 'success') loadComments(); + else alert('Błąd: ' + res.message); + }); + } + + function deleteComment(id) { + if (!confirm('Czy na pewno chcesz trwale usunąć ten komentarz?')) return; + fetch(API_URL + '?action=delete', { + method: 'POST', body: JSON.stringify({ id: id }) + }).then(res => res.json()).then(res => { + if (res.status === 'success') loadComments(); + else alert('Błąd: ' + res.message); + }); + } + + // (Usunięto click-outside listener, bo teraz działamy na hover) // Aktualizuj też pozycję otwartego popovera przy scrollu const originalUpdatePositions = updateMarkerPositions; @@ -296,44 +461,47 @@ const markers = document.querySelectorAll('.comment-marker'); markers.forEach(marker => { - const index = marker.dataset.index; - const data = markersData[index]; - if (!data) return; + const selector = marker.dataset.selector; + if (!selector) return; try { - const el = document.querySelector(data.dom_selector); + const el = document.querySelector(selector); if (el && el.offsetParent !== null) { const rect = el.getBoundingClientRect(); // Jeśli element wyjechał poza ekran (jest w scrollowanym divie ale schowany) - // Można go ukryć, ale na razie po prostu przesuwamy marker.style.top = (rect.top + window.scrollY) + 'px'; marker.style.left = (rect.left + window.scrollX + (rect.width / 2)) + 'px'; - marker.style.display = 'block'; + marker.style.display = (marker.textContent.length > 0) ? 'flex' : 'block'; + // Flex dla licznika, block dla zwykłego? A w stylach mamy display? + // W renderMarkers ustawilismy display:flex ręcznie dla licznika. + // Tu musimy uwazac zeby tego nie zepsuc. Pominmy style.display='block' jesli ma flex. + if (marker.style.display !== 'flex') marker.style.display = 'block'; if (activePopover && activePopover._associatedMarker === marker) { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; // Recalculate position - let top = (rect.top + scrollTop - 10); - let left = (rect.right + scrollLeft + 10); + let top = (rect.top + scrollTop - 5); + let left = (rect.right + scrollLeft + 2); if (rect.right + 260 > window.innerWidth) { - left = (rect.left + scrollLeft - 260); // Flip to left + left = (rect.left + scrollLeft - 260); } activePopover.style.top = top + 'px'; activePopover.style.left = left + 'px'; } } else { - marker.style.display = 'none'; // Ukryj jeśli element zniknął + marker.style.display = 'none'; + if (activePopover && activePopover._associatedMarker === marker) { + activePopover.remove(); + activePopover = null; + } } } catch (e) { } }); - - // Jeśli jest aktywny popover, zamknij go przy szybkim scrollu żeby nie pływał dziwnie? - // Albo po prostu zostaw. } // START @@ -343,11 +511,77 @@ // Odświeżanie przy zmianie rozmiaru okna window.addEventListener('resize', updateMarkerPositions); - // MutationObserver - odświeżaj pozycje jak coś się zmieni w DOM (np otwarcie taba) - const observer = new MutationObserver(() => { - // Debounce dla wydajności - if (window.commentUpdateTimeout) clearTimeout(window.commentUpdateTimeout); - window.commentUpdateTimeout = setTimeout(renderMarkers, 200); + // MutationObserver - odświeżaj pozycje jak coś się zmieni w wygenerowanym DOM (ale ignoruj nasze markery) + // Helper sprawdzający czy element jest częścią naszego UI + function isOurElement(node) { + if (!node || node.nodeType !== 1) return false; + + if (node.classList && ( + node.classList.contains('comment-marker') || + node.classList.contains('comment-popover') || + node.id === 'comment-input-box' || + node.id === 'comment-input-box-overlay' + )) return true; + + if (node.closest && ( + node.closest('.comment-marker') || + node.closest('.comment-popover') || + node.closest('#comment-input-box') + )) return true; + + return false; + } + + const observer = new MutationObserver((mutations) => { + let shouldUpdate = false; + + for (const mutation of mutations) { + // 1. Zmiany atrybutów na naszych elementach + if (mutation.type === 'attributes') { + if (isOurElement(mutation.target)) continue; + } + + // 2. Zmiany w strukturze DOM (dodawanie/usuwanie węzłów) + if (mutation.type === 'childList') { + let externalChanges = false; + + // Sprawdź dodane + if (mutation.addedNodes.length > 0) { + for (let i = 0; i < mutation.addedNodes.length; i++) { + const node = mutation.addedNodes[i]; + if (node.nodeType === 1 && !isOurElement(node)) { + externalChanges = true; break; + } + // Ignorujemy same zmiany tekstowe/puste znaki w body, chyba że to istotne? + // Dla bezpieczeństwa: jeśli to tekst i nie jest pusty -> update + if (node.nodeType === 3 && node.textContent.trim() !== '') { + externalChanges = true; break; + } + } + } + + // Sprawdź usunięte (tylko jeśli jeszcze nie wykryto zmian) + if (!externalChanges && mutation.removedNodes.length > 0) { + for (let i = 0; i < mutation.removedNodes.length; i++) { + const node = mutation.removedNodes[i]; + if (node.nodeType === 1 && !isOurElement(node)) { + externalChanges = true; break; + } + } + } + + // Jeśli zmiany nie dotyczyły "zewnętrznych" elementów (czyli tylko nasze), ignoruj. + if (!externalChanges) continue; + } + + shouldUpdate = true; + break; + } + + if (shouldUpdate) { + if (window.commentUpdateTimeout) clearTimeout(window.commentUpdateTimeout); + window.commentUpdateTimeout = setTimeout(renderMarkers, 200); + } }); observer.observe(document.body, { diff --git a/db_connect.php b/db_connect.php index da40b69..d93472f 100644 --- a/db_connect.php +++ b/db_connect.php @@ -3,8 +3,12 @@ $host = 'localhost'; $db = 'srv105686_magicoprototype'; $user = 'srv105686_magicoprototype'; $pass = 'pMMdmYHXSBs6wM5LHNjd'; +$pass = 'pMMdmYHXSBs6wM5LHNjd'; $charset = 'utf8mb4'; +// Hasło do trybu komentowania +$PROTOTYPE_PASSWORD = 'magic'; // Zmień to hasło! + $dsn = "mysql:host=$host;dbname=$db;charset=$charset"; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,