יומן ביקורת

איך WIZZO CMS רושם מי עשה מה בניהול, אילו פעולות נכנסות ליומן ואיך הערכים הרגישים נשמרים, איך מנקים אותו, ואיך קוד של אתר כותב אליו שורה.

⏱ 10 דק' קריאה 1827 מילים

יומן הביקורת עונה על השאלה "מי שינה את זה, מתי ומה היה שם קודם". כל שינוי תוכן בניהול, כל כניסה ויציאה, כל שינוי של הרשאות או הגדרות, כל ייצוא וייבוא של Excel וכל עדכון ליבה כותבים שורה אחת בטבלה CRM_audit_log, בצורה שאדם קורא ("עדכן את 'אודות' (כותרת, טקסט)") ובצורה שמכונה קוראת ({שדה: [לפני, אחרי]}). המחלקה היא AUDIT. היומן נשמר באתר עצמו ולא נשלח לשום מקום.

בעמוד הזה system/ הוא תיקיית הליבה הפרוסה (api/core בקוד המקור), ו-{admin} הוא CONFIG::$admin_url של האתר. היומן זמין מליבה 5.0.140, עם תיקונים ב-5.0.142 (בדקו ב-system/core/version.txt).

מסך הניהול מגיע בגרסה מאוחרת

הכתיבה ליומן והפאנל audit_log עם ה-JSON שלו (רשימה, פרטי שורה, סינון, ייצוא וניקוי) קיימים. הפאנל עדיין לא רשום ב-CRM_adminPanel_panels, ולכן אין לו כניסה בתפריט והרשאה לקבוצה. מסך הצפייה עצמו יגיע בגרסה מאוחרת יותר, ועד אז הפאנל נגיש לקבוצה שיש לה את כל ההרשאות. בדף הבית של הניהול כבר מוצגות ארבע העריכות האחרונות מתוך היומן.

מה נכנס ליומן#

מה קרהactionמי כותב
רשומה נוצרהcreateהמאזין audit של שכבת אירועי התוכן, מכל דלת כתיבה
רשומה עודכנהupdateכנ"ל. שמירה שלא שינתה כלום לא נרשמת
רשומה נמחקהdeleteכנ"ל
רשומה שוחזרהrestoreכנ"ל, ראו גרסאות ושחזור
שינוי סטטוס פרסוםstatusכנ"ל
רשומה שנוצרה בייבואimportכנ"ל, וגם שורה אחת לכל הרצה של ייבוא Excel
פעולת שורה או קבוצה בפאנלactionכנ"ל. הפעולה היא קופסה שחורה, ולכן השורה כוללת סיכום בלבד, בלי changes
שינוי של הגדרה (טבלת params)settingsהמאזין audit
שינוי של קבוצת הרשאות, או העברת מנהל לקבוצה אחרתpermsהמאזין audit
כניסה לניהולloginמסך ההתחברות. הסיכום אומר באיזו דרך (סיסמה, WIZZO ID, קוד SMS)
ניסיון כניסה שנכשלlogin_failedמסך ההתחברות
יציאהlogoutמסך ההתחברות
כניסה בשם מנהל אחרloginreal_admin_id הוא המנהל שבפועל פעל, והסיכום נכתב "התחבר בשם ..."
ייצוא Excel מרשימה, או מיומן הביקורתexportpanel_table ופאנל audit_log. נרשם כמה שורות יצאו ובאיזה סינון, אף פעם לא השורות עצמן
עדכון גרסת ליבהupdate_coreמסך עדכון הגרסה, עם גרסת המקור והיעד

הרשימה היא הקבוע AUDIT::ACTIONS. ערך אחר ב-AUDIT::log() נדחה.

מה לא נכנס: שינוי שעוקף את הדלתות (ראו "מה השכבה לא רואה" ב-שכבת אירועי התוכן), פעולות באתר הציבורי (כניסת משתמש אתר, טופס יצירת קשר), וקריאות. היומן הוא יומן של הניהול.

טבלת audit_log עצמה#

