Bring On the WordPress File Editor (The Smart Way)

Cleanshot 2025 10 30 at 21.22.33

Remember the WordPress file editor? It’s technically still there, but many hosts disable it with DISALLOW_FILE_EDIT, and even when available, it only works with theme and plugin PHP files. Need to edit a custom or svg file? You’re back to firing up FileZilla like it’s 2005. Let’s fix that.

What it does…

This snippet creates a full-featured file editor right in your WordPress admin (under Tools → File Editor) that lets you edit theme files, plugin files, and even text-based uploads. You get a clean split-panel interface with quick access buttons for common files like theme.json, functions.php, and style.css, plus a manual path input for accessing any file you need. The editor includes CodeMirror syntax highlighting for all file types (PHP, CSS, JavaScript, JSON, HTML, XML, Markdown), a dark mode with the Material theme for late-night coding sessions, built-in JSON validation to catch errors before you save, and automatic file backups so you never lose your work. It’s basically bringing back the old WordPress file editor, but with modern features and actual security measures.

Why it does it…

WordPress still has a built-in file editor, but many hosting providers and security plugins disable it by default using the DISALLOW_FILE_EDIT constant. Even when available, the native editor has limitations—it only works with theme and plugin files, has no backup system, and lacks features like JSON validation or quick access to configuration files. This snippet solves those problems by implementing a standalone editor with proper security layers: capability checks ensure only authorized admins can access it, nonce verification prevents CSRF attacks, path validation stops directory traversal exploits, and file extension whitelisting blocks execution of dangerous file types. The automatic backup system means even if something goes wrong, you have an immediate recovery option with timestamped copies. Plus, you get JSON validation for theme.json files, CodeMirror syntax highlighting for all supported file types, and access to any text-based file in your themes, plugins, or uploads directories—not just PHP files. The Material dark theme was specifically chosen because it provides excellent text contrast when selecting code, unlike some other dark themes that become unreadable.

How it does it…

    • The WP_Secure_Admin_File_Editor class encapsulates all functionality and uses WordPress hooks to integrate seamlessly with the admin panel via admin_menu and admin_enqueue_scripts actions
    • Security is enforced through validate_file_path() which uses realpath() to resolve paths and strpos() to verify files are within allowed base directories (themes, plugins, uploads), preventing directory traversal attacks
    • The ajax_load_file() and ajax_save_file() methods handle file operations via AJAX with check_ajax_referer() for nonce validation and current_user_can() for capability checks on every request
    • Before any file save operation, the code creates a timestamped backup using copy() with the pattern .backup-Y-m-d-His, allowing quick rollback if needed
    • JSON files get special validation through json_decode() before saving, with CodeMirror integration providing real-time syntax highlighting and error detection for all file types
    • Uses WordPress’s built-in wp_code_editor API to initialize CodeMirror with automatic mode detection based on file extensions (JSON, CSS, JS, PHP, HTML, XML, MD), applying the Material theme when dark mode is enabled and keeping the default theme for light mode
    • The sidebar sections ($sidebar_sections) are fully configurable and render in custom order through helper methods (render_quick_access(), render_manual_path(), render_settings()), allowing you to rearrange the interface
    • Custom quick access links can be added via the $custom_quick_links array with label, path, and icon properties for frequently accessed files
    • The JavaScript uses an IIFE to create a private scope and prevent variable conflicts, with editor state managed through closures tracking currentFile, currentEditor, and isDirty flags
    • Implements a custom toast notification system with auto-dismiss timers that appear as floating messages in the top-right corner, avoiding layout shifts and providing consistent feedback for all operations
    • Editor preferences (dark mode, line numbers, word wrap) persist in browser localStorage and dynamically update CodeMirror options when toggled

    Configuration Options

    The snippet includes several configuration properties at the top of the class that you can customize:

    Sidebar Sections Order:

     
    private $sidebar_sections = ['quick_access', 'manual_path', 'settings'];

    Reorder these three values to change the sidebar layout. Valid sections: 'quick_access', 'manual_path', 'settings'.

    Custom Quick Access Links:

     
    private $custom_quick_links = [
        ['label' => 'Custom Config', 'path' => 'plugins/my-plugin/config.json', 'icon' => '⚙️'],
        ['label' => 'Site Settings', 'path' => 'themes/my-theme/settings.php', 'icon' => '🔧']
    ];

    Add custom quick access buttons with a label, relative path from WordPress root, and emoji icon.

    Editor Themes:

     
    private $dark_theme = 'material';
    private $light_theme = 'default';

    Change the dark theme (currently Material for better selection contrast). Other themes would require adding their CSS.

    Security Settings:

     
    private $capability_required = 'edit_themes';
    private $allowed_extensions = ['php', 'css', 'js', 'json', 'txt', 'html', 'xml', 'md', 'svg'];

    Adjust required user capability or modify the list of editable file extensions.

    Installation

    Add this code through Code Snippets Pro ❤️, ASE or similar; OR, paste it into your theme’s functions.php file. Once activated, you’ll find “File Editor” under the Tools menu in your WordPress admin. The editor requires edit_themes capability by default, so it’s restricted to administrators. Type a file path manually (like themes/twentytwentyfour/theme.json) or click the quick access links to start editing.

See the code…

<?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;

See the code on github…

Leave a Reply

Your email address will not be published. Required fields are marked *