יומן הביקורת עונה על השאלה "מי שינה את זה, מתי ומה היה שם קודם". כל שינוי תוכן בניהול, כל כניסה ויציאה, כל שינוי של הרשאות או הגדרות, כל ייצוא וייבוא של 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 | מסך ההתחברות |
| כניסה בשם מנהל אחר | login | real_admin_id הוא המנהל שבפועל פעל, והסיכום נכתב "התחבר בשם ..." |
| ייצוא Excel מרשימה, או מיומן הביקורת | export | panel_table ופאנל audit_log. נרשם כמה שורות יצאו ובאיזה סינון, אף פעם לא השורות עצמן |
| עדכון גרסת ליבה | update_core | מסך עדכון הגרסה, עם גרסת המקור והיעד |
הרשימה היא הקבוע AUDIT::ACTIONS. ערך אחר ב-AUDIT::log() נדחה.
מה לא נכנס: שינוי שעוקף את הדלתות (ראו "מה השכבה לא רואה" ב-שכבת אירועי התוכן), פעולות באתר הציבורי (כניסת משתמש אתר, טופס יצירת קשר), וקריאות. היומן הוא יומן של הניהול.
טבלת audit_log עצמה#
שינוי בטבלה audit_log לא נכתב ליומן (אחרת כל ייצוא היה מוליד שורה על שורה).
CRM_audit_log#
| עמודה | טיפוס | משמעות |
|---|---|---|
id | bigint, PK | מספר רץ |
created_at | datetime | מתי |
admin_id | int | המנהל שפעל. 0 כשאין מנהל (ניסיון כניסה שהמשתמש בו לא זוהה) |
admin_name | varchar(100) | השם של המנהל ברגע האירוע, כך שגם אחרי מחיקת המנהל היומן נשאר קריא |
real_admin_id | int | בהתחזות: המנהל שפעל באמת. אחרת 0 |
ip | varchar(45) | כתובת ה-IP |
action | varchar(24) | הפעולה, מהטבלה למעלה |
tbl | varchar(64) | הטבלה בלי CRM_ (או null) |
row_id | int | הרשומה (או null) |
lang_id | int | השפה (או null) |
panel | varchar(64) | הפאנל שבו נעשה השינוי |
source | varchar(16) | הדלת: form, inline, mcp, import, login, export, update, code... |
summary | varchar(255) | השורה שאדם קורא |
changes | mediumtext | JSON של {שדה: [לפני, אחרי]}, או null |
request_id | char(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) | מחזיר |
|---|---|---|
index | ready, keep_days ורשימת השיטות | |
list | מסננים (למטה), page, limit (ברירת מחדל 50, עד 200) | total, page, pages, rows (החדש ראשון, לפי id) |
detail | id | row עם changes מפוענח, request_id ו-same_request (מזהי שורות אחרות של אותה בקשה, עד 50) |
filters | admins, actions, tables שאפשר לסנן לפיהם, עם תוויות | |
export | אותם מסננים | קובץ audit-log-<תאריך>.xlsx (עד 50,000 שורות, בלי changes) |
cleanup | deleted, 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 קיימת: אין צורך לקרוא לו.
ראו גם#
- שכבת אירועי התוכן ו-CONTENT::register: האירועים שמהם נכתבות רוב השורות.
- גרסאות ושחזור: היומן אומר מי ומה, הגרסאות שומרות את התוכן עצמו.
- הרשאות מנהלים ו-JSON API של הניהול.
- כל הטבלאות ו-סכמת מסד הנתונים.