שינוי בטבלה audit_log לא נכתב ליומן (אחרת כל ייצוא היה מוליד שורה על שורה).

CRM_audit_log#

עמודהטיפוסמשמעות
idbigint, PKמספר רץ
created_atdatetimeמתי
admin_idintהמנהל שפעל. 0 כשאין מנהל (ניסיון כניסה שהמשתמש בו לא זוהה)
admin_namevarchar(100)השם של המנהל ברגע האירוע, כך שגם אחרי מחיקת המנהל היומן נשאר קריא
real_admin_idintבהתחזות: המנהל שפעל באמת. אחרת 0
ipvarchar(45)כתובת ה-IP
actionvarchar(24)הפעולה, מהטבלה למעלה
tblvarchar(64)הטבלה בלי CRM_ (או null)
row_idintהרשומה (או null)
lang_idintהשפה (או null)
panelvarchar(64)הפאנל שבו נעשה השינוי
sourcevarchar(16)הדלת: form, inline, mcp, import, login, export, update, code...
summaryvarchar(255)השורה שאדם קורא
changesmediumtextJSON של {שדה: [לפני, אחרי]}, או null
request_idchar(36)מזהה הבקשה. שורות של אותה בקשה (שמירה על שתי שפות) חולקות אותו

אינדקסים: (tbl, row_id) להיסטוריה של רשומה, (admin_id, created_at) לפעילות של מנהל, created_at ו-(action, created_at) לסינון. הטבלה מגיעה לאתר ב-update_db של עדכון הגרסה (עדכון ליבה). באתר שעוד לא קיבל אותה AUDIT::log() לא כותבת כלום ולא שוברת כלום.

איך נכתבת שורה#

סיכום#

הסיכום הוא משפט קצר בעברית, בלי שם המנהל (הוא בעמודה נפרדת): "יצר את 'אודות'", "שינה את ההגדרה 'site_name' (ערך)", "העביר את המנהל 'דנה' לקבוצה 'עורכים'", "ייצא ל-Excel 120 שורות מהרשימה 'לקוחות'". הכותרת של הרשומה נלקחת מהעמודה title_field של סוג התוכן (שכבת אירועי התוכן), ואם אין אחת, מהראשונה שקיימת מ-title, name, subject, uname, label, question, email. עמודה רגישה לא משמשת ככותרת. אם אין כותרת הסיכום אומר "רשומה 12 ב-events". הכותרת נחתכת ל-60 תווים, ורשימת השדות ששונו כוללת עד שישה ואחריהם "ועוד N". כל סיכום מנוקה מתווי בקרה ומירידות שורה, ונחתך ל-255 תווים.

השינויים#

changes נבנה מהרשומה לפני ואחרי:

פעולהמה ב-changes
createכל שדה שקיבל ערך: [null, ערך]
updateרק השדות ששונו: [לפני, אחרי]
deleteכל מה שהרשומה החזיקה: [ערך, null]. בכמה שפות השדה מופיע כ-title[2]
restore של רשומה שנמחקהכמו יצירה
actionאין

כללי השמירה:

  • ערך רגיש (סיסמה, טוקן, סוד, ערך של הגדרה סודית) מגיע מוסתר מהאירוע, וביומן כתוב "***". אבל העובדה שהשדה שונה כן נרשמת: אפשר לראות שסיסמה הוחלפה, לא מה היא.
  • ערך מעל 64KB לא נשמר: נשמרים רק hash (sha1), len ו-head (200 התווים הראשונים). כך אפשר לדעת ששדה גדול השתנה ולהשוות בלי לנפח את הטבלה.
  • שורה שלמה מעל 1MB נחתכת: כל ערך מעל 2000 תווים הופך ל-hash, אורך ופתיח.
  • שמירות אוטומטיות של אותו מנהל על אותה רשומה ושפה, בתוך 30 דקות, מתמזגות לשורה אחת: ה"לפני" הראשון נשאר וה"אחרי" האחרון מנצח. שדה שחזר לערכו המקורי יוצא מהשורה.

שמירה וניקוי#

