file_manager הוא עורך קוד ודפדפן קבצים בתוך פאנל הניהול, שנועד למפתחים: עורכים תבנית, קובץ SCSS או קובץ הגדרות ישירות על השרת, עם בדיקת תחביר לפני שמירה, זיהוי התנגשות עם מי שערך במקביל, ושמירת גרסאות קודמות. הוא שונה מספריית המדיה ({admin}/storage, ראו אחסון קבצים), שמיועדת לתוכן של עורכי האתר.
system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו), ו-{admin} הוא CONFIG::$admin_url. הפאנל נמצא ב-system/admin/file_manager.php (המחלקה ADMINMODULE_file_manager), הלוגיקה ב-system/libraries/FileManager.php, בדיקות התחביר ב-system/libraries/FileLint.php, והממשק ב-assets/file_manager/app.js.
מי שיכול לפתוח את הפאנל יכול לכתוב קובץ PHP ולהריץ אותו. לכן הוא פתוח רק למפתחים, ולכן ההרשאה לא נסמכת על תצורת הפאנל בלבד (ראו הרשאות). אל תיתנו אותה לעורכי תוכן.
כתובות ופעולות#
| כתובת | מה היא |
|---|---|
{admin}/file_manager | המסך עצמו |
{admin}/file_manager/api | נקודת JSON, הפעולה נבחרת עם action (ב-body כ-JSON, ב-$_POST או ב-?action=) |
{admin}/file_manager/download?path=<נתיב מלא> | הורדת קובץ כ-application/octet-stream |
פעולות קריאה: capabilities, list, read, lint, history, history_content, find_files, search_content.
פעולות כתיבה: write, create_file, create_dir, rename, move, copy, delete, upload, history_restore, core_unlock. הרשימה הזו (WRITE_ACTIONS) היא מה שמוגן ב-CSRF, נרשם ביומן ועובר בדיקת כתיבה.
התשובה היא תמיד JSON עם success, ובכישלון error, ולפעמים code שהממשק יודע לטפל בו: csrf, lint, conflict.
הרשאות#
הבדיקה נעשית ב-constructor של הפאנל: ADMIN::is_developer(). מי שאינו מפתח מקבל 403 (JSON עבור api ו-download, HTML עבור המסך). הבדיקה שם בכוונה ולא בשכבת ההרשאות של הפאנלים: שכבת perm_developer של הפאנלים פועלת רק כשיש לפאנל שורה בטבלה CRM_adminPanel_panels, ובלי שורה היא מדולגת וכל מנהל נכנס.
מפתח (is_developer) הוא מנהל שיש לו developer_token תקף בטבלת המנהלים. הטוקן חתום ומאומת בקוד (ADMIN::decode_developer_token), וטוקן לא תקף נמחק מהשורה. ראו התחברות וסשנים.
הסקריפט {admin}/install_file_manager (מפתחים בלבד, וחוזר על עצמו בבטחה) יוצר את טבלת היומן ורושם את הפאנל עם perm_developer = 1. אם שורת הפאנל נמצאה עם 0 הוא מתקן אותה ל-1, כי 0 חושף גישה לקבצים לכל מנהל.
ה-jail: איפה מותר לעבוד#
כל נתיב שהלקוח שולח הוא נתיב מלא, וכל גישה לדיסק עוברת דרך FileManager::resolve($abs, $mustExist = true):
- מחרוזת ריקה או null byte נדחים.
realpathמנרמל..ועוקב אחרי קישורים סימבוליים, כך שקישור שמצביע אל מחוץ לשורש נדחה לפי היעד האמיתי.- התוצאה חייבת להיות תחת אחד מה"שורשים". שורש 0 הוא תמיד
CONFIG::$base_path. - ביצירת קובץ חדש (
$mustExist = false) תיקיית האב חייבת להתקיים בתוך ה-jail, והשם חייב להיות שם פשוט בלי/או\. - קובץ רגיש (ראו להלן) נדחה.
שורשים נוספים#
הפרמטר file_manager_extra_roots בטבלה CRM_params (נתיבים מוחלטים, מופרדים בשורה חדשה או בפסיק) מוסיף שורשים. נתיב שאינו קיים או אינו תיקייה מדולג.
מצב קריאה בלבד#
file_manager_readonly = 1 הופך את כל השורשים לקריאה בלבד. כל פעולת כתיבה נכשלת, בלי תלות בהרשאות המשתמש.
קבצים רגישים#
קבצים שמסכמים סודות מוסתרים מהרשימות ונדחים בקריאה, בכתיבה ובשינוי שם. הדפוסים (SENSITIVE_PATTERNS):
.envו-.env.*config.php, וכל/includes/*config*.phpmarket_settings.json*_key.jsonו-*_keys.json.git,.ssh,id_rsa*,.htpasswd
הרשימה צרה במכוון: מפתח ממילא יכול להגיע לפרטי ה-DB דרך קובץ PHP שיכתוב, ומטרת המסכה היא להקשות על שליפת סודות של מערכות אחרות בלחיצה אחת אם הסשן של מנהל נחטף. הפרמטר file_manager_show_sensitive = 1 מכבה את המסכה (המסך capabilities מציג את המצב).
תיקיית הליבה נעולה#
תיקיית CONFIG::$core_path מנוהלת מרכזית ונדרסת בכל עדכון גרסה (ראו עדכונים), ולכן כתיבה אליה נדחית עם הסבר. הפעולה core_unlock (נשמרת ב-$_SESSION["fm_core_unlocked"], לסשן הנוכחי בלבד, לא נשמרת לצמיתות) פותחת אותה למקרי חירום. שינוי שנעשה שם ייעלם בעדכון הבא. התאמות לאתר מבצעים ב-application/.
עריכה#
read#
read מחזירה את התוכן, hash של התוכן, mtime, סימון binary ו-too_large, ורמז mode לעורך (modeFor לפי סיומת). קובץ בינארי (null byte בתחילתו) וקובץ גדול מ-MAX_EDIT_BYTES (2MB) מוחזרים בלי תוכן: אפשר להוריד אותם אבל לא לערוך.
write: שתי בדיקות לפני הדיסק#
{ "action": "write", "path": "/var/www/site/application/views/x.tpl", "content": "...", "hash": "<ה-hash מה-read>", "force": false, "csrf": "<token>" }
- התנגשות. אם קיבלתם
hashוהקובץ בדיסק השתנה מאז הקריאה, התשובה היאcode: "conflict"עם התוכן וה-hash הנוכחיים. כך שני אנשים (או אדם ותהליך FTP) לא דורסים זה את זה. - תחביר.
FileLint::checkבודקת קבציphp,phtml,scss,cssו-json. ב-PHP זהtoken_get_all, ובשרת שמאפשרexecגםphp -l; JSON עםjson_decode. שגיאה מחזירהcode: "lint"והקובץ לא נשמר.
force: true עוקף את שתיהן, וגם זה נרשם ביומן (FORCED over lint error).
השמירה עצמה כותבת קובץ זמני .fm_tmp_<hex> באותה תיקייה ואז rename, כך שקובץ חצי-כתוב לא יוגש אם הכתיבה נפסקת. הרשאות הקובץ הקיים נשמרות. אחרי כל שמירה afterWrite מנקה מטמונים רלוונטיים: קובצי scss ו-css מנקים את מטמון ה-SCSS המהודר, וקובצי tpl מנקים את מטמון Smarty. תשובת השמירה כוללת side_effects שמפרט מה נוקה. ראו צינור הנכסים.
היסטוריה#
לפני כל דריסה או מחיקה של קובץ שאינו גדול מ-2MB נשמר עותק דחוס (gz) ב-<cache_folder>/file_history/<sha1 של הנתיב>/, עם meta.json שמתעד זמן, מנהל וגודל. נשמרות 20 גרסאות אחרונות לכל קובץ (HISTORY_KEEP). history מחזירה את הרשימה, history_content את התוכן של גרסה אחת, ו-history_restore משחזרת: קודם נשמרת הגרסה הנוכחית, כך ששחזור הוא עצמו הפיך.
מחיקת תיקיית המטמון (CONFIG::$cache_folder) מוחקת גם את ההיסטוריה. היא לא גיבוי, ראו הקשחה לגיבויים אמיתיים.
פעולות על קבצים ותיקיות#
| פעולה | התנהגות |
|---|---|
create_file | path, content (ברירת מחדל ריק). נכשלת אם הקובץ קיים |
create_dir | path. תיקייה בהרשאה 0755. נכשלת אם קיימת |
rename | path, name (שם פשוט בלבד). נכשלת אם היעד קיים או שם הקובץ רגיש |
move, copy | path, dest (תיקייה). נכשלת אם הקובץ קיים ביעד; הזזת תיקייה לתוך עצמה נחסמת. העתקת תיקייה רקורסיבית |
delete | קובץ: נשמרת גרסה אחרונה ואז נמחק. תיקייה: נמחקת רקורסיבית בלי היסטוריה. מחיקת שורש נחסמת |
upload | dest (תיקייה, ב-$_POST) וקובץ בשדה file. שם הקובץ הוא basename של השם שנשלח. קובץ קיים נשמר בהיסטוריה לפני הדריסה. השמירה עם move_uploaded_file |
העלאה בפאנל הזה לא מסננת סיומות (זה כלי למפתחים). לא משתמשים בה להעלאת תוכן ציבורי; לשם כך אבטחת העלאות.
חיפוש:
find_files(root,query,limitברירת מחדל 200): חיפוש שם קובץ מטושטש (fuzzy), עם דילוג עלnode_modules,.git,cache,.wz_history,vendor.search_content(root,query,limit300,ignore_case,exts): חיפוש בתוכן. כש-execו-grepזמינים (נבדק בפועל מול קובץ ניסיון, לא רק לפיgrep --version) משתמש ב-grep, אחרת סורק ב-PHP. התשובה מציינתengineו-truncated.
CSRF#
פעולות הכתיבה דורשות אסימון CSRF בשיטת double-submit cookie. האסימון נשלח ב-csrf ב-JSON, ב-$_POST["csrf"] או בכותרת X-WZ-CSRF, ומושווה (hash_equals) לעוגיית admin_csrf. הפעולה capabilities יוצרת את העוגייה בקריאה הראשונה ומחזירה את האסימון, ובקריאות הבאות מחזירה את הקיים. העוגייה חיה שעה, ולכן עורך שנשאר פתוח יכול לקבל code: "csrf": הממשק (app.js) מבקש אסימון חדש ומשחזר את השמירה במקום לגרום לאיבוד טיוטה. הפרטים הכלליים בCSRF, XSS ו-SQL injection.
יומן ביקורת#
כל פעולת כתיבה, וגם הורדה, נרשמת ב-CRM_file_manager_log:
| עמודה | משמעות |
|---|---|
id | |
admin_id | המנהל שביצע |
action | write, create_file, create_dir, rename, move, copy, delete, upload, history_restore, core_unlock, download |
path | הנתיב (נחתך ל-900 תווים) |
bytes | גודל |
note | הערה (נחתך ל-250), למשל FORCED over lint error |
ip | REMOTE_ADDR |
user_agent | נחתך ל-250 |
created | זמן |
הרישום לא שובר פעולה: כישלון כתיבה ליומן נבלע. לכן אם יומן ביקורת חשוב לכם, בדקו מדי פעם שיש בו שורות.
SELECT created, admin_id, action, path
FROM CRM_file_manager_log
ORDER BY id DESC
LIMIT 50;
כותרות בהורדה#
download שולחת Content-Disposition: attachment, Cache-Control: private, no-store ו-X-Content-Type-Options: nosniff. זו כותרת האבטחה היחידה שהליבה שולחת בכל המערכת; כותרות נוספות מוגדרות בשרת (ראו הקשחה).
הגדרות מהירות#
פרמטר ב-CRM_params | משמעות |
|---|---|
file_manager_readonly | 1 = כל הפאנל לקריאה בלבד |
file_manager_extra_roots | שורשים נוספים מעבר ל-base_path |
file_manager_show_sensitive | 1 = ביטול המסכה של קבצים רגישים |
ערכי הפרמטרים נערכים ב-{admin}/Params (ראו פרמטרים ושפות). להשבתה מוחלטת של הפאנל במערכת קבועה, אפשר להגדיר file_manager_readonly = 1, או להסיר את הרשאת המפתח מכל המנהלים.