Last active
October 31, 2025 04:02
-
-
Save brandonjp/bbcc81657795cfa1afe409c648f65d43 to your computer and use it in GitHub Desktop.
WP_Secure_Admin_File_Editor - PHP Code Snippet [SnipSnip.pro] - https://snipsnip.pro/s/922 - Modern WordPress file editor with JSON validation, auto-backups, and security controls for editing theme.json, plugins, and text files from wp-admin.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Title: WordPress Secure File Editor with Theme.json Support | |
| * Description: Adds a comprehensive file editor to WordPress admin for editing theme files, plugin files, and text-based media. Includes special support for theme.json editing with syntax validation and a modern interface with security controls. | |
| * Version: 2.3.0 | |
| * Author: SnipSnip Pro | |
| * Last Updated: 2025-10-30 | |
| * Blog URL: https://snipsnip.pro/s/922 | |
| * Requirements: WordPress 5.9+, PHP 7.4+ | |
| * License: GPL v2 or later | |
| * | |
| * Changelog: | |
| * 2.3.0 - Replaced Monokai with Material theme, added configurable theme settings | |
| * 2.2.0 - Fixed dark mode selection highlight color for better readability | |
| * 2.1.0 - Dark mode now only affects CodeMirror editor, not the surrounding container | |
| * 2.0.0 - Added CodeMirror for all file types, Monokai theme for dark mode, dark mode now editor-only | |
| * 1.9.0 - Fixed file reload issue with CodeMirror not properly destroying previous instance | |
| * 1.8.0 - Added configurable sidebar section order and custom quick access buttons | |
| * 1.7.0 - Added dark mode, version display, editor preferences, line numbers toggle | |
| * 1.6.0 - Fixed file reload issue where same file wouldn't reload after switching files | |
| * 1.5.0 - Unified notification system, auto-dismiss messages, consistent UX for all feedback | |
| * 1.4.0 - Fixed validation result button sizing issue | |
| * 1.3.0 - Fixed HTML rendering bug, renamed class to remove theme.json specificity, improved UI | |
| * 1.2.0 - Fixed manual path input bug, improved UI clarity, removed confusing browse section | |
| * 1.1.0 - Fixed editor height, added manual file path input, added quick access to common files | |
| * 1.0.0 - Initial release with theme.json editor, file browser, and security features | |
| */ | |
| if (!class_exists('WP_Secure_Admin_File_Editor')): | |
| class WP_Secure_Admin_File_Editor { | |
| const VERSION = '2.3.0'; | |
| private $capability_required = 'edit_themes'; | |
| private $allowed_extensions = ['php', 'css', 'js', 'json', 'txt', 'html', 'xml', 'md', 'svg']; | |
| private $base_paths = []; | |
| /** | |
| * Sidebar section order configuration | |
| * Valid sections: 'quick_access', 'manual_path', 'settings' | |
| * Reorder this array to change sidebar layout | |
| */ | |
| private $sidebar_sections = ['quick_access', 'manual_path', 'settings']; | |
| /** | |
| * Custom quick access links | |
| * Add custom links here - format: ['label' => 'File Label', 'path' => 'themes/theme-name/file.ext', 'icon' => '📄'] | |
| */ | |
| private $custom_quick_links = [ | |
| // Example: ['label' => 'Custom Config', 'path' => 'plugins/my-plugin/config.json', 'icon' => '⚙️'] | |
| ]; | |
| /** | |
| * Editor theme configuration | |
| * Dark theme options: 'material', 'dracula', 'tomorrow-night', 'base16-dark' | |
| * Light theme: 'default' | |
| */ | |
| private $dark_theme = 'material'; | |
| private $light_theme = 'default'; | |
| public function __construct() { | |
| // Set up allowed base paths | |
| $this->base_paths = [ | |
| 'themes' => get_theme_root(), | |
| 'plugins' => WP_PLUGIN_DIR, | |
| 'uploads' => wp_upload_dir()['basedir'] | |
| ]; | |
| add_action('admin_menu', [$this, 'add_admin_menu']); | |
| add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']); | |
| add_action('wp_ajax_sfe_load_file', [$this, 'ajax_load_file']); | |
| add_action('wp_ajax_sfe_save_file', [$this, 'ajax_save_file']); | |
| add_action('wp_ajax_sfe_browse_files', [$this, 'ajax_browse_files']); | |
| } | |
| /** | |
| * Add admin menu item | |
| */ | |
| public function add_admin_menu() { | |
| add_management_page( | |
| 'File Editor', | |
| 'File Editor', | |
| $this->capability_required, | |
| 'secure-file-editor', | |
| [$this, 'render_editor_page'] | |
| ); | |
| } | |
| /** | |
| * Enqueue CSS and JavaScript | |
| */ | |
| public function enqueue_assets($hook) { | |
| if ('tools_page_secure-file-editor' !== $hook) { | |
| return; | |
| } | |
| wp_enqueue_code_editor(['type' => 'application/json']); | |
| wp_enqueue_script('jquery'); | |
| $this->add_inline_styles(); | |
| $this->add_inline_scripts(); | |
| } | |
| /** | |
| * Add inline CSS | |
| */ | |
| private function add_inline_styles() { | |
| $css = <<<'STYLES' | |
| .sfe-container { | |
| display: flex; | |
| gap: 20px; | |
| margin: 20px 0; | |
| height: calc(100vh - 200px); | |
| } | |
| .sfe-version { | |
| display: inline-block; | |
| margin-left: 10px; | |
| padding: 2px 8px; | |
| background: #f0f0f1; | |
| border-radius: 3px; | |
| font-size: 11px; | |
| color: #646970; | |
| font-weight: normal; | |
| } | |
| .sfe-sidebar { | |
| flex: 0 0 300px; | |
| background: #fff; | |
| border: 1px solid #ccd0d4; | |
| padding: 15px; | |
| overflow-y: auto; | |
| } | |
| .sfe-settings { | |
| margin-bottom: 20px; | |
| padding-bottom: 15px; | |
| border-bottom: 1px solid #ccd0d4; | |
| } | |
| .sfe-quick-access { | |
| margin-bottom: 20px; | |
| padding-bottom: 15px; | |
| border-bottom: 1px solid #ccd0d4; | |
| } | |
| .sfe-manual-path-section { | |
| margin-bottom: 20px; | |
| padding-bottom: 15px; | |
| border-bottom: 1px solid #ccd0d4; | |
| } | |
| .sfe-settings h4, | |
| .sfe-quick-access h4, | |
| .sfe-manual-path-section h4 { | |
| margin: 0 0 10px 0; | |
| font-size: 13px; | |
| text-transform: uppercase; | |
| color: #646970; | |
| } | |
| .sfe-setting-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 0; | |
| font-size: 13px; | |
| } | |
| .sfe-setting-row label { | |
| cursor: pointer; | |
| } | |
| .sfe-toggle { | |
| position: relative; | |
| display: inline-block; | |
| width: 40px; | |
| height: 20px; | |
| } | |
| .sfe-toggle input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .sfe-toggle-slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: #ccc; | |
| transition: .3s; | |
| border-radius: 20px; | |
| } | |
| .sfe-toggle-slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 14px; | |
| width: 14px; | |
| left: 3px; | |
| bottom: 3px; | |
| background-color: white; | |
| transition: .3s; | |
| border-radius: 50%; | |
| } | |
| .sfe-toggle input:checked + .sfe-toggle-slider { | |
| background-color: #2271b1; | |
| } | |
| .sfe-toggle input:checked + .sfe-toggle-slider:before { | |
| transform: translateX(20px); | |
| } | |
| .cm-s-material.CodeMirror { | |
| background: #263238; | |
| color: #eeffff; | |
| } | |
| .cm-s-material .CodeMirror-gutters { | |
| background: #263238; | |
| border-right: 1px solid #37474f; | |
| } | |
| .cm-s-material .CodeMirror-linenumber { | |
| color: #546e7a; | |
| } | |
| .cm-s-material .CodeMirror-cursor { | |
| border-left: 1px solid #ffcc00; | |
| } | |
| .cm-s-material span.cm-comment { | |
| color: #546e7a; | |
| } | |
| .cm-s-material span.cm-atom { | |
| color: #f07178; | |
| } | |
| .cm-s-material span.cm-number { | |
| color: #f78c6c; | |
| } | |
| .cm-s-material span.cm-property { | |
| color: #82aaff; | |
| } | |
| .cm-s-material span.cm-attribute { | |
| color: #ffcb6b; | |
| } | |
| .cm-s-material span.cm-keyword { | |
| color: #c792ea; | |
| } | |
| .cm-s-material span.cm-string { | |
| color: #c3e88d; | |
| } | |
| .cm-s-material span.cm-variable { | |
| color: #eeffff; | |
| } | |
| .cm-s-material span.cm-variable-2 { | |
| color: #82aaff; | |
| } | |
| .cm-s-material span.cm-def { | |
| color: #82aaff; | |
| } | |
| .cm-s-material span.cm-bracket { | |
| color: #eeffff; | |
| } | |
| .cm-s-material span.cm-tag { | |
| color: #f07178; | |
| } | |
| .cm-s-material span.cm-link { | |
| color: #82aaff; | |
| } | |
| .cm-s-material span.cm-error { | |
| background: #ff5370; | |
| color: #fff; | |
| } | |
| .cm-s-material .CodeMirror-selected { | |
| background: #314549; | |
| } | |
| .cm-s-material .CodeMirror-line::selection, | |
| .cm-s-material .CodeMirror-line > span::selection, | |
| .cm-s-material .CodeMirror-line > span > span::selection { | |
| background: #314549; | |
| } | |
| .cm-s-material .CodeMirror-line::-moz-selection, | |
| .cm-s-material .CodeMirror-line > span::-moz-selection, | |
| .cm-s-material .CodeMirror-line > span > span::-moz-selection { | |
| background: #314549; | |
| } | |
| .sfe-editor-area { | |
| flex: 1; | |
| background: #fff; | |
| border: 1px solid #ccd0d4; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .sfe-file-tree { | |
| list-style: none; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| .sfe-file-tree li { | |
| padding: 5px 0; | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| .sfe-file-tree li:hover { | |
| background: #f0f0f1; | |
| } | |
| .sfe-folder { | |
| font-weight: 600; | |
| color: #2271b1; | |
| } | |
| .sfe-file { | |
| padding-left: 20px; | |
| color: #50575e; | |
| } | |
| .sfe-file.active { | |
| background: #2271b1; | |
| color: #fff; | |
| } | |
| .sfe-editor-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| padding-bottom: 15px; | |
| border-bottom: 1px solid #ccd0d4; | |
| } | |
| .sfe-file-info { | |
| font-size: 14px; | |
| color: #50575e; | |
| } | |
| .sfe-editor-actions { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .sfe-editor-content { | |
| flex: 1; | |
| position: relative; | |
| min-height: 400px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .sfe-editor-content textarea { | |
| width: 100%; | |
| flex: 1; | |
| min-height: 100%; | |
| font-family: 'Courier New', monospace; | |
| font-size: 13px; | |
| border: 1px solid #ccd0d4; | |
| padding: 10px; | |
| resize: none; | |
| box-sizing: border-box; | |
| } | |
| .sfe-editor-content .CodeMirror { | |
| height: 100%; | |
| min-height: 400px; | |
| border: 1px solid #ccd0d4; | |
| } | |
| .sfe-notice { | |
| margin: 15px 0; | |
| padding: 12px; | |
| border-left: 4px solid #00a32a; | |
| background: #fff; | |
| box-shadow: 0 1px 1px rgba(0,0,0,0.04); | |
| } | |
| .sfe-notice.error { | |
| border-left-color: #d63638; | |
| } | |
| .sfe-notice.warning { | |
| border-left-color: #dba617; | |
| } | |
| .sfe-quick-access { | |
| margin-bottom: 20px; | |
| padding-bottom: 15px; | |
| border-bottom: 1px solid #ccd0d4; | |
| } | |
| .sfe-quick-access h4 { | |
| margin: 0 0 10px 0; | |
| font-size: 13px; | |
| text-transform: uppercase; | |
| color: #646970; | |
| } | |
| .sfe-file-path-input { | |
| display: flex; | |
| gap: 5px; | |
| margin-bottom: 15px; | |
| } | |
| .sfe-file-path-input input { | |
| flex: 1; | |
| padding: 6px 10px; | |
| border: 1px solid #8c8f94; | |
| border-radius: 3px; | |
| font-size: 13px; | |
| } | |
| .sfe-file-path-input button { | |
| padding: 6px 12px; | |
| background: #2271b1; | |
| color: #fff; | |
| border: none; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| } | |
| .sfe-file-path-input button:hover { | |
| background: #135e96; | |
| } | |
| .sfe-quick-link { | |
| display: block; | |
| padding: 8px 10px; | |
| margin: 5px 0; | |
| background: #f6f7f7; | |
| border: 1px solid #dcdcde; | |
| border-radius: 3px; | |
| text-decoration: none; | |
| color: #2271b1; | |
| transition: all 0.2s; | |
| } | |
| .sfe-quick-link:hover { | |
| background: #2271b1; | |
| color: #fff; | |
| border-color: #2271b1; | |
| } | |
| .sfe-toast { | |
| position: fixed; | |
| top: 32px; | |
| right: 20px; | |
| min-width: 300px; | |
| max-width: 500px; | |
| padding: 12px 16px; | |
| background: #fff; | |
| border-left: 4px solid #00a32a; | |
| box-shadow: 0 3px 8px rgba(0,0,0,0.15); | |
| border-radius: 3px; | |
| font-size: 13px; | |
| z-index: 999999; | |
| animation: slideIn 0.3s ease-out; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .sfe-toast.error { | |
| border-left-color: #d63638; | |
| } | |
| .sfe-toast.warning { | |
| border-left-color: #dba617; | |
| } | |
| .sfe-toast.info { | |
| border-left-color: #2271b1; | |
| } | |
| .sfe-toast-close { | |
| margin-left: auto; | |
| cursor: pointer; | |
| color: #646970; | |
| font-size: 16px; | |
| line-height: 1; | |
| padding: 0 4px; | |
| } | |
| .sfe-toast-close:hover { | |
| color: #000; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| transform: translateX(400px); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| } | |
| .sfe-editor-actions { | |
| position: relative; | |
| } | |
| STYLES; | |
| wp_add_inline_style('wp-admin', $css); | |
| } | |
| /** | |
| * Add inline JavaScript | |
| */ | |
| private function add_inline_scripts() { | |
| $js = <<<'JAVASCRIPT' | |
| window.addEventListener("load", function() { | |
| (function createSecureFileEditorScope() { | |
| "use strict"; | |
| let currentFile = null; | |
| let currentEditor = null; | |
| let isDirty = false; | |
| function init() { | |
| loadSettings(); | |
| setupEventListeners(); | |
| loadQuickAccessFiles(); | |
| } | |
| function loadSettings() { | |
| const darkMode = localStorage.getItem('sfe_dark_mode') === 'true'; | |
| const lineNumbers = localStorage.getItem('sfe_line_numbers') !== 'false'; | |
| const wordWrap = localStorage.getItem('sfe_word_wrap') !== 'false'; | |
| document.getElementById('sfe-dark-mode').checked = darkMode; | |
| document.getElementById('sfe-line-numbers').checked = lineNumbers; | |
| document.getElementById('sfe-word-wrap').checked = wordWrap; | |
| } | |
| function setupEventListeners() { | |
| const saveButton = document.getElementById('sfe-save-button'); | |
| const validateButton = document.getElementById('sfe-validate-button'); | |
| const loadManualButton = document.getElementById('sfe-load-manual'); | |
| const manualPathInput = document.getElementById('sfe-manual-path'); | |
| const darkModeToggle = document.getElementById('sfe-dark-mode'); | |
| const lineNumbersToggle = document.getElementById('sfe-line-numbers'); | |
| const wordWrapToggle = document.getElementById('sfe-word-wrap'); | |
| if (saveButton) { | |
| saveButton.addEventListener('click', saveFile); | |
| } | |
| if (validateButton) { | |
| validateButton.addEventListener('click', validateJSON); | |
| } | |
| if (loadManualButton) { | |
| loadManualButton.addEventListener('click', function() { | |
| const filePath = manualPathInput.value.trim(); | |
| if (filePath) { | |
| loadFile(filePath); | |
| } else { | |
| showToast('Please enter a file path', 'error'); | |
| } | |
| }); | |
| } | |
| if (manualPathInput) { | |
| manualPathInput.addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| const filePath = this.value.trim(); | |
| if (filePath) { | |
| loadFile(filePath); | |
| } | |
| } | |
| }); | |
| } | |
| if (darkModeToggle) { | |
| darkModeToggle.addEventListener('change', function() { | |
| localStorage.setItem('sfe_dark_mode', this.checked); | |
| if (currentEditor) { | |
| currentEditor.codemirror.setOption('theme', this.checked ? 'material' : 'default'); | |
| } | |
| }); | |
| } | |
| if (lineNumbersToggle) { | |
| lineNumbersToggle.addEventListener('change', function() { | |
| localStorage.setItem('sfe_line_numbers', this.checked); | |
| if (currentEditor) { | |
| currentEditor.codemirror.setOption('lineNumbers', this.checked); | |
| } | |
| }); | |
| } | |
| if (wordWrapToggle) { | |
| wordWrapToggle.addEventListener('change', function() { | |
| localStorage.setItem('sfe_word_wrap', this.checked); | |
| if (currentEditor) { | |
| currentEditor.codemirror.setOption('lineWrapping', this.checked); | |
| } | |
| }); | |
| } | |
| window.addEventListener('beforeunload', function(e) { | |
| if (isDirty) { | |
| e.preventDefault(); | |
| e.returnValue = ''; | |
| } | |
| }); | |
| const editorTextarea = document.getElementById('sfe-editor-textarea'); | |
| if (editorTextarea) { | |
| editorTextarea.addEventListener('input', function() { | |
| isDirty = true; | |
| }); | |
| } | |
| } | |
| function loadQuickAccessFiles() { | |
| const quickLinks = document.querySelectorAll('[data-file-path]'); | |
| quickLinks.forEach(function(link) { | |
| link.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| const filePath = this.getAttribute('data-file-path'); | |
| loadFile(filePath); | |
| }); | |
| }); | |
| } | |
| function loadFile(filePath) { | |
| // Warn if unsaved changes | |
| if (isDirty && currentFile && currentFile !== filePath) { | |
| if (!confirm('You have unsaved changes. Continue loading new file?')) { | |
| return; | |
| } | |
| } | |
| showToast('Loading file...', 'info', 2000); | |
| jQuery.ajax({ | |
| url: ajaxurl, | |
| type: 'POST', | |
| data: { | |
| action: 'sfe_load_file', | |
| nonce: sfeData.nonce, | |
| file: filePath | |
| }, | |
| success: function(response) { | |
| if (response.success) { | |
| currentFile = filePath; | |
| displayFile(response.data); | |
| isDirty = false; | |
| } else { | |
| showToast(response.data.message, 'error'); | |
| } | |
| }, | |
| error: function() { | |
| showToast('Failed to load file', 'error'); | |
| } | |
| }); | |
| } | |
| function displayFile(data) { | |
| const fileInfo = document.getElementById('sfe-file-info'); | |
| const editorTextarea = document.getElementById('sfe-editor-textarea'); | |
| const validateButton = document.getElementById('sfe-validate-button'); | |
| const lineNumbers = document.getElementById('sfe-line-numbers').checked; | |
| const wordWrap = document.getElementById('sfe-word-wrap').checked; | |
| const darkMode = document.getElementById('sfe-dark-mode').checked; | |
| if (fileInfo) { | |
| fileInfo.textContent = data.file; | |
| } | |
| if (editorTextarea) { | |
| // Always destroy existing CodeMirror instance first | |
| if (currentEditor) { | |
| currentEditor.codemirror.toTextArea(); | |
| currentEditor = null; | |
| } | |
| // Reset textarea | |
| editorTextarea.style.display = ''; | |
| editorTextarea.disabled = false; | |
| editorTextarea.value = data.content; | |
| // Determine mode based on file extension | |
| let mode = 'text/plain'; | |
| if (data.file.endsWith('.json')) { | |
| mode = 'application/json'; | |
| } else if (data.file.endsWith('.css')) { | |
| mode = 'css'; | |
| } else if (data.file.endsWith('.js')) { | |
| mode = 'javascript'; | |
| } else if (data.file.endsWith('.php')) { | |
| mode = 'php'; | |
| } else if (data.file.endsWith('.html')) { | |
| mode = 'htmlmixed'; | |
| } else if (data.file.endsWith('.xml')) { | |
| mode = 'xml'; | |
| } else if (data.file.endsWith('.md')) { | |
| mode = 'markdown'; | |
| } | |
| // Initialize CodeMirror for all files if available | |
| if (typeof wp.codeEditor !== 'undefined') { | |
| currentEditor = wp.codeEditor.initialize(editorTextarea, { | |
| codemirror: { | |
| mode: mode, | |
| lineNumbers: lineNumbers, | |
| lineWrapping: wordWrap, | |
| theme: darkMode ? 'material' : 'default' | |
| } | |
| }); | |
| currentEditor.codemirror.on('change', function() { | |
| isDirty = true; | |
| }); | |
| } | |
| if (validateButton) { | |
| validateButton.style.display = data.file.endsWith('.json') ? 'inline-block' : 'none'; | |
| } | |
| } | |
| } | |
| function saveFile() { | |
| if (!currentFile) { | |
| showToast('No file loaded', 'error'); | |
| return; | |
| } | |
| let content; | |
| if (currentEditor) { | |
| content = currentEditor.codemirror.getValue(); | |
| } else { | |
| content = document.getElementById('sfe-editor-textarea').value; | |
| } | |
| showToast('Saving file...', 'info', 2000); | |
| jQuery.ajax({ | |
| url: ajaxurl, | |
| type: 'POST', | |
| data: { | |
| action: 'sfe_save_file', | |
| nonce: sfeData.nonce, | |
| file: currentFile, | |
| content: content | |
| }, | |
| success: function(response) { | |
| if (response.success) { | |
| showToast('✓ File saved successfully!', 'success', 3000); | |
| isDirty = false; | |
| } else { | |
| showToast(response.data.message, 'error'); | |
| } | |
| }, | |
| error: function() { | |
| showToast('Failed to save file', 'error'); | |
| } | |
| }); | |
| } | |
| function validateJSON() { | |
| let content; | |
| if (currentEditor) { | |
| content = currentEditor.codemirror.getValue(); | |
| } else { | |
| content = document.getElementById('sfe-editor-textarea').value; | |
| } | |
| try { | |
| JSON.parse(content); | |
| showToast('✓ Valid JSON', 'success'); | |
| } catch (e) { | |
| showToast('✗ Invalid JSON: ' + e.message, 'error'); | |
| } | |
| } | |
| function showToast(message, type, duration) { | |
| duration = duration || 4000; | |
| const existingToast = document.querySelector('.sfe-toast'); | |
| if (existingToast) { | |
| existingToast.remove(); | |
| } | |
| const toast = document.createElement('div'); | |
| toast.className = 'sfe-toast ' + (type || 'info'); | |
| const messageSpan = document.createElement('span'); | |
| messageSpan.textContent = message; | |
| const closeBtn = document.createElement('span'); | |
| closeBtn.className = 'sfe-toast-close'; | |
| closeBtn.innerHTML = '×'; | |
| closeBtn.onclick = function() { | |
| toast.remove(); | |
| }; | |
| toast.appendChild(messageSpan); | |
| toast.appendChild(closeBtn); | |
| document.body.appendChild(toast); | |
| setTimeout(function() { | |
| if (toast.parentNode) { | |
| toast.remove(); | |
| } | |
| }, duration); | |
| } | |
| init(); | |
| })(); | |
| }); | |
| JAVASCRIPT; | |
| wp_add_inline_script('jquery', $js); | |
| wp_localize_script('jquery', 'sfeData', [ | |
| 'nonce' => wp_create_nonce('sfe_editor_nonce'), | |
| 'ajaxurl' => admin_url('admin-ajax.php') | |
| ]); | |
| } | |
| /** | |
| * Get default quick access links | |
| */ | |
| private function get_default_quick_links() { | |
| $links = []; | |
| $theme = wp_get_theme(); | |
| // Theme.json | |
| if (file_exists(get_stylesheet_directory() . '/theme.json')) { | |
| $links[] = [ | |
| 'label' => 'theme.json', | |
| 'path' => 'themes/' . $theme->get_stylesheet() . '/theme.json', | |
| 'icon' => '📄' | |
| ]; | |
| } | |
| // Functions.php | |
| if (file_exists(get_stylesheet_directory() . '/functions.php')) { | |
| $links[] = [ | |
| 'label' => 'functions.php', | |
| 'path' => 'themes/' . $theme->get_stylesheet() . '/functions.php', | |
| 'icon' => '⚙️' | |
| ]; | |
| } | |
| // Style.css | |
| if (file_exists(get_stylesheet_directory() . '/style.css')) { | |
| $links[] = [ | |
| 'label' => 'style.css', | |
| 'path' => 'themes/' . $theme->get_stylesheet() . '/style.css', | |
| 'icon' => '🎨' | |
| ]; | |
| } | |
| return $links; | |
| } | |
| /** | |
| * Render quick access section | |
| */ | |
| private function render_quick_access() { | |
| $default_links = $this->get_default_quick_links(); | |
| $all_links = array_merge($default_links, $this->custom_quick_links); | |
| if (empty($all_links)) { | |
| return; | |
| } | |
| ?> | |
| <div class="sfe-quick-access"> | |
| <h4>Quick Access</h4> | |
| <?php foreach ($all_links as $link): ?> | |
| <a href="#" class="sfe-quick-link" data-file-path="<?php echo esc_attr($link['path']); ?>"> | |
| <?php echo esc_html($link['icon']); ?> <?php echo esc_html($link['label']); ?> | |
| </a> | |
| <?php endforeach; ?> | |
| </div> | |
| <?php | |
| } | |
| /** | |
| * Render manual path section | |
| */ | |
| private function render_manual_path() { | |
| ?> | |
| <div class="sfe-manual-path-section"> | |
| <h4>Manual File Path</h4> | |
| <div class="sfe-file-path-input"> | |
| <input type="text" id="sfe-manual-path" placeholder="e.g., themes/twentytwentyfour/theme.json" /> | |
| <button type="button" id="sfe-load-manual">Load</button> | |
| </div> | |
| <p style="font-size: 12px; color: #646970; margin-top: 5px;"> | |
| Enter path format: <code>themes/your-theme/file.php</code><br> | |
| or <code>plugins/plugin-name/config.json</code> | |
| </p> | |
| </div> | |
| <?php | |
| } | |
| /** | |
| * Render settings section | |
| */ | |
| private function render_settings() { | |
| ?> | |
| <div class="sfe-settings"> | |
| <h4>Editor Settings</h4> | |
| <div class="sfe-setting-row"> | |
| <label for="sfe-dark-mode">Dark Mode</label> | |
| <label class="sfe-toggle"> | |
| <input type="checkbox" id="sfe-dark-mode"> | |
| <span class="sfe-toggle-slider"></span> | |
| </label> | |
| </div> | |
| <div class="sfe-setting-row"> | |
| <label for="sfe-line-numbers">Line Numbers</label> | |
| <label class="sfe-toggle"> | |
| <input type="checkbox" id="sfe-line-numbers" checked> | |
| <span class="sfe-toggle-slider"></span> | |
| </label> | |
| </div> | |
| <div class="sfe-setting-row"> | |
| <label for="sfe-word-wrap">Word Wrap</label> | |
| <label class="sfe-toggle"> | |
| <input type="checkbox" id="sfe-word-wrap" checked> | |
| <span class="sfe-toggle-slider"></span> | |
| </label> | |
| </div> | |
| </div> | |
| <?php | |
| } | |
| /** | |
| * Render the editor page | |
| */ | |
| public function render_editor_page() { | |
| if (!current_user_can($this->capability_required)) { | |
| wp_die(__('You do not have sufficient permissions to access this page.')); | |
| } | |
| ?> | |
| <div class="wrap"> | |
| <h1> | |
| Secure File Editor | |
| <span class="sfe-version">v<?php echo esc_html(self::VERSION); ?></span> | |
| </h1> | |
| <p>Edit theme files, plugin files, and text-based content with a modern interface and security controls.</p> | |
| <div class="sfe-container"> | |
| <div class="sfe-sidebar"> | |
| <?php | |
| // Render sidebar sections in configured order | |
| foreach ($this->sidebar_sections as $section) { | |
| switch ($section) { | |
| case 'quick_access': | |
| $this->render_quick_access(); | |
| break; | |
| case 'manual_path': | |
| $this->render_manual_path(); | |
| break; | |
| case 'settings': | |
| $this->render_settings(); | |
| break; | |
| } | |
| } | |
| ?> | |
| </div> | |
| <div class="sfe-editor-area"> | |
| <div class="sfe-editor-header"> | |
| <div class="sfe-file-info" id="sfe-file-info">No file loaded</div> | |
| <div class="sfe-editor-actions"> | |
| <button type="button" class="button button-primary" id="sfe-save-button">Save Changes</button> | |
| <button type="button" class="button" id="sfe-validate-button" style="display: none;">Validate JSON</button> | |
| </div> | |
| </div> | |
| <div class="sfe-editor-content"> | |
| <textarea id="sfe-editor-textarea" placeholder="Load a file to begin editing..."></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="margin-top: 20px; padding: 15px; background: #fff3cd; border-left: 4px solid #ffc107;"> | |
| <strong>⚠️ Security Warning:</strong> Only authorized administrators should use this tool. Always backup files before editing. Invalid code can break your site. | |
| </div> | |
| </div> | |
| <?php | |
| } | |
| /** | |
| * AJAX: Load file content | |
| */ | |
| public function ajax_load_file() { | |
| check_ajax_referer('sfe_editor_nonce', 'nonce'); | |
| if (!current_user_can($this->capability_required)) { | |
| wp_send_json_error(['message' => 'Insufficient permissions']); | |
| } | |
| $file = sanitize_text_field($_POST['file']); | |
| $real_path = $this->validate_file_path($file); | |
| if (!$real_path) { | |
| wp_send_json_error(['message' => 'Invalid file path']); | |
| } | |
| if (!file_exists($real_path)) { | |
| wp_send_json_error(['message' => 'File does not exist']); | |
| } | |
| if (!is_readable($real_path)) { | |
| wp_send_json_error(['message' => 'File is not readable']); | |
| } | |
| $content = file_get_contents($real_path); | |
| if ($content === false) { | |
| wp_send_json_error(['message' => 'Failed to read file']); | |
| } | |
| wp_send_json_success([ | |
| 'file' => $file, | |
| 'content' => $content, | |
| 'size' => filesize($real_path), | |
| 'modified' => date('Y-m-d H:i:s', filemtime($real_path)) | |
| ]); | |
| } | |
| /** | |
| * AJAX: Save file content | |
| */ | |
| public function ajax_save_file() { | |
| check_ajax_referer('sfe_editor_nonce', 'nonce'); | |
| if (!current_user_can($this->capability_required)) { | |
| wp_send_json_error(['message' => 'Insufficient permissions']); | |
| } | |
| $file = sanitize_text_field($_POST['file']); | |
| $content = wp_unslash($_POST['content']); | |
| $real_path = $this->validate_file_path($file); | |
| if (!$real_path) { | |
| wp_send_json_error(['message' => 'Invalid file path']); | |
| } | |
| if (!file_exists($real_path)) { | |
| wp_send_json_error(['message' => 'File does not exist']); | |
| } | |
| if (!is_writable($real_path)) { | |
| wp_send_json_error(['message' => 'File is not writable']); | |
| } | |
| // Validate JSON files | |
| if (pathinfo($real_path, PATHINFO_EXTENSION) === 'json') { | |
| json_decode($content); | |
| if (json_last_error() !== JSON_ERROR_NONE) { | |
| wp_send_json_error(['message' => 'Invalid JSON: ' . json_last_error_msg()]); | |
| } | |
| } | |
| // Create backup | |
| $backup_path = $real_path . '.backup-' . date('Y-m-d-His'); | |
| copy($real_path, $backup_path); | |
| $result = file_put_contents($real_path, $content); | |
| if ($result === false) { | |
| wp_send_json_error(['message' => 'Failed to save file']); | |
| } | |
| wp_send_json_success([ | |
| 'message' => 'File saved successfully', | |
| 'backup' => basename($backup_path) | |
| ]); | |
| } | |
| /** | |
| * AJAX: Browse files | |
| */ | |
| public function ajax_browse_files() { | |
| check_ajax_referer('sfe_editor_nonce', 'nonce'); | |
| if (!current_user_can($this->capability_required)) { | |
| wp_send_json_error(['message' => 'Insufficient permissions']); | |
| } | |
| $path = sanitize_text_field($_POST['path']); | |
| $real_path = $this->validate_file_path($path); | |
| if (!$real_path || !is_dir($real_path)) { | |
| wp_send_json_error(['message' => 'Invalid directory']); | |
| } | |
| $files = scandir($real_path); | |
| $items = []; | |
| foreach ($files as $file) { | |
| if ($file === '.' || $file === '..') { | |
| continue; | |
| } | |
| $full_path = $real_path . '/' . $file; | |
| $items[] = [ | |
| 'name' => $file, | |
| 'type' => is_dir($full_path) ? 'dir' : 'file', | |
| 'path' => $path . '/' . $file | |
| ]; | |
| } | |
| wp_send_json_success($items); | |
| } | |
| /** | |
| * Validate and resolve file path | |
| */ | |
| private function validate_file_path($file) { | |
| // Parse the path | |
| $parts = explode('/', trim($file, '/')); | |
| if (count($parts) < 2) { | |
| return false; | |
| } | |
| $base_type = array_shift($parts); | |
| if (!isset($this->base_paths[$base_type])) { | |
| return false; | |
| } | |
| $base_path = $this->base_paths[$base_type]; | |
| $real_path = realpath($base_path . '/' . implode('/', $parts)); | |
| if (!$real_path || strpos($real_path, $base_path) !== 0) { | |
| return false; | |
| } | |
| $extension = pathinfo($real_path, PATHINFO_EXTENSION); | |
| if (!in_array($extension, $this->allowed_extensions)) { | |
| return false; | |
| } | |
| return $real_path; | |
| } | |
| } | |
| endif; | |
| if (class_exists('WP_Secure_Admin_File_Editor')): | |
| new WP_Secure_Admin_File_Editor(); | |
| endif; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
WordPress Secure File Editor
A modern, secure file editor for WordPress with CodeMirror syntax highlighting, dark mode, and support for theme.json and all text-based files.
Installation
Add this code through Code Snippets Pro ❤️ or paste into your theme's functions.php. Once activated, find "File Editor" under Tools in WordPress admin.
Documentation & Customization
📖 Read the Complete Guide - Features, configuration options, security details, and usage examples.
Version: 2.3.0