diff --git a/web_interface/templates/v3/base.html b/web_interface/templates/v3/base.html index b427011b..9e42766c 100644 --- a/web_interface/templates/v3/base.html +++ b/web_interface/templates/v3/base.html @@ -4836,53 +4836,100 @@ const newRow = document.createElement('tr'); newRow.className = 'custom-feed-row'; newRow.setAttribute('data-index', newIndex); - newRow.innerHTML = ` - - - - - - - -
- - - No logo -
- - - - - - - - `; + + // Create name cell + const nameCell = document.createElement('td'); + nameCell.className = 'px-4 py-3 whitespace-nowrap'; + const nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.name = `${fullKey}.${newIndex}.name`; + nameInput.value = ''; + nameInput.className = 'block w-full px-2 py-1 border border-gray-300 rounded text-sm'; + nameInput.placeholder = 'Feed Name'; + nameInput.required = true; + nameCell.appendChild(nameInput); + + // Create URL cell + const urlCell = document.createElement('td'); + urlCell.className = 'px-4 py-3 whitespace-nowrap'; + const urlInput = document.createElement('input'); + urlInput.type = 'url'; + urlInput.name = `${fullKey}.${newIndex}.url`; + urlInput.value = ''; + urlInput.className = 'block w-full px-2 py-1 border border-gray-300 rounded text-sm'; + urlInput.placeholder = 'https://example.com/feed'; + urlInput.required = true; + urlCell.appendChild(urlInput); + + // Create logo cell + const logoCell = document.createElement('td'); + logoCell.className = 'px-4 py-3 whitespace-nowrap'; + const logoContainer = document.createElement('div'); + logoContainer.className = 'flex items-center space-x-2'; + + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.id = `${fieldId}_logo_${newIndex}`; + fileInput.accept = 'image/png,image/jpeg,image/bmp,image/gif'; + fileInput.style.display = 'none'; + // Use addEventListener instead of string-based onchange to prevent injection + fileInput.addEventListener('change', function(e) { + handleCustomFeedLogoUpload(e, fieldId, newIndex, pluginId, fullKey); + }); + + const uploadButton = document.createElement('button'); + uploadButton.type = 'button'; + uploadButton.className = 'px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded'; + // Use addEventListener instead of string-based onclick to prevent injection + uploadButton.addEventListener('click', function() { + document.getElementById(`${fieldId}_logo_${newIndex}`).click(); + }); + const uploadIcon = document.createElement('i'); + uploadIcon.className = 'fas fa-upload mr-1'; + uploadButton.appendChild(uploadIcon); + uploadButton.appendChild(document.createTextNode(' Upload')); + + const noLogoSpan = document.createElement('span'); + noLogoSpan.className = 'text-xs text-gray-400'; + noLogoSpan.textContent = 'No logo'; + + logoContainer.appendChild(fileInput); + logoContainer.appendChild(uploadButton); + logoContainer.appendChild(noLogoSpan); + logoCell.appendChild(logoContainer); + + // Create enabled cell + const enabledCell = document.createElement('td'); + enabledCell.className = 'px-4 py-3 whitespace-nowrap text-center'; + const enabledInput = document.createElement('input'); + enabledInput.type = 'checkbox'; + enabledInput.name = `${fullKey}.${newIndex}.enabled`; + enabledInput.checked = true; + enabledInput.value = 'true'; + enabledInput.className = 'h-4 w-4 text-blue-600'; + enabledCell.appendChild(enabledInput); + + // Create remove cell + const removeCell = document.createElement('td'); + removeCell.className = 'px-4 py-3 whitespace-nowrap text-center'; + const removeButton = document.createElement('button'); + removeButton.type = 'button'; + removeButton.className = 'text-red-600 hover:text-red-800 px-2 py-1'; + // Use addEventListener instead of string-based onclick to prevent injection + removeButton.addEventListener('click', function() { + removeCustomFeedRow(this); + }); + const removeIcon = document.createElement('i'); + removeIcon.className = 'fas fa-trash'; + removeButton.appendChild(removeIcon); + removeCell.appendChild(removeButton); + + // Append all cells to row + newRow.appendChild(nameCell); + newRow.appendChild(urlCell); + newRow.appendChild(logoCell); + newRow.appendChild(enabledCell); + newRow.appendChild(removeCell); tbody.appendChild(newRow); } @@ -4960,7 +5007,15 @@ method: 'POST', body: formData }) - .then(response => response.json()) + .then(response => { + // Check HTTP status before parsing JSON + if (!response.ok) { + return response.text().then(text => { + throw new Error(`Upload failed: ${response.status} ${response.statusText}${text ? ': ' + text : ''}`); + }); + } + return response.json(); + }) .then(data => { if (data.status === 'success' && data.data && data.data.files && data.data.files.length > 0) { const uploadedFile = data.data.files[0]; @@ -4972,6 +5027,10 @@ const pathName = existingPathInput ? existingPathInput.name : `${fullKey}.${index}.logo.path`; const idName = existingIdInput ? existingIdInput.name : `${fullKey}.${index}.logo.id`; + // Normalize path: remove leading slashes, then add single leading slash + const normalizedPath = uploadedFile.path.replace(/^\/+/, ''); + const imageSrc = '/' + normalizedPath; + // Clear logoCell and build DOM safely to prevent XSS logoCell.textContent = ''; // Clear existing content @@ -4985,36 +5044,42 @@ fileInput.id = `${fieldId}_logo_${index}`; fileInput.accept = 'image/png,image/jpeg,image/bmp,image/gif'; fileInput.style.display = 'none'; - fileInput.setAttribute('onchange', `handleCustomFeedLogoUpload(event, '${fieldId}', ${index}, '${pluginId}', '${fullKey}')`); + // Use addEventListener instead of string-based onchange to prevent injection + fileInput.addEventListener('change', function(e) { + handleCustomFeedLogoUpload(e, fieldId, index, pluginId, fullKey); + }); // Create upload button const uploadButton = document.createElement('button'); uploadButton.type = 'button'; uploadButton.className = 'px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded'; - uploadButton.setAttribute('onclick', `document.getElementById('${fieldId}_logo_${index}').click()`); + // Use addEventListener instead of string-based onclick to prevent injection + uploadButton.addEventListener('click', function() { + document.getElementById(`${fieldId}_logo_${index}`).click(); + }); const uploadIcon = document.createElement('i'); uploadIcon.className = 'fas fa-upload mr-1'; uploadButton.appendChild(uploadIcon); uploadButton.appendChild(document.createTextNode(' Upload')); - // Create img element - set src via setAttribute to prevent XSS + // Create img element - use normalized path, set src via property to prevent XSS const img = document.createElement('img'); - img.setAttribute('src', `/${uploadedFile.path}`); - img.setAttribute('alt', 'Logo'); + img.src = imageSrc; // Use property assignment with normalized path + img.alt = 'Logo'; img.className = 'w-8 h-8 object-cover rounded border'; img.id = `${fieldId}_logo_preview_${index}`; - // Create hidden input for path - set value via setAttribute to prevent XSS + // Create hidden input for path - set value via property to prevent XSS const pathInput = document.createElement('input'); pathInput.type = 'hidden'; - pathInput.setAttribute('name', pathName); - pathInput.setAttribute('value', uploadedFile.path); + pathInput.name = pathName; + pathInput.value = uploadedFile.path; - // Create hidden input for id - set value via setAttribute to prevent XSS + // Create hidden input for id - set value via property to prevent XSS const idInput = document.createElement('input'); idInput.type = 'hidden'; - idInput.setAttribute('name', idName); - idInput.setAttribute('value', String(uploadedFile.id)); // Ensure it's a string + idInput.name = idName; + idInput.value = String(uploadedFile.id); // Ensure it's a string // Append all elements to container container.appendChild(fileInput);