השורות נשמרות לפי הפרמטר audit_keep_days, ברירת מחדל 365 ימים. אין לו שורה מוכנה: כדי לשנות הוסיפו שורה במסך Params (פרמטרים ושפות) עם sysName audit_keep_days.

הניקוי לא דורש משימת cron. בכתיבה הראשונה של כל יום AUDIT::cleanup() רצה פעם אחת: היא מוחקת בקבוצות של 5000 שורות (עד 20 קבוצות בריצה) כל מה שישן מהמגבלה. קובץ audit_cleanup.stamp בתיקיית ה-cache מונע ריצה שנייה באותו יום. אפשר גם להפעיל ניקוי ידני דרך הפאנל, ראו למטה.

ה-API של פאנל audit_log#

כל הקריאות ל-{admin}/audit_log/<method>?pmode=empg מחזירות JSON, ודורשות מנהל מחובר שיש לו הרשאה לפאנל audit_log (כמתואר למעלה, כרגע קבוצה עם כל ההרשאות). בלי הרשאה: {"ok": false, "error": "not allowed"}. ראו JSON API של הניהול על pmode.

שיטהקלט (GET)מחזיר
indexready, keep_days ורשימת השיטות
listמסננים (למטה), page, limit (ברירת מחדל 50, עד 200)total, page, pages, rows (החדש ראשון, לפי id)
detailidrow עם changes מפוענח, request_id ו-same_request (מזהי שורות אחרות של אותה בקשה, עד 50)
filtersadmins, actions, tables שאפשר לסנן לפיהם, עם תוויות
exportאותם מסנניםקובץ audit-log-<תאריך>.xlsx (עד 50,000 שורות, בלי changes)
cleanupdeleted, keep_days: מוחק עכשיו מה שישן מהמגבלה

מסננים של list ו-export:

פרמטרמשמעות
admin_idמנהל
actionפעולה אחת או כמה, מופרדות בפסיק: create,update
tblטבלה
row_idרשומה
date_from, date_toטווח תאריכים כולל (כל פורמט ש-strtotime מבין)
qחיפוש בטקסט הסיכום

כל שורה בתשובה היא id, created_at, admin_id, admin_name, real_admin_id, real_admin_name, ip, action, action_label, tbl, tbl_label, row_id, lang_id, panel, source, summary ו-has_changes. tbl_label הוא ה-label של סוג התוכן, או שם בעברית לטבלאות הליבה (הגדרות, מנהלים, קבוצות מנהלים).

curl -s -b cookies.txt \
  "https://example.com{admin}/audit_log/list?pmode=empg&action=delete,perms&date_from=2026-10-01&limit=20"
הערכים הם טקסט שהמשתמשים הקלידו

כותרת של רשומה, או שם משתמש שהוקלד בניסיון כניסה, נכנסים לסיכום כפי שהם. כשאתם מציגים שורה בממשק משלכם, הציגו אותה כטקסט ולא כ-HTML. הייצוא ל-Excel כבר כותב כל תא כטקסט, כך שערך שמתחיל ב-= לא יהפוך לנוסחה.

ייצוא היומן עצמו נרשם ביומן כפעולת export, כולל הסינון שהופעל.

כתיבת שורה מקוד של אתר#

יש שתי דרכים, לפי מה שרוצים לתעד.

שינוי של רשומה בטבלה. שלחו אירוע דרך שכבת אירועי התוכן (CONTENT::snapshot ו-CONTENT::emit). המאזין audit ימלא לבד פעולה, סיכום ו-changes, והגרסאות יראו אותו גם הן. זו הדרך הנכונה לכל כתיבה שעוקפת טופס.

פעולה שאינה רשומה (שליחת מייל מהניהול, איפוס של סוג מטמון, הפעלת תהליך): AUDIT::log() ישירות.

class events_admin extends wz_controller
{
	public function send_reminders()
	{
		if (!ADMIN::is_admin()) return;

		$sent = $this->send_all();   // הלוגיקה של האתר

		AUDIT::log([
			"action"  => "action",
			"tbl"     => "events",
			"panel"   => "Events",
			"source"  => "code",
			"summary" => "שלח " . $sent . " תזכורות לנרשמים לאירוע",
			"changes" => ["sent" => [null, $sent]],
		]);
	}
}

