// CrowdProof Observe - Sidebar Script let serverUrl = null; let pendingImageFile = null; // Stores image file from context menu // Apply browser theme async function applyTheme() { const theme = await browser.theme.getCurrent(); const colors = theme.colors || {}; document.documentElement.style.setProperty('--theme-bg', colors.popup || colors.frame || ''); document.documentElement.style.setProperty('--theme-text', colors.popup_text || colors.tab_text || ''); document.documentElement.style.setProperty('--theme-border', colors.popup_border || colors.toolbar_top_separator || ''); document.documentElement.style.setProperty('--theme-button-bg', colors.button_background_active || colors.toolbar || ''); document.documentElement.style.setProperty('--theme-highlight', colors.tab_line || colors.ntp_text || colors.popup_highlight || ''); document.documentElement.style.setProperty('--theme-highlight-text', colors.popup_highlight_text || colors.toolbar_text || colors.popup_text || ''); } browser.theme.onUpdated.addListener(applyTheme); applyTheme(); // Screen management function showScreen(screenId) { document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden')); document.getElementById(screenId).classList.remove('hidden'); } function showStatus(message, isError = false) { const status = document.getElementById('status'); status.textContent = message; status.className = `status ${isError ? 'error' : 'success'}`; setTimeout(() => { status.className = 'status hidden'; }, 4000); } // API helper async function apiRequest(endpoint, options = {}) { if (!serverUrl) { throw new Error('Server URL not configured'); } const url = `${serverUrl}${endpoint}`; const fetchOptions = { credentials: 'include', ...options }; return fetch(url, fetchOptions); } // Form submission handler async function handleFormSubmit(e, type) { e.preventDefault(); console.log('=== FORM SUBMIT START ==='); console.log('Type:', type); console.log('Server URL:', serverUrl); const form = e.target; const formData = new FormData(form); // For photo submissions, use pendingImageFile if available if (type === 'photo' && pendingImageFile) { formData.set('file', pendingImageFile); pendingImageFile = null; hideImagePreview(); } console.log('Form data entries:'); for (let [key, value] of formData.entries()) { console.log(' ', key, '=', value); } const submitBtn = form.querySelector('button[type="submit"]'); const originalText = submitBtn.textContent; submitBtn.textContent = 'Creating...'; submitBtn.disabled = true; const url = `${serverUrl}/create_observation/${type}`; console.log('Fetching URL:', url); try { console.log('About to fetch...'); const response = await fetch(url, { method: 'POST', body: formData, credentials: 'include', headers: { 'Accept': 'application/json' } }); console.log('Fetch completed'); console.log('Response status:', response.status); const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { const result = await response.json(); if (result.success) { showStatus('Observation created successfully!'); form.reset(); if (type === 'photo') hideImagePreview(); } else if (result.error === 'login_required') { showStatus('Session expired. Please login again.', true); showScreen('login'); } else { showStatus(result.error || 'Failed to create observation', true); } } else { // Fallback to old redirect-based detection if (response.url.includes('/login')) { showStatus('Session expired. Please login again.', true); showScreen('login'); } else if (response.url.includes('/observations')) { showStatus('Observation created successfully!'); form.reset(); if (type === 'photo') hideImagePreview(); } else { showStatus('Failed to create observation', true); } } } catch (error) { console.error('Fetch error:', error); showStatus('Error: ' + error.message, true); } finally { submitBtn.textContent = originalText; submitBtn.disabled = false; console.log('=== FORM SUBMIT END ==='); } } // Check for pending URL from context menu async function checkPendingUrl() { const result = await browser.storage.local.get('pendingUrl'); if (result.pendingUrl) { // Clear it await browser.storage.local.remove('pendingUrl'); // Switch to URL tab document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelector('.tab[data-type="url"]').classList.add('active'); document.querySelectorAll('.obs-form').forEach(f => f.classList.add('hidden')); document.getElementById('form-url').classList.remove('hidden'); // Populate URL field document.getElementById('url-reference_url').value = result.pendingUrl; } } // Check for pending image from context menu async function checkPendingImage() { const result = await browser.storage.local.get('pendingImage'); if (result.pendingImage) { const { dataUrl, sourceUrl } = result.pendingImage; // Clear it await browser.storage.local.remove('pendingImage'); // Switch to Photo tab document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelector('.tab[data-type="photo"]').classList.add('active'); document.querySelectorAll('.obs-form').forEach(f => f.classList.add('hidden')); document.getElementById('form-photo').classList.remove('hidden'); // Populate the reference URL field if (sourceUrl) { document.getElementById('photo-photo_url').value = sourceUrl; } // Convert dataUrl to File and store it if (dataUrl) { const response = await fetch(dataUrl); const blob = await response.blob(); pendingImageFile = new File([blob], `image-${Date.now()}.png`, { type: 'image/png' }); // Show preview document.getElementById('image-preview-img').src = dataUrl; document.getElementById('image-preview').classList.remove('hidden'); showStatus('Image captured and ready to submit!'); } else { showStatus('Could not capture image data', true); } } } // Show image preview function showImagePreview(src) { document.getElementById('image-preview-img').src = src; document.getElementById('image-preview').classList.remove('hidden'); } // Hide image preview function hideImagePreview() { document.getElementById('image-preview').classList.add('hidden'); document.getElementById('image-preview-img').src = ''; } // Update logos and instance name from server async function updateLogos() { if (!serverUrl) return; const loginLogo = document.getElementById('login-logo'); const mainLogo = document.getElementById('main-logo'); const loginTitle = document.getElementById('login-title'); const mainTitle = document.getElementById('main-title'); const loginInstanceName = document.getElementById('login-instance-name'); const mainInstanceName = document.getElementById('main-instance-name'); // Fetch instance name from login page try { const pageResponse = await fetch(`${serverUrl}/login`, { credentials: 'include' }); if (pageResponse.ok) { const html = await pageResponse.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const brand = doc.querySelector('.navbar-brand'); if (brand) { const instanceName = brand.textContent.trim(); loginInstanceName.textContent = instanceName; mainInstanceName.textContent = instanceName; } else { throw new Error('Brand not found'); } } else { throw new Error('Page fetch failed'); } } catch (e) { loginInstanceName.textContent = 'CrowdProof (Error)'; mainInstanceName.textContent = 'CrowdProof (Error)'; } // Fetch logo try { const response = await fetch(`${serverUrl}/static/logo.svg`, { credentials: 'include' }); if (response.ok) { const svgText = await response.text(); const dataUrl = 'data:image/svg+xml;base64,' + btoa(svgText); loginLogo.src = dataUrl; mainLogo.src = dataUrl; loginLogo.classList.remove('hidden'); mainLogo.classList.remove('hidden'); loginTitle.classList.add('hidden'); mainTitle.classList.add('hidden'); } else { throw new Error('Logo not found'); } } catch (e) { // Fallback to text loginLogo.classList.add('hidden'); mainLogo.classList.add('hidden'); loginTitle.classList.remove('hidden'); mainTitle.classList.remove('hidden'); } } // Load observations list for access screen async function loadObservations() { const listEl = document.getElementById('observations-list'); listEl.innerHTML = '

