diff --git a/index.html b/index.html index 3e2ad03..0305321 100644 --- a/index.html +++ b/index.html @@ -637,6 +637,29 @@ 0 + + + + + + + + + + + + + + + + + + + diff --git a/main.js b/main.js index 51f8d44..9a3ad9f 100644 --- a/main.js +++ b/main.js @@ -31,9 +31,10 @@ function createWindow() { width: 1200, height: 800, webPreferences: { - nodeIntegration: true, - contextIsolation: false, - enableRemoteModule: true, + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true, + sandbox: false, }, }); @@ -844,24 +845,28 @@ ipcMain.handle('move-to-extras', async (event, filePath) => { }); // IPC handler for opening file in default player +// FIXED: Use execFile instead of exec to prevent command injection ipcMain.handle('open-file-in-player', async (event, filePath) => { try { - const { exec } = require('child_process'); + const { execFile } = require('child_process'); - // Open file in default player based on OS - let command; + // Open file in default player based on OS using execFile (safe against injection) + let command, args; if (process.platform === 'darwin') { // macOS - command = `open "${filePath}"`; + command = 'open'; + args = [filePath]; } else if (process.platform === 'win32') { // Windows - command = `start "" "${filePath}"`; + command = 'cmd.exe'; + args = ['/c', 'start', '""', filePath]; } else { // Linux - command = `xdg-open "${filePath}"`; + command = 'xdg-open'; + args = [filePath]; } - exec(command, (error, stdout, stderr) => { + execFile(command, args, (error, stdout, stderr) => { if (error) { console.error('Error opening file:', error); } else { @@ -876,6 +881,32 @@ ipcMain.handle('open-file-in-player', async (event, filePath) => { } }); +// IPC handler for validating folder path (renderer can't access fs directly) +ipcMain.handle('validate-folder', async (event, folderPath) => { + try { + const stats = fs.statSync(folderPath); + return { success: true, exists: true, isDirectory: stats.isDirectory() }; + } catch (error) { + return { success: false, exists: false, isDirectory: false, error: error.message }; + } +}); + +// IPC handler for getting file stats (renderer can't access fs directly) +ipcMain.handle('get-file-stats', async (event, filePath) => { + try { + const stats = fs.statSync(filePath); + return { + success: true, + size: stats.size, + mtime: stats.mtime, + isFile: stats.isFile(), + isDirectory: stats.isDirectory() + }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + // IPC handler for logging audit events ipcMain.handle('log-audit-event', async (event, { directoryPath, action, details }) => { try { @@ -906,4 +937,4 @@ app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } -}); \ No newline at end of file +}); diff --git a/preload.js b/preload.js new file mode 100644 index 0000000..d9b5953 --- /dev/null +++ b/preload.js @@ -0,0 +1,53 @@ +const { contextBridge, ipcRenderer } = require('electron'); +const path = require('path'); + +const fakeIpcRenderer = { + invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), + on: (channel, callback) => { + const listener = (_event, ...args) => callback(...args); + ipcRenderer.on(channel, listener); + return () => ipcRenderer.removeListener(channel, listener); + }, +}; + +const fakeElectronModule = { + ipcRenderer: fakeIpcRenderer, + app: null, + BrowserWindow: null, + dialog: null, + ipcMain: null, +}; + +const fakePathModule = { + join: (...args) => path.join(...args), + dirname: (p) => path.dirname(p), + basename: (p) => path.basename(p), + extname: (p) => path.extname(p), + sep: path.sep, + resolve: (...args) => path.resolve(...args), +}; + +contextBridge.exposeInMainWorld('electronAPI', { + ipcRenderer: fakeIpcRenderer, +}); + +contextBridge.exposeInMainWorld('createModuleRequire', () => { + return function requirePolyfill(moduleName) { + if (moduleName === 'electron') { + return fakeElectronModule; + } + if (moduleName === 'path') { + return fakePathModule; + } + if (moduleName === 'fs') { + throw new Error('Direct fs access disabled. Use IPC.'); + } + if (moduleName.startsWith('./')) { + const baseName = moduleName.replace('./', '').replace('.js', ''); + const mod = window[baseName]; + if (mod) return mod; + throw new Error(`Module not found: ${moduleName}`); + } + throw new Error(`Module not allowed: ${moduleName}`); + }; +}); diff --git a/renderer.js b/renderer.js index 6070bc5..8a5c5cc 100644 --- a/renderer.js +++ b/renderer.js @@ -1,4 +1,13 @@ -const UIManager = require('./utils/renderer/UIManager'); +// UIManager is loaded via window.UIManager from index.html +const UIManager = window.UIManager; + +function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} + +window.escapeHtml = escapeHtml; // Initialize the application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { @@ -47,4 +56,4 @@ document.addEventListener('DOMContentLoaded', () => { console.log('UIManager initialized and exposed to window'); }); -console.log('Movie Mapper renderer loaded'); \ No newline at end of file +console.log('Movie Mapper renderer loaded'); diff --git a/utils/renderer/FileListManager.js b/utils/renderer/FileListManager.js index fdeb60d..5d5ffcb 100644 --- a/utils/renderer/FileListManager.js +++ b/utils/renderer/FileListManager.js @@ -60,24 +60,37 @@ class FileListManager { _createFolderElement(file) { const fileItem = document.createElement('div'); fileItem.className = 'file-item folder-item'; - fileItem.innerHTML = ` -
📁
-
${file.name}
-
-
-
-
- `; - // Add click handler to navigate into folder + const icon = document.createElement('div'); + icon.style.cssText = 'font-size: 18px; margin-right: 10px;'; + icon.textContent = '\uD83D\uDCC1'; + + const nameEl = document.createElement('div'); + nameEl.className = 'file-name folder-name'; + nameEl.textContent = file.name; + + const durationEl = document.createElement('div'); + durationEl.className = 'file-duration'; + + const qualityEl = document.createElement('div'); + qualityEl.className = 'file-quality'; + + const fpsEl = document.createElement('div'); + fpsEl.className = 'file-fps'; + + const tagsEl = document.createElement('div'); + tagsEl.className = 'file-tags'; + + fileItem.append(icon, nameEl, durationEl, qualityEl, fpsEl, tagsEl); + + const filePath = file.path; + const fileName = file.name; fileItem.addEventListener('click', (e) => { - // Don't trigger if clicking on any child elements if (e.target !== fileItem && e.target.className !== 'file-name folder-name') { return; } - if (this.onFolderClick) { - this.onFolderClick(file.path, file.name); + this.onFolderClick(filePath, fileName); } }); @@ -97,35 +110,91 @@ class FileListManager { fileItem.dataset.index = index; fileItem.dataset.filePath = file.path; - // Use actual duration from file metadata const duration = file.duration || '00:00'; const quality = file.quality || 'unknown'; const fps = file.fps || 'unknown'; - // Add label for problematic files - const problemLabel = file.isProblematic ? '⚠️ ' : ''; + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle'; + dragHandle.textContent = '\u22EE\u22EE'; - fileItem.innerHTML = ` -
⋮⋮
-
- -
${episodeNumber}
- -
-
${file.name}
-
${duration}
-
${quality}
-
${fps}
-
- 🏷️ - 🎥 - 🗑️ - 🎬 - -
- `; + const episodeContainer = document.createElement('div'); + episodeContainer.className = 'episode-number-container'; + + const leftArrow = document.createElement('button'); + leftArrow.className = 'episode-arrow episode-arrow-left'; + leftArrow.title = 'Decrease episode range'; + leftArrow.textContent = '\u25C0'; + + const episodeNum = document.createElement('div'); + episodeNum.className = 'episode-number'; + episodeNum.dataset.episodeStart = episodeNumber; + episodeNum.dataset.episodeEnd = episodeNumber; + episodeNum.textContent = episodeNumber; + + const rightArrow = document.createElement('button'); + rightArrow.className = 'episode-arrow episode-arrow-right'; + rightArrow.title = 'Increase episode range'; + rightArrow.textContent = '\u25B6'; + + episodeContainer.append(leftArrow, episodeNum, rightArrow); + + const nameEl = document.createElement('div'); + nameEl.className = 'file-name'; + nameEl.textContent = file.name; + + if (file.isProblematic) { + const warn = document.createElement('span'); + warn.className = 'problematic-label'; + warn.textContent = '\u26A0\ufe0f'; + nameEl.prepend(warn, ' '); + } + + const durEl = document.createElement('div'); + durEl.className = 'file-duration'; + durEl.textContent = duration; + + const qualEl = document.createElement('div'); + qualEl.className = 'file-quality'; + qualEl.textContent = quality; + + const fpsEl = document.createElement('div'); + fpsEl.className = 'file-fps'; + fpsEl.textContent = fps; + + const tagsEl = document.createElement('div'); + tagsEl.className = 'file-tags'; + + const extraTag = document.createElement('span'); + extraTag.className = 'tag-icon extra-tag'; + extraTag.title = 'Mark as Extra'; + extraTag.textContent = '\uD83C\uDFF7\uFE0F'; + + const btsTag = document.createElement('span'); + btsTag.className = 'tag-icon behind-the-scenes-tag'; + btsTag.title = 'Add Behind the Scenes'; + btsTag.textContent = '\uD83C\uDFA5'; + + const delTag = document.createElement('span'); + delTag.className = 'tag-icon delete-tag'; + delTag.title = 'Mark for Deletion'; + delTag.textContent = '\uD83D\uDDD1\uFE0F'; + + const previewBtn = document.createElement('span'); + previewBtn.className = 'video-preview-btn'; + previewBtn.title = 'Preview Video'; + previewBtn.textContent = '\uD83C\uDFAC'; + + const playBtn = document.createElement('button'); + playBtn.className = 'play-button'; + playBtn.style.cssText = 'opacity: 0.3; cursor: default; flex-shrink: 0;'; + playBtn.disabled = true; + playBtn.textContent = '\u25B6\uFE0F'; + + tagsEl.append(extraTag, btsTag, delTag, previewBtn, playBtn); + + fileItem.append(dragHandle, episodeContainer, nameEl, durEl, qualEl, fpsEl, tagsEl); - // Add drag and drop event listeners fileItem.addEventListener('dragstart', this._handleDragStart.bind(this)); fileItem.addEventListener('dragend', this._handleDragEnd.bind(this)); fileItem.addEventListener('dragover', this._handleDragOver.bind(this)); diff --git a/utils/renderer/ModalManager.js b/utils/renderer/ModalManager.js index c6f6ec9..351476f 100644 --- a/utils/renderer/ModalManager.js +++ b/utils/renderer/ModalManager.js @@ -1,5 +1,4 @@ -const { ipcRenderer } = require('electron'); -const fs = require('fs'); +const { ipcRenderer } = window.electronAPI; /** * ModalManager - Manages video preview modal @@ -142,50 +141,42 @@ class ModalManager { const videoContainer = document.getElementById('video-preview-container'); const loadingIndicator = document.getElementById('video-preview-loading'); - // Show loading loadingIndicator.style.display = 'block'; - videoContainer.innerHTML = ''; - fileInfo.innerHTML = `Loading: ${filePath.split('/').pop()}`; + videoContainer.textContent = ''; + const fileName = filePath.split('/').pop(); + fileInfo.textContent = `Loading: ${fileName}`; try { - // Check if file exists - if (!fs.existsSync(filePath)) { + const statsResult = await ipcRenderer.invoke('get-file-stats', filePath); + if (!statsResult.success) { throw new Error('File not found'); } - // Get file stats for info - const stats = fs.statSync(filePath); - const fileName = filePath.split('/').pop(); + fileInfo.textContent = `File: ${fileName} | Size: ${(statsResult.size / (1024 * 1024)).toFixed(2)} MB | Path: ${filePath}`; - // Update file info - fileInfo.innerHTML = ` - File: ${fileName}
- Size: ${(stats.size / (1024 * 1024)).toFixed(2)} MB
- Path: ${filePath} - `; - - // For MKV files, open directly in default player const ext = filePath.toLowerCase().split('.').pop(); if (ext === 'mkv') { - // Close the modal and open in default player directly this.videoPreviewModal.style.display = 'none'; ipcRenderer.invoke('open-file-in-player', filePath); } else { - // For other formats, try to create a video player - videoContainer.innerHTML = ` - - `; - - // Hide loading + const video = document.createElement('video'); + video.id = 'preview-video'; + video.controls = true; + video.style.cssText = 'width: 100%; max-width: 800px; height: auto; margin: 0 auto; display: block;'; + const source = document.createElement('source'); + source.src = `file://${filePath}`; + source.type = 'video/mp4'; + video.appendChild(source); + videoContainer.appendChild(video); loadingIndicator.style.display = 'none'; } } catch (error) { console.error('Error loading video preview:', error); - fileInfo.innerHTML = `Error: Could not load preview for ${filePath}`; - videoContainer.innerHTML = '

