commit da9084d8737b392d254977e67322fa44f8d4bd3f Author: Daniel Messer Date: Thu Oct 23 13:12:13 2025 -0400 Initial commit. I think this works. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34ebcc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# ---> macOS +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..9d03a66 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,11 @@ +# Installing SimplyReports SQL Extractor (SiReS Ex) + +1. Download ```sires-ex.xpi``` +2. Drag that file into Firefox +3. Click "Add" when prompted + +## Troubleshooting + +- If the icon doesn't appear, check the Extensions page (`about:addons`) +- Make sure the target page has an input with id or name "SQLStatementHide" +- Check browser console for any error messages diff --git a/README.md b/README.md new file mode 100644 index 0000000..0243a2b --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# SimplyReports SQL Extractor (SiReS Ex) + +The SimplyReports SQL Extractor is a Firefox add-on that will pull the SQL query from a SimplyReports results page. After extracting the query, it allows you to copy that query to the clipboard or save it as a file. + +## Features + +- Visual indicator when a SimplyReports SQL query is found on a page +- Copy extracted SQL to clipboard +- Save SQL as .sql file with custom filename +- Basic SQL formatting for better readability +- Auto-extraction when opening the extension popup + +## Usage + +1. Build a report in SimplyReports. +2. Run the report. +3. On the results page, you should see a green indicator on the top right of the page that indicates that SiReS Ex found a SimplyReports query in your results page. +4. Click the add-on's icon and you'll be prompted to copy the query to the clipboard or save the query to a file. diff --git a/background.js b/background.js new file mode 100644 index 0000000..49f86bb --- /dev/null +++ b/background.js @@ -0,0 +1,29 @@ +// background.js - Background script for handling downloads + +browser.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "downloadSQL") { + const { sqlQuery, filename } = request; + + // Create blob with SQL content + const blob = new Blob([sqlQuery], { type: 'text/sql' }); + const url = URL.createObjectURL(blob); + + // Generate filename if not provided + const finalFilename = filename || `sql_query_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.sql`; + + // Download the file + browser.downloads.download({ + url: url, + filename: finalFilename, + saveAs: true + }).then(() => { + URL.revokeObjectURL(url); + sendResponse({ success: true }); + }).catch((error) => { + console.error('Download failed:', error); + sendResponse({ success: false, error: error.message }); + }); + + return true; // Keep the message channel open for async response + } +}); diff --git a/content.js b/content.js new file mode 100644 index 0000000..3054834 --- /dev/null +++ b/content.js @@ -0,0 +1,89 @@ +// content.js - Content script that runs on web pages + +function extractSQLQuery() { + // Look for the specific input element + const sqlInput = document.getElementById('SQLStatementHide'); + + if (sqlInput) { + const sqlQuery = sqlInput.value; + if (sqlQuery && sqlQuery.trim()) { + return cleanupSQLQuery(sqlQuery.trim()); + } else { + return null; + } + } + + // Fallback: look for any input with name="SQLStatementHide" + const sqlInputByName = document.querySelector('input[name="SQLStatementHide"]'); + if (sqlInputByName) { + const sqlQuery = sqlInputByName.value; + if (sqlQuery && sqlQuery.trim()) { + return cleanupSQLQuery(sqlQuery.trim()); + } + } + + return null; +} + +function cleanupSQLQuery(sqlQuery) { + // Fix double single quotes around dates/values (''2017-10-23'' becomes '2017-10-23') + // This handles the common issue where systems incorrectly double-escape quotes + return sqlQuery.replace(/''/g, "'"); +} + +// Listen for messages from popup +browser.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "extractSQL") { + const sqlQuery = extractSQLQuery(); + sendResponse({ + success: sqlQuery !== null, + sqlQuery: sqlQuery, + url: window.location.href, + timestamp: new Date().toISOString() + }); + } +}); + +// Optional: Add a visual indicator when SQL is found +function addVisualIndicator() { + const sqlInput = document.getElementById('SQLStatementHide') || + document.querySelector('input[name="SQLStatementHide"]'); + + if (sqlInput && sqlInput.value && sqlInput.value.trim()) { + // Add a small visual indicator that SQL was found + if (!document.getElementById('sql-extractor-indicator')) { + const indicator = document.createElement('div'); + indicator.id = 'sql-extractor-indicator'; + indicator.style.cssText = ` + position: fixed; + top: 10px; + right: 10px; + background: #4CAF50; + color: white; + padding: 5px 10px; + border-radius: 3px; + font-size: 12px; + z-index: 10000; + font-family: Arial, sans-serif; + opacity: 0.8; + pointer-events: none; + `; + indicator.textContent = 'SQL Query Found'; + document.body.appendChild(indicator); + + // Remove indicator after 3 seconds + setTimeout(() => { + if (document.getElementById('sql-extractor-indicator')) { + document.body.removeChild(indicator); + } + }, 3000); + } + } +} + +// Run indicator check when page loads +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', addVisualIndicator); +} else { + addVisualIndicator(); +} diff --git a/icon-128.png b/icon-128.png new file mode 100644 index 0000000..56f5fce Binary files /dev/null and b/icon-128.png differ diff --git a/icon-16.png b/icon-16.png new file mode 100644 index 0000000..0508a1b Binary files /dev/null and b/icon-16.png differ diff --git a/icon-32.png b/icon-32.png new file mode 100644 index 0000000..3f7c952 Binary files /dev/null and b/icon-32.png differ diff --git a/icon-48.png b/icon-48.png new file mode 100644 index 0000000..f3fc2b3 Binary files /dev/null and b/icon-48.png differ diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..8145d60 --- /dev/null +++ b/icon.svg @@ -0,0 +1,11 @@ + + + + + + SQL + + + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f7c314d --- /dev/null +++ b/manifest.json @@ -0,0 +1,42 @@ +{ + "manifest_version": 2, + "name": "SimplyReports SQL Extractor", + "version": "1.0", + "description": "Grab the SQL query used in a SimplyReports results page and copy it to the clipboard or save it to a file.", + + "permissions": [ + "activeTab", + "clipboardWrite", + "downloads" + ], + + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"] + } + ], + + "background": { + "scripts": ["background.js"], + "persistent": false + }, + + "browser_action": { + "default_popup": "popup.html", + "default_title": "Extract SQL Query", + "default_icon": { + "16": "icon-16.png", + "32": "icon-32.png", + "48": "icon-48.png", + "128": "icon-128.png" + } + }, + + "icons": { + "16": "icon-16.png", + "32": "icon-32.png", + "48": "icon-48.png", + "128": "icon-128.png" + } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..cd7ccb2 --- /dev/null +++ b/popup.html @@ -0,0 +1,149 @@ + + + + + + + +
+