Loading...

'; try { const response = await fetch(`${serverUrl}/observations`, { credentials: 'include', headers: { 'Accept': 'application/json' } }); if (response.url.includes('/login')) { showScreen('login'); return; } const data = await response.json(); if (data.success && data.observations) { renderObservationsList(data.observations); } else { listEl.innerHTML = '

Failed to load observations

'; } } catch (error) { console.error('Load observations error:', error); listEl.innerHTML = '

Error loading observations

'; } } // Render observations list with access toggles function renderObservationsList(observations) { const listEl = document.getElementById('observations-list'); if (!observations || observations.length === 0) { listEl.innerHTML = '

No observations found

'; return; } listEl.innerHTML = observations.map(obs => { const isOwner = obs.is_owner; const disabledClass = isOwner ? '' : 'disabled'; const disabledAttr = isOwner ? '' : 'disabled'; const details = obs.details || {}; // Determine which button is active based on public_access value const noneActive = obs.public_access === 0 ? 'active-none' : ''; const editorActive = obs.public_access === 1 ? 'active-editor' : ''; const viewerActive = obs.public_access === 2 ? 'active-viewer' : ''; // Build thumbnail for photos let thumbHtml = ''; if (obs.observation_type === 'photo' && details.image_url) { thumbHtml = ``; } // Build type-specific details let detailsHtml = ''; if (obs.observation_type === 'photo') { if (details.description) detailsHtml += `
${escapeHtml(details.description)}
`; if (details.date) detailsHtml += `
Date: ${escapeHtml(details.date)}
`; } else if (obs.observation_type === 'email') { if (details.subject) detailsHtml += `
Subject: ${escapeHtml(details.subject)}
`; if (details.sender) detailsHtml += `
From: ${escapeHtml(details.sender)}
`; if (details.date) detailsHtml += `
Date: ${escapeHtml(details.date)}
`; } else if (obs.observation_type === 'url') { if (details.url) detailsHtml += `
${escapeHtml(details.url)}
`; if (details.date) detailsHtml += `
Date: ${escapeHtml(details.date)}
`; } else if (obs.observation_type === 'log') { if (details.source) detailsHtml += `
Source: ${escapeHtml(details.source)}
`; if (details.date) detailsHtml += `
Date: ${escapeHtml(details.date)}
`; } else if (obs.observation_type === 'statement') { if (details.issuer) detailsHtml += `
Issuer: ${escapeHtml(details.issuer)}
`; if (details.recipient) detailsHtml += `
To: ${escapeHtml(details.recipient)}
`; if (details.date) detailsHtml += `
Date: ${escapeHtml(details.date)}
`; } return `
${thumbHtml}
${escapeHtml(obs.display_name)}
${obs.observation_type}
${detailsHtml}
`; }).join(''); // Add click handlers for toggle buttons listEl.querySelectorAll('.access-toggle button:not([disabled])').forEach(btn => { btn.addEventListener('click', async (e) => { const item = e.target.closest('.observation-item'); const uuid = item.dataset.uuid; const level = parseInt(e.target.dataset.level); await setPublicAccess(uuid, level, item); }); }); } // Escape HTML to prevent XSS function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Set public access for an observation async function setPublicAccess(uuid, level, itemEl) { const buttons = itemEl.querySelectorAll('.access-toggle button'); buttons.forEach(btn => btn.disabled = true); try { const response = await fetch(`${serverUrl}/set_public_access/observation/${uuid}/${level}`, { method: 'POST', credentials: 'include' }); if (response.ok) { // Update button states buttons.forEach(btn => { btn.classList.remove('active-none', 'active-editor', 'active-viewer'); const btnLevel = parseInt(btn.dataset.level); if (btnLevel === level) { if (level === 0) btn.classList.add('active-none'); else if (level === 1) btn.classList.add('active-editor'); else if (level === 2) btn.classList.add('active-viewer'); } }); } else { showStatus('Failed to update access', true); } } catch (error) { console.error('Set public access error:', error); showStatus('Error updating access', true); } finally { buttons.forEach(btn => btn.disabled = false); } } // Listen for storage changes to catch pending URL/image even when sidebar is already open browser.storage.onChanged.addListener((changes, area) => { if (area === 'local') { if (changes.pendingUrl && changes.pendingUrl.newValue) { checkPendingUrl(); } if (changes.pendingImage && changes.pendingImage.newValue) { checkPendingImage(); } } }); // Initialize async function init() { console.log('Init starting...'); const result = await browser.storage.local.get('serverUrl'); serverUrl = result.serverUrl || 'https://crowdproof.silogroup.org'; console.log('Server URL:', serverUrl); // Set create account link document.getElementById('createAccountLink').href = `${serverUrl}/register`; updateLogos(); try { const response = await apiRequest('/profile'); if (response.url.includes('/login')) { showScreen('login'); } else { showScreen('main'); // Check for pending URL/image after showing main screen await checkPendingUrl(); await checkPendingImage(); } } catch (error) { console.error('Auth check failed:', error); showScreen('login'); } } // Setup all event handlers when DOM is ready document.addEventListener('DOMContentLoaded', () => { console.log('=== DOM READY ==='); // Open settings document.getElementById('openSettings').addEventListener('click', () => { showScreen('settings'); }); // Login form document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); console.log('Login form submitted'); const username = document.getElementById('username').value; const password = document.getElementById('password').value; const errorEl = document.getElementById('loginError'); try { const formData = new FormData(); formData.append('username', username); formData.append('password', password); const response = await apiRequest('/login', { method: 'POST', body: formData }); if (!response.url.includes('/login')) { showScreen('main'); } else { errorEl.textContent = 'Invalid username or password'; errorEl.classList.remove('hidden'); } } catch (error) { errorEl.textContent = error.message; errorEl.classList.remove('hidden'); } }); // Login settings button document.getElementById('loginSettingsBtn').addEventListener('click', async () => { const result = await browser.storage.local.get('serverUrl'); document.getElementById('settings-serverUrl').value = result.serverUrl || 'https://crowdproof.silogroup.org'; showScreen('settings'); }); // Logout document.getElementById('logoutBtn').addEventListener('click', async () => { try { await apiRequest('/logout'); showScreen('login'); } catch (error) { showStatus('Logout failed: ' + error.message, true); } }); // Settings button on main screen document.getElementById('mainSettingsBtn').addEventListener('click', async () => { const result = await browser.storage.local.get('serverUrl'); document.getElementById('settings-serverUrl').value = result.serverUrl || ''; showScreen('settings'); }); // Settings back button document.getElementById('settingsBackBtn').addEventListener('click', async () => { // Go back to appropriate screen if (serverUrl) { try { const response = await apiRequest('/profile'); if (response.url.includes('/login')) { showScreen('login'); } else { showScreen('main'); } } catch { showScreen('login'); } } else { showScreen('setup'); } }); // Access button document.getElementById('accessBtn').addEventListener('click', () => { showScreen('access'); loadObservations(); }); // Access back button document.getElementById('accessBackBtn').addEventListener('click', () => { showScreen('main'); }); // Settings form document.getElementById('settingsForm').addEventListener('submit', async (e) => { e.preventDefault(); const newUrl = document.getElementById('settings-serverUrl').value.replace(/\/+$/, ''); await browser.storage.local.set({ serverUrl: newUrl }); serverUrl = newUrl; document.getElementById('createAccountLink').href = `${serverUrl}/register`; updateLogos(); const statusEl = document.getElementById('settingsStatus'); statusEl.textContent = 'Settings saved'; statusEl.className = 'status success'; statusEl.classList.remove('hidden'); setTimeout(() => { statusEl.classList.add('hidden'); }, 2000); }); // Tab switching document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { const type = tab.dataset.type; console.log('Tab clicked:', type); document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); document.querySelectorAll('.obs-form').forEach(f => f.classList.add('hidden')); document.getElementById(`form-${type}`).classList.remove('hidden'); }); }); // Capture URL button document.getElementById('captureUrl').addEventListener('click', async () => { console.log('Capture URL clicked'); try { const tabs = await browser.tabs.query({ active: true, currentWindow: true }); if (tabs[0]) { document.getElementById('url-reference_url').value = tabs[0].url; if (!document.getElementById('url-observation_name').value) { document.getElementById('url-observation_name').value = tabs[0].title; } showStatus('URL captured!'); } } catch (error) { console.error('Capture URL error:', error); showStatus('Could not capture URL', true); } }); // Log file import document.getElementById('log-file-import').addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); document.getElementById('log-log_content').value = text; showStatus('File imported'); } catch (error) { showStatus('Could not read file', true); } }); // Photo file input - show preview document.getElementById('photo-file').addEventListener('change', (e) => { const file = e.target.files[0]; if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (event) => { showImagePreview(event.target.result); }; reader.readAsDataURL(file); // Clear any pending context menu image since user selected a file pendingImageFile = null; } else { hideImagePreview(); } }); // Form submissions const forms = ['url', 'statement', 'log', 'photo', 'email']; forms.forEach(type => { const form = document.getElementById(`form-${type}`); console.log(`Attaching handler for form-${type}:`, form); form.addEventListener('submit', (e) => { console.log(`Form ${type} submit event fired`); handleFormSubmit(e, type); }); }); // Paste target for photo form - click to enable paste const pasteTarget = document.getElementById('paste-target'); console.log('Paste target element:', pasteTarget); // Click to focus (enables paste) pasteTarget.addEventListener('click', () => { console.log('Paste target clicked, focusing...'); pasteTarget.focus(); }); // Update text on focus/blur pasteTarget.addEventListener('focus', () => { console.log('Paste target focused'); pasteTarget.querySelector('span').textContent = 'Ready - paste now (Ctrl+V)'; }); pasteTarget.addEventListener('blur', () => { console.log('Paste target blurred'); pasteTarget.querySelector('span').textContent = 'Click to enable paste'; }); // Handle paste when paste target is focused (document-level since divs don't receive paste) document.addEventListener('paste', async (e) => { // Only handle if paste target is focused if (document.activeElement !== pasteTarget) return; console.log('Paste event received, paste target is focused'); const photoForm = document.getElementById('form-photo'); if (photoForm.classList.contains('hidden')) return; console.log('Clipboard data:', e.clipboardData); e.preventDefault(); const items = e.clipboardData?.items; if (!items) return; console.log('Items:', items.length); for (const item of items) { console.log('Item type:', item.type); if (item.type.startsWith('image/')) { const blob = item.getAsFile(); console.log('Blob:', blob); if (!blob) continue; // Create a File from the blob const file = new File([blob], `screenshot-${Date.now()}.png`, { type: blob.type }); console.log('File created:', file.name, file.size); // Build FormData from the photo form const form = document.getElementById('form-photo'); const formData = new FormData(); formData.append('file', file); formData.append('observation_name', form.querySelector('[name="observation_name"]').value); formData.append('photo_description', form.querySelector('[name="photo_description"]').value); formData.append('photo_url', form.querySelector('[name="photo_url"]').value); formData.append('photo_date', form.querySelector('[name="photo_date"]').value); // Submit pasteTarget.classList.add('ready'); pasteTarget.querySelector('span').textContent = 'Submitting...'; try { const response = await fetch(`${serverUrl}/create_observation/photo`, { method: 'POST', body: formData, credentials: 'include', headers: { 'Accept': 'application/json' } }); const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { const result = await response.json(); if (result.success) { showStatus('Photo observation created!'); form.reset(); hideImagePreview(); pendingImageFile = null; } else if (result.error === 'login_required') { showStatus('Session expired. Please login again.', true); showScreen('login'); } else { showStatus(result.error || 'Failed to create observation', true); } } else { // Fallback if (response.url.includes('/login')) { showStatus('Session expired. Please login again.', true); showScreen('login'); } else if (response.url.includes('/observations')) { showStatus('Photo observation created!'); form.reset(); hideImagePreview(); pendingImageFile = null; } else { showStatus('Failed to create observation', true); } } } catch (error) { showStatus('Error: ' + error.message, true); } finally { pasteTarget.classList.remove('ready'); pasteTarget.querySelector('span').textContent = 'Click to enable paste'; } return; } } showStatus('No image found in clipboard', true); }); // Email drop target const emailDropTarget = document.getElementById('email-drop-target'); emailDropTarget.addEventListener('dragover', (e) => { e.preventDefault(); emailDropTarget.classList.add('dragover'); }); emailDropTarget.addEventListener('dragleave', () => { emailDropTarget.classList.remove('dragover'); }); emailDropTarget.addEventListener('drop', async (e) => { e.preventDefault(); emailDropTarget.classList.remove('dragover'); const files = e.dataTransfer?.files; if (!files || files.length === 0) return; const file = files[0]; const validExtensions = ['.msg', '.eml']; const hasValidExt = validExtensions.some(ext => file.name.toLowerCase().endsWith(ext)); if (!hasValidExt) { showStatus('Please drop a .msg or .eml file', true); return; } // Build FormData from the email form const form = document.getElementById('form-email'); const formData = new FormData(); formData.append('msg_file', file); formData.append('observation_name', form.querySelector('[name="observation_name"]').value); formData.append('email_description', form.querySelector('[name="email_description"]').value); // Submit emailDropTarget.classList.add('ready'); emailDropTarget.querySelector('span').textContent = 'Submitting...'; try { const response = await fetch(`${serverUrl}/create_observation/email`, { method: 'POST', body: formData, credentials: 'include', headers: { 'Accept': 'application/json' } }); const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { const result = await response.json(); if (result.success) { showStatus('Email observation created!'); form.reset(); } else if (result.error === 'login_required') { showStatus('Session expired. Please login again.', true); showScreen('login'); } else { showStatus(result.error || 'Failed to create observation', true); } } else { // Fallback if (response.url.includes('/login')) { showStatus('Session expired. Please login again.', true); showScreen('login'); } else if (response.url.includes('/observations')) { showStatus('Email observation created!'); form.reset(); } else { showStatus('Failed to create observation', true); } } } catch (error) { showStatus('Error: ' + error.message, true); } finally { emailDropTarget.classList.remove('ready'); emailDropTarget.querySelector('span').textContent = 'Drop .msg or .eml file to submit'; } }); console.log('=== ALL HANDLERS ATTACHED ==='); // Initialize init(); });