רק action חובה, וחייבת להיות אחת מ-AUDIT::ACTIONS. מנהל, real_admin_id, IP, פאנל ו-request_id מושלמים לבד מהבקשה. הסיכום הוא משפט שאדם יבין בלי להסתכל ב-changes, ולכן שימו בו מה נעשה ועל מה, ולא "בוצע". אל תשימו ב-changes ערכים רגישים: הקריאה הישירה לא מסתירה כלום, כי היא לא יודעת מה רגיש. העבירו רק מספרים וטקסט שמותר לראות.

מתודות של AUDIT#

כל המתודות סטטיות. אף אחת לא זורקת חריגה: כשל נכתב ליומן השגיאות של PHP (AUDIT <where> failed: ...) והמתודה מחזירה false או 0.

AUDIT::log()#

AUDIT::log(array $row): int|false

כותבת שורה ומחזירה את ה-id שלה, או false כשהפעולה לא תקינה או שאין טבלה. שדות: action (חובה), summary, tbl, row_id, lang_id, panel, source, changes (מערך או JSON), admin_id, admin_name, real_admin_id, ip, request_id. כל מה שלא ניתן מושלם מהבקשה.

AUDIT::login(), AUDIT::login_failed() ו-AUDIT::logout()#

AUDIT::login(int $admin_id, string $how = "", int $real_admin_id = 0): int|false
AUDIT::login_failed(string $uname, string $reason = "", int $admin_id = 0): int|false
AUDIT::logout(): int|false

שורות כניסה ויציאה. מסך ההתחברות של הליבה קורא להן, ולכן רק מי שבנה מסך כניסה משלו לניהול צריך לקרוא להן. login_failed מקבלת את מה שהוקלד בשדה שם המשתמש בלבד: את הסיסמה לא מעבירים אליה לעולם. הטקסט מנוקה מתווי בקרה ונחתך ל-100 תווים.

AUDIT::cleanup() ו-AUDIT::keep_days()#

AUDIT::cleanup(): int
AUDIT::keep_days(): int

cleanup() מוחקת שורות ישנות מ-keep_days() ומחזירה כמה נמחקו. keep_days() מחזירה את הפרמטר audit_keep_days או 365.

AUDIT::ready()#

AUDIT::ready(bool $refresh = false): bool

האם CRM_audit_log קיימת. מתבצעת שאילתה אחת בבקשה, ו-$refresh מכריחה בדיקה מחדש (למשל אחרי update_db באותה בקשה).

AUDIT::admin_name() ו-AUDIT::clean()#

AUDIT::admin_name(int $admin_id): string
AUDIT::clean(string $s, int $max): string

admin_name() מחזירה את השם של מנהל (שם מלא, ובהיעדרו שם משתמש). clean() הופכת טקסט לשורה אחת: תווי בקרה ושבירות שורה הופכים לרווח, ומה שעובר את $max נחתך עם "…". השתמשו בה לכל טקסט שהמשתמש הקליד לפני שהוא נכנס לסיכום.

AUDIT::action_for(), AUDIT::summary_for() ו-AUDIT::changes_for()#

AUDIT::action_for(array $event): string
AUDIT::summary_for(array $event, string $action, array $fields = []): string
AUDIT::changes_for(array $event): array

הלוגיקה של המאזין, חשופה לבדיקה ולשימוש חוזר: איזו פעולה האירוע מייצג, איזה סיכום נכתב לו, ואיזה changes נבנה ממנו. הקלט הוא אירוע כפי שמתואר ב-שכבת אירועי התוכן.

AUDIT::listener_content()#

AUDIT::listener_content(array $event): void

המאזין audit של הליבה. הוא נרשם בעצמו כש-AUDIT קיימת: אין צורך לקרוא לו.

ראו גם#

התיעוד נכתב מתוך הקוד של ליבה 5.0.185.