SQL Query Extractor

+
+ + + + + +
+ + + +
+ +
+ Click "Extract SQL Query" to search for SQL in the current page +
+ + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..24ad42b --- /dev/null +++ b/popup.js @@ -0,0 +1,167 @@ +// popup.js - Popup script for user interactions + +let currentSQLQuery = null; + +document.addEventListener('DOMContentLoaded', function() { + const extractBtn = document.getElementById('extractBtn'); + const copyBtn = document.getElementById('copyBtn'); + const downloadBtn = document.getElementById('downloadBtn'); + const filenameInput = document.getElementById('filenameInput'); + const statusDiv = document.getElementById('status'); + const sqlPreview = document.getElementById('sqlPreview'); + const actionButtons = document.getElementById('actionButtons'); + + // Extract SQL Query + extractBtn.addEventListener('click', function() { + extractBtn.disabled = true; + extractBtn.textContent = 'Extracting...'; + + // Send message to content script + browser.tabs.query({ active: true, currentWindow: true }, function(tabs) { + browser.tabs.sendMessage(tabs[0].id, { action: "extractSQL" }, function(response) { + extractBtn.disabled = false; + extractBtn.textContent = 'Extract SQL Query'; + + if (response && response.success) { + // Debug: Log the original SQL + console.log('Original SQL:', response.sqlQuery); + + // Store the formatted version as the main query + currentSQLQuery = formatSQL(response.sqlQuery); + + // Debug: Log the formatted SQL + console.log('Formatted SQL:', currentSQLQuery); + console.log('Formatted SQL contains newlines:', currentSQLQuery.includes('\n')); + + showStatus('success', 'SQL Query found and extracted!'); + showSQLPreview(currentSQLQuery); + actionButtons.style.display = 'block'; + + // Set default filename based on current page + const url = new URL(response.url); + const defaultName = `sql_query_${url.hostname}_${new Date().toISOString().slice(0, 10)}.sql`; + filenameInput.value = defaultName; + } else { + showStatus('error', 'No SQL query found in the current page'); + actionButtons.style.display = 'none'; + sqlPreview.style.display = 'none'; + currentSQLQuery = null; + } + }); + }); + }); + + // Copy to Clipboard + copyBtn.addEventListener('click', function() { + if (!currentSQLQuery) return; + + // Debug: Log what we're trying to copy + console.log('Copying SQL:', currentSQLQuery); + console.log('SQL length:', currentSQLQuery.length); + console.log('Contains newlines:', currentSQLQuery.includes('\n')); + + navigator.clipboard.writeText(currentSQLQuery).then(function() { + const originalText = copyBtn.textContent; + copyBtn.textContent = 'Copied!'; + copyBtn.style.backgroundColor = '#28a745'; + + setTimeout(function() { + copyBtn.textContent = originalText; + copyBtn.style.backgroundColor = ''; + }, 1500); + }).catch(function(err) { + showStatus('error', 'Failed to copy to clipboard: ' + err.message); + }); + }); + + // Download as File + downloadBtn.addEventListener('click', function() { + if (!currentSQLQuery) return; + + downloadBtn.disabled = true; + downloadBtn.textContent = 'Downloading...'; + + const filename = filenameInput.value.trim() || null; + + // Send message to background script + browser.runtime.sendMessage({ + action: "downloadSQL", + sqlQuery: currentSQLQuery, + filename: filename + }, function(response) { + downloadBtn.disabled = false; + downloadBtn.textContent = 'Save as File'; + + if (response && response.success) { + const originalText = downloadBtn.textContent; + downloadBtn.textContent = 'Downloaded!'; + downloadBtn.style.backgroundColor = '#28a745'; + + setTimeout(function() { + downloadBtn.textContent = originalText; + downloadBtn.style.backgroundColor = ''; + }, 1500); + } else { + showStatus('error', 'Failed to download file: ' + (response ? response.error : 'Unknown error')); + } + }); + }); + + function showStatus(type, message) { + statusDiv.className = `status ${type}`; + statusDiv.textContent = message; + statusDiv.style.display = 'block'; + + // Auto-hide after 5 seconds + setTimeout(function() { + statusDiv.style.display = 'none'; + }, 5000); + } + + function showSQLPreview(sqlQuery) { + sqlPreview.textContent = sqlQuery; + sqlPreview.style.display = 'block'; + } + + function formatSQL(sql) { + // Simple but reliable SQL formatting + let formatted = sql.trim(); + + // Normalize whitespace first + formatted = formatted.replace(/\s+/g, ' '); + + // Add line breaks before major keywords (simple approach) + formatted = formatted.replace(/\sFROM\s/gi, '\nFROM '); + formatted = formatted.replace(/\sWHERE\s/gi, '\nWHERE '); + formatted = formatted.replace(/\sAND\s/gi, '\n AND '); + formatted = formatted.replace(/\sOR\s/gi, '\n OR '); + formatted = formatted.replace(/\sORDER\sBY\s/gi, '\nORDER BY '); + formatted = formatted.replace(/\sGROUP\sBY\s/gi, '\nGROUP BY '); + formatted = formatted.replace(/\sHAVING\s/gi, '\nHAVING '); + formatted = formatted.replace(/\sJOIN\s/gi, '\nJOIN '); + formatted = formatted.replace(/\sINNER\sJOIN\s/gi, '\nINNER JOIN '); + formatted = formatted.replace(/\sLEFT\sJOIN\s/gi, '\nLEFT JOIN '); + formatted = formatted.replace(/\sRIGHT\sJOIN\s/gi, '\nRIGHT JOIN '); + + // Add semicolon if missing + if (!formatted.trim().endsWith(';')) { + formatted += ';'; + } + + // Clean up any leading/trailing whitespace on lines + formatted = formatted.split('\n').map(line => line.trim()).join('\n'); + + return formatted; + } + + // Auto-extract on popup open if we're on a page that might have SQL + browser.tabs.query({ active: true, currentWindow: true }, function(tabs) { + const currentTab = tabs[0]; + if (currentTab.url && !currentTab.url.startsWith('chrome://') && !currentTab.url.startsWith('moz-extension://')) { + // Auto-extract after a short delay + setTimeout(function() { + extractBtn.click(); + }, 500); + } + }); +});