Error loading video preview

'; + fileInfo.textContent = 'Error: Could not load preview'; + const p = document.createElement('p'); + p.style.cssText = 'color: #dc3545; text-align: center;'; + p.textContent = 'Error loading video preview'; + videoContainer.appendChild(p); loadingIndicator.style.display = 'none'; } } diff --git a/utils/renderer/UIManager.js b/utils/renderer/UIManager.js index badb939..90ccdf9 100644 --- a/utils/renderer/UIManager.js +++ b/utils/renderer/UIManager.js @@ -1,5 +1,5 @@ -const { ipcRenderer } = require('electron'); const path = require('path'); +const { ipcRenderer } = window.electronAPI; const AppState = require('./AppState'); const FileListManager = require('./FileListManager'); const TagManager = require('./TagManager'); @@ -417,7 +417,10 @@ class UIManager { this.searchResultsEl.innerHTML = ''; if (results.length === 0) { - this.searchResultsEl.innerHTML = '

No shows found.

'; + const p = document.createElement('p'); + p.style.cssText = 'color: #888; text-align: center; padding: 10px;'; + p.textContent = 'No shows found.'; + this.searchResultsEl.appendChild(p); return; } @@ -425,14 +428,20 @@ class UIManager { const resultItem = document.createElement('div'); resultItem.className = 'search-result-item'; - // Create a more detailed display with separate elements const showName = show.seriesName || show.name; const showYear = show.firstAired ? show.firstAired.split('-')[0] : ''; - resultItem.innerHTML = ` -
${showName}
- ${showYear ? `
${showYear}
` : ''} - `; + const nameEl = document.createElement('div'); + nameEl.className = 'show-name'; + nameEl.textContent = showName; + resultItem.appendChild(nameEl); + + if (showYear) { + const yearEl = document.createElement('div'); + yearEl.className = 'show-year'; + yearEl.textContent = showYear; + resultItem.appendChild(yearEl); + } resultItem.addEventListener('click', () => this.selectShow(show)); this.searchResultsEl.appendChild(resultItem); @@ -1609,60 +1618,74 @@ class UIManager { const videoContainer = document.getElementById('video-preview-container'); const loadingIndicator = document.getElementById('video-preview-loading'); - // Show loading loadingIndicator.style.display = 'block'; if (videoContainer) videoContainer.innerHTML = ''; - if (fileInfo) fileInfo.innerHTML = `Loading: ${filePath.split('/').pop()}`; + if (fileInfo) { + const fileName = filePath.split('/').pop(); + const strong = document.createElement('strong'); + strong.textContent = 'Loading: '; + fileInfo.textContent = ''; + fileInfo.appendChild(strong); + fileInfo.appendChild(document.createTextNode(fileName)); + } try { - // Check if file exists - const fs = require('fs'); - if (!fs.existsSync(filePath)) { + const statsResult = await ipcRenderer.invoke('get-file-stats', filePath); + if (!statsResult.success) { throw new Error('File not found'); } - // Get file stats for info - const stats = fs.statSync(filePath); const fileName = filePath.split('/').pop(); - // Update file info if (fileInfo) { - fileInfo.innerHTML = ` - File: ${fileName}
- Size: ${(stats.size / (1024 * 1024)).toFixed(2)} MB
- Path: ${filePath} - `; + fileInfo.textContent = ''; + const parts = [ + { bold: 'File: ', text: fileName }, + { bold: '\nSize: ', text: `${(statsResult.size / (1024 * 1024)).toFixed(2)} MB` }, + { bold: '\nPath: ', text: filePath } + ]; + parts.forEach(p => { + const strong = document.createElement('strong'); + strong.textContent = p.bold; + fileInfo.appendChild(strong); + fileInfo.appendChild(document.createTextNode(p.text)); + }); } - // For MKV files, open directly in default player const ext = filePath.toLowerCase().split('.').pop(); if (ext === 'mkv') { - // Close the modal and open in default player directly if (this.videoPreviewModal) { this.videoPreviewModal.style.display = 'none'; } ipcRenderer.invoke('open-file-in-player', filePath); } else { - // For other formats, try to create a video player if (videoContainer) { - videoContainer.innerHTML = ` - - `; + const video = document.createElement('video'); + video.id = 'preview-video'; + video.controls = true; + video.style.cssText = 'width: 100%; max-width: 800px; height: auto; margin: 0 auto; display: block;'; + const source = document.createElement('source'); + source.src = `file://${filePath}`; + source.type = 'video/mp4'; + video.appendChild(source); + videoContainer.appendChild(video); } - - // Hide loading if (loadingIndicator) loadingIndicator.style.display = 'none'; } } catch (error) { console.error('Error loading video preview:', error); if (fileInfo) { - fileInfo.innerHTML = `Error: Could not load preview for ${filePath}`; + fileInfo.textContent = ''; + const strong = document.createElement('strong'); + strong.textContent = 'Error: '; + fileInfo.appendChild(strong); + fileInfo.appendChild(document.createTextNode('Could not load preview')); } if (videoContainer) { - videoContainer.innerHTML = '

Error loading video preview

'; + const p = document.createElement('p'); + p.style.cssText = 'color: #dc3545; text-align: center;'; + p.textContent = 'Error loading video preview'; + videoContainer.appendChild(p); } if (loadingIndicator) loadingIndicator.style.display = 'none'; } @@ -1698,14 +1721,13 @@ class UIManager { async handleFolderClick(folderPath, folderName) { console.log('Folder clicked:', folderName, 'at path:', folderPath); - // Validate folder path exists - const fs = require('fs'); - if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) { + // Validate folder path exists via IPC (no direct fs access) + const validation = await ipcRenderer.invoke('validate-folder', folderPath); + if (!validation.exists || !validation.isDirectory) { console.error('Invalid folder path:', folderPath); - return; // Don't navigate + return; } - // Open the folder await this.openDirectory(folderPath); }