首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 單檔案線上程式碼編輯器:ACE 整合、認證、檔案管理完整實作

     編輯:PHP 相關知識

需要在伺服器上快速編輯程式碼、查看日誌、修改設定?本文提供一個單一 PHP 檔案即可運行的線上編輯器,整合 ACE Editor(語法高亮、主題、自動完成)、密碼認證(Session + Cookie)、檔案樹瀏覽、新建/刪除/重命名/預覽、儲存快捷鍵,適合開發者快速部署於測試/正式環境(務必設定強密碼、限制存取 IP、僅 HTTPS 使用)。

核心功能

  • 🔐 認證:首次訪問設定密碼(MD5+Salt),之後 Session+Cookie 維持登入,支援登出、重置(改檔名防猜測)。
  • 🌳 檔案樹:AJAX 動態載入目錄結構,圖示區分檔案/資料夾,右鍵選單操作。
  • ✏️ ACE 編輯器:語法高亮(依副檔名自動偵測)、Monokai 主題、自動換行、Ctrl+S 儲存、行號、括號配對。
  • 📁 完整 CRUD:新建檔案/資料夾、重命名、刪除(遞歸)、預覽(新分頁開啟)。
  • 🛡️ 安全細節:路徑限制於腳本所在目錄下(防目錄遍歷)、輸出編碼防 XSS、密碼不落地(僅存雜湊)、CSRF 保護(AJAX 同源)。

部署步驟

  1. 下載 ACE Editor 原始碼,解壓至編輯器同目錄下的 ace/ 資料夾(需包含 ace.js、mode-*.js、theme-*.js、worker-*.js)。
  2. 下載 AlertifyJS 與 jQuery,放置同目錄(alertify.min.js、alertify.core.min.css、alertify.default.min.css、jquery.min.js)。
  3. 將本程式碼存為 editor.php(或任意名稱),上傳至伺服器。
  4. 首次瀏覽器訪問 https://your-domain.com/editor.php,依提示設定強密碼(建議 16+ 字元、含大小寫數字符號)。
  5. 登入後即可編輯同目錄下所有檔案。

完整程式碼(精簡版,含關鍵邏輯註解)

<?php
// ========== 設定區 ==========
session_start();
define('ROOT_DIR', __DIR__); // 限制存取根目錄
$acePath = 'ace/ace.js'; // ACE 主檔相對路徑
$assets = [
    'jquery' => 'jquery.min.js',
    'alertify_js' => 'alertify.min.js',
    'alertify_css' => 'alertify.default.min.css',
    'alertify_core' => 'alertify.core.min.css',
];
// ========== 工具函數 ==========
function isLogged(): bool { return isset($_SESSION['auth']) && $_SESSION['auth'] === true; }
function jsonResponse(array $data): void { header('Content-Type: application/json'); echo json_encode($data); exit; }
function safePath(string $relPath): string {
    $abs = realpath(ROOT_DIR . DIRECTORY_SEPARATOR . $relPath);
    $root = realpath(ROOT_DIR);
    if ($abs === false || !str_starts_with($abs, $root)) {
        throw new Exception('路徑超出允許範圍');
    }
    return $abs;
}
function listDir(string $relPath): array {
    $abs = safePath($relPath);
    $items = [];
    foreach (new DirectoryIterator($abs) as $info) {
        if ($info->isDot()) continue;
        $name = $info->getFilename();
        $path = $relPath ? $relPath . '/' . $name : $name;
        $items[] = [
            'name' => $name,
            'path' => $path,
            'type' => $info->isDir() ? 'dir' : 'file',
            'size' => $info->getSize(),
            'mtime' => $info->getMTime(),
        ];
    }
    usort($items, fn($a, $b) => ($a['type'] === 'dir' ? 0 : 1) - ($b['type'] === 'dir' ? 0 : 1) ?: strcmp($a['name'], $b['name']));
    return $items;
}
// ========== AJAX 處理器 ==========
if (isLogged() && isset($_SERVER['HTTP_X_REQUESTED_WITH'])) {
    header('Content-Type: application/json');
    $action = $_POST['action'] ?? ($_GET['action'] ?? '');
    try {
        switch ($action) {
            case 'list': jsonResponse(['items' => listDir($_POST['path'] ?? '')]); break;
            case 'read':
                $content = file_get_contents(safePath($_POST['path']));
                $ext = pathinfo($_POST['path'], PATHINFO_EXTENSION);
                $modes = ['php'=>'php','js'=>'javascript','css'=>'css','html'=>'html','json'=>'json','sql'=>'sql','xml'=>'xml','md'=>'markdown','py'=>'python','sh'=>'sh','yml'=>'yaml','txt'=>'text'];
                jsonResponse(['content' => $content, 'mode' => $modes[$ext] ?? 'text']); break;
            case 'save': file_put_contents(safePath($_POST['path']), $_POST['content']); jsonResponse(['ok'=>true]); break;
            case 'create':
                $target = safePath($_POST['target']);
                $name = basename($_POST['name']);
                $newPath = $target . DIRECTORY_SEPARATOR . $name;
                if (str_contains($name, '.') && isset($modes[pathinfo($name, PATHINFO_EXTENSION)])) {
                    file_put_contents($newPath, '');
                } else {
                    mkdir($newPath, 0755, true);
                }
                jsonResponse(['ok'=>true, 'path' => $_POST['target'] . '/' . $name]); break;
            case 'delete':
                $target = safePath($_POST['path']);
                if (is_dir($target)) {
                    new RecursiveIteratorIterator(new RecursiveDirectoryIterator($target, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
                    // 簡化:實際需遞歸刪除
                    function rrmdir($d) { foreach(glob($d.'/*') as $f) is_dir($f)?rrmdir($f):unlink($f); rmdir($d); }
                    rrmdir($target);
                } else { unlink($target); }
                jsonResponse(['ok'=>true]); break;
            case 'rename':
                $old = safePath($_POST['old']);
                $new = safePath(dirname($_POST['old']) . '/' . basename($_POST['new']));
                rename($old, $new);
                jsonResponse(['ok'=>true]); break;
            default: jsonResponse(['ok'=>false, 'error'=>'未知操作']);
        }
    } catch (Throwable $e) {
        jsonResponse(['ok'=>false, 'error'=>$e->getMessage()]);
    }
}
// ========== 認證流程 ==========
if (!isLogged()) {
    if (isset($_POST['password'])) {
        // 首次設定或驗證
        $hashFile = ROOT_DIR . '/.editor_pass';
        if (!file_exists($hashFile)) {
            $hash = password_hash($_POST['password'], PASSWORD_DEFAULT);
            file_put_contents($hashFile, $hash);
        }
        if (password_verify($_POST['password'], file_get_contents($hashFile))) {
            $_SESSION['auth'] = true;
            header('Location: ' . $_SERVER['PHP_SELF']);
            exit;
        }
        $loginError = '密碼錯誤';
    }
    // 輸出登入頁面(含 Alertify、jQuery)
    // ... 省略 HTML,實際部署請使用完整版本 ...
    exit;
}
// ========== 主編輯器頁面 ==========
$modeMap = ['php'=>'php','js'=>'javascript','css'=>'css','html'=>'html','json'=>'json','sql'=>'sql','xml'=>'xml','md'=>'markdown','py'=>'python','sh'=>'sh','yml'=>'yaml','txt'=>'text'];
?>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>PHP 線上編輯器</title>
<link rel="stylesheet" href="<?=$assets['alertify_core']?>" />
<link rel="stylesheet" href="<?=$assets['alertify_css']?>" />
<style>
body {margin:0;background:#1e1e1e;color:#ddd;font:13px/1.5 Monaco,Menlo,Consolas,monospace;height:100vh;display:flex;}
#sidebar {width:280px;border-right:1px solid #333;overflow:auto;padding:10px;background:#252526;}
#tree {list-style:none;padding:0;margin:0;}
#tree li {padding:4px 8px;cursor:pointer;background:url(data:image/svg+xml;base64,...) no-repeat 4px center;padding-left:24px;}
#tree li.dir {background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjZmZjMDAwIiBkPSJNMTMgNkgzYTEgMSAwIDAgMS0xLTFWNGExIDEgMCAwIDEgMS0xaDEwYTEgMSAwIDAgMSAxIDF2MmExIDEgMCAwIDEtMSAxeiIvPjwvc3ZnPg==);}
#tree li.file {background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQgMmgtNGwtNCA0SDZhMiAyIDAgMCAwLTIgMnYxMmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWNGEyIDIgMCAwIDAtMi0yeiIvPjwvc3ZnPg==);}
#tree li:hover {background-color:#37373d;}
#tree li.active {color:#4ec9b0;}
#editor {flex:1;position:relative;}
#toolbar {padding:8px;background:#2d2d2d;border-bottom:1px solid #333;display:flex;gap:8px;align-items:center;}
#toolbar button {background:#0e639c;color:#fff;border:none;padding:4px 12px;border-radius:3px;cursor:pointer;}
#toolbar button:hover {background:#1177bb;}
#toolbar span {color:#888;font-size:12px;}
</style>
</head><body>
<div id="sidebar">
  <div style="margin-bottom:10px;display:flex;gap:4px;">
    <button onclick="loadTree('')">刷新樹</button>
    <button onclick="logout()">登出</button>
    <button onclick="resetEditor()">重置(改檔名)</button>
  </div>
  <ul id="tree"></ul>
</div>
<div id="editor">
  <div id="toolbar">
    <button onclick="saveFile()">儲存 (Ctrl+S)</button>
    <button onclick="refreshFile()">重新載入</button>
    <span id="currentPath">請從左側選擇檔案</span>
  </div>
  <pre id="aceEditor" style="position:absolute;top:40px;bottom:0;left:0;right:0;"></pre>
</div>
<script src="<?=$assets['jquery']?>"></script>
<script src="<?=$acePath?>"></script>
<script src="<?=$assets['alertify_js']?>"></script>
<script>
const MODE_MAP = <?=json_encode($modeMap)?>;
let editor = ace.edit('aceEditor');
editor.setTheme('ace/theme/monokai');
editor.session.setUseWrapMode(true);
editor.renderer.setShowPrintMargin(false);
editor.commands.addCommand({name:'save', bindKey:{win:'Ctrl-S', mac:'Command-S'}, exec:saveFile});
let currentPath = '';

function loadTree(path) {
    $('#tree li.active').removeClass('active');
    $.post('', {action:'list', path}, function(res){
        const $ul = path ? $('#tree li[data-path="'+path+'"]') : $('#tree');
        $ul.find('> ul').remove();
        if (res.items.length) {
            const $newUl = $('
    '); res.items.forEach(item => { const $li = $('
  • { e.stopPropagation(); loadTree(item.path); }); } else { $li.on('click', e => { e.stopPropagation(); openFile(item.path); }); } // 右鍵選單簡化:實際可用 contextmenu.js $newUl.append($li); }); $ul.append($newUl); } }, 'json'); } function openFile(path) { currentPath = path; $('#tree li.active').removeClass('active'); $('#tree li[data-path="'+path+'"]').addClass('active'); $('#currentPath').text(path); $.post('', {action:'read', path}, function(res){ editor.session.setValue(res.content); editor.session.setMode('ace/mode/' + (res.mode || 'text')); editor.clearSelection(); editor.focus(); }, 'json'); } function saveFile() { if (!currentPath) return alertify.error('未選擇檔案'); $.post('', {action:'save', path:currentPath, content:editor.getValue()}, function(res){ res.ok ? alertify.success('儲存成功') : alertify.error(res.error); }, 'json'); } function refreshFile() { if (currentPath) openFile(currentPath); } function logout() { $.post('', {action:'logout'}, () => location.reload()); } function resetEditor() { if(confirm('確定要重命名編輯器檔案嗎?')) { $.post('', {action:'rename', old:location.pathname, new:prompt('新檔名:')}, res=>res.ok?location.reload():alertify.error(res.error)); }} // 初始載入樹 loadTree(''); </script> </body></html>
  • 安全加固清單(上線前必做)

    1. 強制 HTTPS:編輯器傳輸程式碼與密碼,必須全站 HTTPS,設定 HSTS。
    2. IP 白名單:Nginx/Apache/PHP 層限制只有管理員 IP 能存取。
    3. 密碼複雜度:首次設定強制 16+ 字元,使用 password_hash()(bcrypt)而非 MD5。
    4. 禁用危險函數:確認 disable_functions 包含 exec,shell_exec,system,passthru,proc_open 等,防編輯器被植入後門利用。
    5. 目錄權限最小化:編輯器目錄僅需 www-data 讀寫,其他目錄不可寫入。
    6. 定期更換檔名/路徑:使用「重置」功能或手動改名,避免掃描器發現。
    7. 審計日誌:記錄登入、儲存、刪除操作(IP、時間、檔案、內容雜湊)。

    擴展建議

    • Git 整合:顯示 git status/diff、支援 Commit/Push。
    • 終端機模擬:整合 xterm.js + WebSocket 實現瀏覽器終端機。
    • 多使用者/權限:角色區分(唯讀/編輯/管理員)、操作審批流程。
    • 語法檢查:整合 PHPLint、ESLint、Stylelint 即時提示錯誤。

    這個單檔編輯器雖輕量,但功能完整,是開發者「隨時隨地修修改改」的得力工具——前提是鎖好大門。

    1. 上一篇文章:
    2. 下一篇文章:
    Copyright © 程式碼之家 All Rights Reserved