Skip to content

Instantly share code, notes, and snippets.

@brandonjp
Last active October 31, 2025 04:02
Show Gist options
  • Select an option

  • Save brandonjp/bbcc81657795cfa1afe409c648f65d43 to your computer and use it in GitHub Desktop.

Select an option

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.
<?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 = '&times;';
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;
@brandonjp
Copy link
Author

brandonjp commented Oct 31, 2025

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

WordPress Secure File Editor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment