ADMIN (רפרנס)

כל המתודות הציבוריות של המחלקה ADMIN - סשנים של מנהלים, התחברות ובדיקת פרטים, הרשאות, התחברות כמנהל אחר, CSRF ואימות טוקן מפתח של WIZZO ID.

⏱ 13 דק' קריאה 2258 מילים ערוך דף זה ב-GitHub

המחלקה ADMIN (system/collections/ADMIN.php) היא המחלקה הסטטית שמחליטה מי המנהל המחובר ומה מותר לו. היא מנהלת סשנים בטבלה CRM_adminPanel_sessions, בודקת סיסמאות מול CRM_adminPanel_admins, מגבילה ניסיונות כושלים, ומחשבת הרשאות לפי קבוצת המנהל. העמוד הזה מתעד כל מתודה ציבורית עם החתימה המדויקת מהקוד; ההסבר הרחב על זרימת ההתחברות נמצא ב-התחברות, סשנים ו-WIZZO ID ועל מודל ההרשאות ב-הרשאות וקבוצות.

system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו), ו-{admin} הוא CONFIG::$admin_url.

הערה

שמות העוגיות בדפדפן מקבלים את הקידומת CRM_ (COOKIES מוסיפה אותה בכל קריאה), כך שעוגיית הסשן היא בפועל CRM_admin_session. בקוד תמיד כותבים את השם בלי הקידומת: COOKIES::get("admin_session").

מאפיינים וקבועים#

שםערךהערות
ADMIN::$login_use_ipfalseרלוונטי רק לעוגיית admin_token הישנה: כש-false לא נבדקת כתובת ה-IP שנשמרה בעוגייה
ADMIN::WIZZO_ID_DEVELOPER_PUBLIC_KEYקבוע מחרוזתהמפתח הציבורי (RSA) שמול חתימת טוקן המפתח מאומתת. החלפתו היא שחרור ליבה
סשן (פנימי, private)admin_session, 168 שעותהטוקן הוא bin2hex(random_bytes(32)) (64 תווי hex), נשמר בטבלה ולא בעוגייה בלבד

סשנים#

הסשן של מנהל הוא שורה ב-CRM_adminPanel_sessions (עמודות: id, admin_id, token, ip, user_agent, device_label, created_at, last_active_at, expires_at, is_impersonated, impersonated_by). העוגייה admin_session (httpOnly, מאובטחת, 7 ימים) מחזיקה רק את הטוקן. לצידה נקבעת עוגיית admin_logged עם הערך 1, שה-JavaScript יכול לקרוא, ושאינה סודית: היא רק דגל שאומר "יש סשן".

ADMIN::ensure_sessions_table()#

public static function ensure_sessions_table()

יוצרת את CRM_adminPanel_sessions אם היא לא קיימת, ומוסיפה לטבלת המנהלים את העמודות auth_provider (ברירת מחדל local) ו-external_id אם חסרות. רצה פעם אחת לכל בקשה (משתנה static), וכל מתודת סשן אחרת קוראת לה בעצמה. כל חריגה נבלעת בשקט. אין ערך החזרה.

ADMIN::get_id()#

public static function get_id()

מחזירה את ה-id של המנהל המחובר כמספר שלם, או false אם אין סשן תקף. התוצאה נשמרת לכל הבקשה (גם התוצאה השלילית).

  1. מחפשת את הטוקן מהעוגייה admin_session בטבלה, עם expires_at > NOW().
  2. אם נמצא סשן: מרעננת last_active_at ו-expires_at ומחדשת את העוגייה, אבל לכל היותר פעם ב-5 דקות.
  3. אם אין סשן חדש אבל יש עוגיית admin_token ישנה (מקודדת ב-MISC::encode): מאמתת אותה, מצמידה אליה סשן קיים של אותו מנהל או יוצרת חדש (admin_log), ומוחקת את העוגייה הישנה.
$adminId = ADMIN::get_id();
if (!$adminId) { /* אין מנהל מחובר */ }

ADMIN::is_admin()#

public static function is_admin()

מחזירה true אם יש סשן תקף וגם שורת המנהל עדיין קיימת ב-CRM_adminPanel_admins. מנהל שנמחק מהטבלה מפסיק להיות מנהל גם אם הסשן שלו עוד חי. זה הכלי הנכון לשער של קוד שמיועד למנהלים בלבד.

if (!ADMIN::is_admin()) { header("HTTP/1.1 403 Forbidden"); exit; }

ADMIN::admin_log($adminID)#

public static function admin_log($adminID)

יוצרת סשן חדש למנהל: מחוללת טוקן, כותבת שורה (כולל ip, user_agent ותווית מכשיר כמו Chrome on Windows), קובעת את עוגיית admin_logged ואת עוגיית admin_session, ומחזירה את תוצאת setcookie. המתודה לא בודקת סיסמה: היא נקראת רק אחרי שהאימות הצליח (ראו check_details). הרשאה לקרוא לה היא הרשאה להפוך לכל מנהל.

ADMIN::admin_logout()#

public static function admin_logout()

מוחקת את שורת הסשן הנוכחי מהטבלה ואת העוגיות admin_session, admin_logged ו-admin_token, ומאפסת את המטמון של הבקשה. אין ערך החזרה.

ADMIN::get_current_session_id()#

public static function get_current_session_id()

מחזירה את id של שורת הסשן הנוכחית (int), או 0. חשוב: הערך מלא רק אחרי שנקראה get_id() בבקשה הזו.

ADMIN::get_active_sessions($adminID)#

public static function get_active_sessions($adminID)

מחזירה מערך של הסשנים הפעילים של מנהל (expires_at > NOW()), מהאחרון שהיה פעיל. העמודות: id, ip, device_label, created_at, last_active_at, is_impersonated, impersonated_by. הטוקן עצמו לא מוחזר. משמשת את מסך הסשנים של מנהל בפאנל Admins.

ADMIN::revoke_session($sessionID, $adminID)#

public static function revoke_session($sessionID, $adminID)

מוחקת סשן אחד. התנאי כולל גם admin_id, כך שאי אפשר למחוק סשן של מנהל אחר. אין ערך החזרה.

ADMIN::logout_all($adminID)#

public static function logout_all($adminID)

מוחקת את כל הסשנים של המנהל, כולל הנוכחי.

ADMIN::invalidate_sessions_on_password_change($adminID)#

public static function invalidate_sessions_on_password_change($adminID)

מוחקת את כל הסשנים של המנהל חוץ מהנוכחי. אם אין סשן נוכחי (למשל איפוס סיסמה מחוץ לסשן) היא מתנהגת כמו logout_all. קראו לה מיד אחרי שינוי סיסמה.

ADMIN::cleanup_expired_sessions()#

public static function cleanup_expired_sessions()

מוחקת סשנים שפג תוקפם ושורות ב-CRM_adminPanel_admins_bad_attempts ישנות מ-24 שעות. נקראת מהקונטרולר admin_session_cleanup. אין ערך החזרה.

התחברות ובדיקת פרטים#

ADMIN::check_details($username, $password, $phone = false, $record_bad_attempt = true)#

public static function check_details ($username, $password, $phone = false, $record_bad_attempt = true)
פרמטרמשמעות
$usernameשם משתמש; עובר strtolower(trim()) ומושווה לעמודה uname
$passwordסיסמה; עוברת trim() ומאומתת עם Bcrypt::verify מול password
$phoneקוד OTP. אם הועבר ערך, הוא מאומת מול phone_last_code (גם הוא Bcrypt) בנוסף לסיסמה
$record_bad_attemptהאם לרשום ניסיון כושל ב-CRM_adminPanel_admins_bad_attempts ולבדוק מגבלת IP

ערכי החזרה, ושלושתם שונים:

ערךמתי
trueהסיסמה (והקוד, אם נדרש) נכונים וגם ה-IP מותר. מחדשת את עוגיית הסשן אם קיימת
falseמשתמש לא קיים, סיסמה שגויה, IP לא ברשימת allow_ip של המנהל
"to_many_ettempts" (מחרוזת, כך באיות בקוד)5 ניסיונות כושלים או יותר לאותו משתמש ב-10 הדקות האחרונות, או 15 ניסיונות מאותו IP (רק כש-$record_bad_attempt פעיל)

פרטים נוספים מהקוד:

  • allow_ip שמור כרשימה בפורמט bgl_implode של כתובות מקודדות ב-MISC::encode; הבדיקה משווה את MISC::encode($_SERVER['REMOTE_ADDR']) לרשימה.
  • מגבלת הניסיונות נבדקת רק למשתמש שקיים. שם משתמש לא קיים לא נרשם ולא נחסם.
  • ניסיון כושל נרשם עם uname, ip ו-password, כשהעמודה האחרונה מכילה את ה-hash של הסיסמה האמיתית של המנהל אחרי MISC::encode. אל תחשפו את הטבלה הזו.
שימו לב

השוו תמיד עם === true, כפי שעושים הקונטרולרים (controllers/admin_login.php). המחרוזת "to_many_ettempts" היא truthy, ולכן if (ADMIN::check_details(...)) מזהה חסימה בגלל ניסיונות כושלים כהצלחה.

// כך נראית ההתחברות בקונטרולר הכניסה, בקיצור
$ret = ADMIN::check_details($_POST["username"], $_POST["password"]);

if ($ret === true)
{
    $user = DB::get_val("adminPanel_admins", ["uname" => $_POST["username"]]);
    ADMIN::admin_log($user["id"]);
}
elseif ($ret === "to_many_ettempts")
{
    // להציג "נסו שוב בעוד כמה דקות"
}

ADMIN::login($username, $password)#

public static function login ($username, $password)

קיצור: קוראת ל-check_details ואז ל-admin_log עבור המשתמש שנמצא. מחזירה את תוצאת setcookie בהצלחה או false. אף קונטרולר בליבה לא משתמש בה (הם קוראים ל-check_details ו-admin_log בנפרד, כי יש להם שלב OTP).

אזהרה
login() בודקת את תוצאת check_details בתנאי רפוי (if (self::check_details(...))). כשהחשבון חסום בגלל ניסיונות כושלים, check_details מחזירה את המחרוזת "to_many_ettempts", והתנאי עובר: login() יוצרת סשן עבור המשתמש בלי שהסיסמה אומתה. כמו כן היא מחפשת את המשתמש לפי $username כפי שהועבר, בלי strtolower(trim()). אל תשתמשו בה בקוד חדש; השתמשו ב-check_details(...) === true ואז ב-admin_log.

קבוצות והרשאות#

ADMIN::GetAdminGroup()#

public static function GetAdminGroup()

מחזירה את group_id של המנהל המחובר, או false בלי סשן. התוצאה נשמרת ב-MISC::$GLOBALS["AdminGroupID"] לכל הבקשה.

ADMIN::has_perms($perm = "", $curr_page = false, $curr_action = false)#

public static function has_perms($perm = "", $curr_page = false, $curr_action = false)
פרמטרמשמעות
$permהרשאה ספציפית ("edit", "delete", "export", "publish", או כל מפתח מ-extra_perms של הפאנל). מחרוזת ריקה בודקת גישה בלבד
$curr_pagepanel_name של הפאנל. ברירת מחדל false = $_GET["module"]
$curr_actionשם הפעולה. ברירת מחדל false = $_GET["pname"]

מחזירה bool. הרשאות הקבוצה נקראות מ-CRM_adminPanel_admins_groups.perms ונשמרות ב-MISC::$GLOBALS["admin_perms"]. סדר ההחלטות:

מצבתוצאה
אין מנהל מחובר או אין קבוצהfalse
perms של הקבוצה הוא "-1"true לכל דבר
הפאנל לא קיים ב-adminPanel_panelstrue אם לקבוצה יש גישה לפאנל wizzo_market; אחרת נבדקת רשימת market_panels (בלי $perm מספיק שהפאנל ברשימה; עם $perm נבדק !empty($mp[$perm])); אחרת false
הפאנל לא ברשימת הקבוצהfalse
הפעולה ריקה או indexבלי $perm: true. עם $perm: $found_page[$perm] === true
פעולה שאין לה שורה ב-adminPanel_panels_optionstrue (גם אם $perm הועבר)
פעולה עם שורהחייבת להופיע ב-subs של הפאנל; עם $perm נבדק !empty($sub[$perm])

צורת ה-JSON של perms (כפי ש-Admins/save_group שומר אותה):

[{"id": 12, "checked": true, "edit": true, "delete": false, "export": true, "publish": true,
  "subs": [{"id": 40, "checked": true, "edit": true, "delete": false}],
  "market_panels": [{"panel_name": "services_poosh_stats", "send": true}]}]

id הוא adminPanel_panels.id, ו-subs[].id הוא adminPanel_panels_options.id.

// בתוך פאנל: להציג כפתור מחיקה רק למי שמורשה
if (ADMIN::has_perms("delete")) { /* ... */ }

// בדיקה מפורשת של פאנל אחר
$canEditNews = ADMIN::has_perms("edit", "news", "index");
שימו לב

פעולה (pname) שאין לה שורה ב-adminPanel_panels_options מקבלת true בלי קשר להרשאות, כי הקוד מניח שאלה דפי insert/edit. אם יש לכם פעולה רגישה, ודאו שנרשמה ל-panel_options, או בדקו has_perms("edit") בתוכה במפורש.

ADMIN::is_developer()#

static function is_developer()

מחזירה true אם המנהל המחובר הוא מפתח של Wizzo, לפי העמודה developer_token שנשמרה בשורת המנהל ביום ההתחברות דרך WIZZO ID. התשובה מחושבת פעם אחת לכל בקשה (תפריט הניהול שואל עשרות פעמים). הלוגיקה:

  1. אין סשן מנהל: false (לא נשמר במטמון, כי הבקשה עוד עלולה להתחבר).
  2. אין טוקן: false.
  3. טוקן עם חתימה תקפה (decode_developer_token) ושדה time שעוד לא עבר: true, בלי בקשת רשת.
  4. טוקן שפג או שאינו חתום במפתח הנוכחי: פנייה אחת ל-WIZZO ID (wizzo_auth::renew_developer_token) שמחליפה את הטוקן או מוחקת אותו. טוקן שאפילו אינו נפרס כטוקן נמחק מיד.

פאנל עם perm_developer = 1 מוצג רק כש-is_developer() מחזירה true.

ADMIN::decode_developer_token($encoded)#

public static function decode_developer_token($encoded)

מקבלת את הטוקן השמור (MISC::encode של JSON עם user_id, time, sig, pk_sig) ומחזירה את האובייקט המפוענח אם החתימה pk_sig (RSA-SHA256 על המחרוזת dev|user_id|time) מאומתת מול ADMIN::WIZZO_ID_DEVELOPER_PUBLIC_KEY עם openssl_verify. בכל כשל, כולל חסר user_id, חסר pk_sig, חתימה של מפתח אחר, או חוסר בהרחבת openssl (נכשלת סגורה ורושמת ל-error_log), היא מחזירה false. האימות מקומי, ללא קריאת HTTP.

הערה

מפתח ההצפנה של MISC::encode נמצא בכל ליבה, ולכן כל אחד יכול לפענח טוקן. החתימה היא מה שמונע זיוף. בדקו מפתח באמצעות decode_developer_token או is_developer, ולא באמצעות MISC::decode בלבד.

רב-לשוניות באדמין#

ADMIN::init_multilang()#

public static function init_multilang()

טוענת את כל הפלטפורמות (CRM_platforms, לפי ord) ל-MISC::$GLOBALS["langs"], מכבדת את $_GET["lang"] (שומרת בעוגיית admin_lang, מקודדת), ובוחרת את שפת הניהול: MISC::$GLOBALS["admin_lang_data"] ו-MISC::$GLOBALS["admin_lang_id"]. ברירת המחדל היא הפלטפורמה הראשונה. בסוף היא מעדכנת את last_login של המנהל ל-NOW(). נקראת מ-PAGE בטעינת עמוד ניהול (PAGE.php); אין סיבה לקרוא לה ידנית. אין ערך החזרה.

הערה
last_login מתעדכן בכל טעינת עמוד ניהול, לא רק בהתחברות, ולכן הוא מתאר "פעילות אחרונה".

התחברות כמנהל אחר#

ADMIN::create_login_as_token($targetAdminId, $requesterId)#

public static function create_login_as_token($targetAdminId, $requesterId)

מחזירה מחרוזת MISC::encode של {target_id, requester_id, time, rand}. הכפתור "התחבר כמנהל" ב-Admins יוצר אותה, ורק כש-ADMIN::is_developer() ועבור מנהל שאינו המחובר.

ADMIN::validate_login_as_token($token)#

public static function validate_login_as_token($token)

מפענחת את הטוקן ומחזירה את המערך (target_id, requester_id, time, rand) אם הוא תקין ולא עברו מעל 60 שניות מיצירתו; אחרת false.

ADMIN::admin_log_impersonate($targetAdminId, $requesterId = null)#

public static function admin_log_impersonate($targetAdminId, $requesterId = null)

יוצרת סשן עבור $targetAdminId עם is_impersonated = 1, impersonated_by = $requesterId ותווית מכשיר שמסתיימת ב-(impersonated). מחזירה false אם המנהל לא קיים; אחרת את תוצאת setcookie. הקונטרולר admin_login::login_as קורא לה ואז מעביר לכתובת הניהול.

שימו לב
login_as בודק רק שלשורת המבקש יש developer_token לא ריק, ולא קורא ל-is_developer(). הטוקן עצמו הוא MISC::encode ללא חתימה. אל תרחיבו את השימוש בו ואל תחשפו כתובת login_as?token=... ביומנים.

הגנת CSRF#

תבנית double-submit: עוגייה אקראית admin_csrf (httpOnly, שעה אחת) שהלקוח חייב להחזיר גם כפרמטר בבקשה.

public static function generate_csrf_cookie()

מחזירה את הערך של admin_csrf אם קיים, ואחרת מחוללת טוקן חדש (64 תווי hex), קובעת אותו בעוגייה (שעה) ומחזירה אותו. את הערך שמים בשדה נסתר של הטופס, או מוסרים ללקוח כדי שישלח אותו בכותרת X-WZ-CSRF (כך עושה file_manager).

ADMIN::verify_csrf_token($posted_token)#

public static function verify_csrf_token($posted_token)

מחזירה true רק אם גם העוגייה וגם הערך שנשלח אינם ריקים והם שווים (hash_equals).

// בעמוד
$csrf = ADMIN::generate_csrf_cookie();

// בטיפול בבקשה (כפי שעושה file_manager לפעולות כתיבה)
$token = $_POST["csrf"] ?? $_SERVER["HTTP_X_WZ_CSRF"] ?? "";
if (!ADMIN::verify_csrf_token($token)) { /* לדחות */ }
שימו לב
controllers/admin_login.php בודק CSRF רק כש-$_POST["csrf_token"] לא ריק (!empty(...) && !verify(...)), ולכן בקשה בלי השדה עוברת. בפאנלים חדשים כתבו if (!ADMIN::verify_csrf_token($token)) die(...) ללא התנאי הראשון. העוגייה חיה שעה, ולכן טאב עריכה פתוח ארוך עלול לקבל כישלון תקין: טפלו בו בשליחה חוזרת עם טוקן חדש.

עזרים#

ADMIN::icon_html($icon)#

public static function icon_html($icon)

מנקה אייקון של פאנל לפני הצגה בתפריט. שדה icon מכיל HTML גולמי, ואייקונים שיוצאים מכלי עיצוב מגיעים עם <svg width="100%" height="auto">. הערך auto אינו חוקי כמאפיין SVG, והדפדפן רושם שגיאה בקונסולה. הפונקציה מוחקת מכל תגית <svg ...> את המאפיינים width ו-height שערכם auto, ומחזירה את שאר ה-HTML כפי שהוא (מחרוזת). קלט שאינו SVG (תג <i> של FontAwesome, אימוג'י, מחרוזת ריקה) מוחזר ללא שינוי.

echo ADMIN::icon_html('<svg width="100%" height="auto" viewBox="0 0 24 24"></svg>');
// <svg width="100%" viewBox="0 0 24 24"></svg>

ראו גם#

מצאתם טעות או חוסר? תקנו את הדף או פתחו Issue בריפו. התיעוד נכתב מתוך הקוד של ליבה 5.0.115.