גרסאות ושחזור

איך WIZZO CMS שומר גרסה לכל שמירה של סוג תוכן, מה כלל השמירה והניקוי, איך משחזרים גם רשומה שנמחקה, ואיך קוראים להיסטוריה ולשחזור מהקוד.

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

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

בעמוד הזה system/ הוא תיקיית הליבה הפרוסה (api/core בקוד המקור), ו-{admin} הוא CONFIG::$admin_url של האתר. הגרסאות זמינות מליבה 5.0.138 (בדקו ב-system/core/version.txt), עם תיקונים ב-5.0.141. מגרסה 5.0.156 יש ב-panel_table מגירת היסטוריה לכל שורה ולשונית "נמחקו לאחרונה".

להפעיל גרסאות לטבלה#

טבלה שומרת גרסאות כשהיא רשומה כסוג תוכן עם versions => 1. הרישום נעשה בקוד הפאנל:

class ADMINMODULE_Events extends bgl_controller
{
	public function index()
	{
		CONTENT::register("events", [
			"label"         => "אירועים",
			"panel"         => "Events",
			"versions"      => 1,
			"keep_versions" => 100,
			"title_field"   => "title",
		]);

		// ... panel_table / Form כרגיל
	}
}

האפשרויות של register() מפורטות ב-שכבת אירועי התוכן. כאן חשובות שלוש: versions מפעילה, keep_versions קובעת כמה גרסאות נשארות, ו-title_field נותנת כותרת לרשומות ברשימת הגרסאות וב"נמחקו לאחרונה". טבלה שלא נרשמה לא נוגעים בה: אין גרסאות ואין עלות.

מתי נשמרת גרסה#

מצבמה קורה
שמירה ראשונה אחרי שהגרסאות הופעלונשמרת קודם גרסת בסיס (baseline, מספר 0, בלי מנהל) עם הרשומה כפי שהייתה לפני השמירה, ואחריה גרסת השמירה. כך גם תוכן שנכתב לפני ההפעלה יש לו דרך חזרה
שמירה שלא שינתה כלוםלא נכתבת גרסה
שמירה אוטומטית (autosave) של אותו מנהל באותה רשומה, עד 10 דקות אחרי שמירה אוטומטית קודמתמחליפה את הגרסה האוטומטית האחרונה ולא מוסיפה חדשה
יצירהגרסה ראשונה, בלי גרסת בסיס
מחיקהגרסה אחת מסוג delete שמחזיקה את כל השפות, תחת lang_id שווה 0
שחזורגרסה חדשה מסוג restore, כדי שאפשר יהיה לבטל גם אותו
טיוטה שפורסמהגרסה מסוג publish, ראו סטטוס וטיוטות
ייבואגרסה מסוג import

גרסאות נשמרות לכל שפה בנפרד: שמירה בעברית ושמירה באנגלית הן שתי רשומות היסטוריה עם מספרי גרסה משלהן (version_no רץ לכל טבלה, רשומה ושפה).

CRM_content_versions#

עמודהטיפוסמשמעות
idbigint, PKמזהה הגרסה
tblvarchar(64)שם הטבלה בלי CRM_
row_idintה-id של הרשומה
lang_idintהשפה. 0 בגרסת מחיקה
version_nointהמספר הרץ של הגרסה ברשומה ובשפה. גרסת בסיס היא 0
kindvarchar(12)baseline, save, autosave, restore, publish, import או delete
datamediumtextהרשומה כ-JSON: {"main": {...}, "content": {...}}. ערכים רגישים מוסתרים ("***") כבר באירוע
changedvarchar(1000)שמות השדות ששונו, בפורמט |a||b| (מופרדים ב-||, נחתך בשלמות שדה)
admin_idintהמנהל ששמר. 0 בגרסת בסיס ובשמירה שאין מאחוריה מנהל
sourcevarchar(16)הדלת (form, inline, mcp, import, restore...)
notevarchar(255)הערה, למשל "שוחזר מגרסה 12"
created_atdatetimeמתי נשמרה

אינדקס מורכב על (tbl, row_id, lang_id, id) משרת את ההיסטוריה של רשומה, ואינדקס על created_at את הניקוי. הטבלה מגיעה לאתר ב-update_db של עדכון הגרסה (עדכון ליבה). בלי הטבלה כל המתודות מחזירות תשובה ריקה ושום שמירה לא נפגעת: CONTENT_VERSIONS::ready() אומרת אם היא קיימת.

ערכים רגישים לא חוזרים בשחזור

עמודה שהוסתרה ("***") לא נכתבת חזרה. סיסמה או מפתח שהשתנו אחרי הגרסה נשארים כפי שהם עכשיו. כמו כן id, ord וכל עמודה שמתחילה ב-wz_ (סטטוס הפרסום) לא חוזרים, וגם לא טבלאות צד של שדות: תגיות SEO, כתובת ידידותית, גלריות ותגיות שנשמרות בטבלאות נפרדות. השחזור מחזיר את הטבלה הראשית ואת טבלת _content בלבד.

שחזור#

שחזור הוא שמירה חדשה שמחזירה את הערכים של גרסה. הוא עובד בשלושה מקרים:

  1. רשומה קיימת. העמודות של הגרסה נכתבות מעל הרשומה, לכל שפה שהגרסה מחזיקה. מה שלא השתנה לא נכתב. אם שום דבר לא השתנה התשובה היא unchanged.
  2. רשומה שנמחקה, מגרסת מחיקה או מכל גרסה שלה. הרשומה נוצרת מחדש עם אותו id ועם כל השפות שהגרסה מחזיקה. אם ה-id כבר תפוס במשהו אחר, השחזור נכשל עם id_taken.
  3. גרסה של רשומה קיימת שהיא גרסת מחיקה. נכשל עם id_taken (הרשומה חזרה כבר).

אחרי שחזור נשלח אירוע restore (ולכן נכתבת גרסת restore וגם שורה ביומן, ראו יומן ביקורת), ודף הרשומה (preview_url, ואם יש כתובת SEO גם היא) יוצא מה-cache של הדפים.

קוד שעושה שחזור:

$res = CONTENT_VERSIONS::restore(345);

if (empty($res["success"])) {
	// version_not_found | type_without_versions | table_missing | version_empty
	// | record_missing | id_taken | insert_failed | failed
	error_log("restore failed: " . $res["error"]);
} elseif (!empty($res["unchanged"])) {
	// הרשומה כבר זהה לגרסה
} else {
	// $res["row_id"], $res["langs"], $res["version"] (ה-id של גרסת השחזור)
}

ניקוי יומי#

בלי ניקוי הטבלה גדלה בלי גבול. המדיניות:

  • בכל רשומה ושפה נשארות תמיד הגרסאות החדשות ביותר עד המספר keep, בלי קשר לגילן.
  • גרסה שגילה עולה על days ימים ושהיא מעבר ל-keep החדשות נמחקת.
  • גרסאות baseline ו-delete לא נמחקות לעולם.

ערכי המדיניות, לפי הסדר: הפרמטר content_versions_keep של האתר, אחריו keep_versions של סוג התוכן (ברירת מחדל 50), ובסוף 50. הפרמטר content_versions_days קובע את הימים (ברירת מחדל 30). הפרמטרים הם אופציונליים ואין להם שורה מוכנה: כדי לשנות, הוסיפו שורה במסך Params (פרמטרים ושפות) עם sysName זה.

הניקוי רץ כמשימת cron: כתובת system/content_versions_cron/cleanup, מדי יום ב-04:30 (30 4 * * *). המשימה נמצאת בתיקיית משימות המערכת (קטגוריה 1) ומגיעה לאתר בעדכון הגרסה, יחד עם הקונטרולר. אם אתר לא קיבל אותה, הוסיפו אותה ב-cron_manager עם אותה כתובת. הקונטרולר דורש את הטוקן של ה-cron (tk) או מנהל מחובר, ומחזיר שורת טקסט:

OK records=12 deleted=340 keep=50 days=30

records הוא מספר הרשומות והשפות שנבדקו, ו-deleted מספר הגרסאות שנמחקו. בכל הרצה נבדקות עד 5000 רשומות לכל טבלה, והשאר ממתינות להרצה הבאה.

ה-API של ההיסטוריה: content_history#

מסכי ההיסטוריה של הניהול מדברים עם פאנל AJAX בשם content_history. אפשר לקרוא לו גם מ-JS של פאנל שלכם. כל הקריאות ב-{admin}/content_history/<method>?pmode=empg ומחזירות JSON. צפייה דורשת גישה לפאנל של סוג התוכן (העמודה panel ב-CRM_content_types), ושחזור דורש גם את ההרשאה edit שלו (הרשאות).

שיטהקלטמחזיר
historytbl, id, lang (ברירת מחדל: השפה הנוכחית)versions חדש ראשון (בלי data), type, policy (keep, days)
versionidגרסה אחת עם data מפוענח
comparea, b (מזהה גרסה או current; a=0 = הגרסה שלפני b)fields: שדה אחרי שדה עם before, after, changed, kind (short, long, html) ובטקסט ארוך גם diff ברמת מילה
deletedtblrecords: מה שנמחק ולא חזר, החדש ראשון, עם כותרת
conflicttbl, id, lang, baseהאם מישהו אחר שמר מאז שהטופס נפתח על גרסה base
csrfטוקן לשחזור
restorePOST: version_id, csrf (או הכותרת X-WZ-CSRF)תשובת CONTENT_VERSIONS::restore()

שגיאות חוזרות בצורה {"success": false, "error": "<קוד>"}: missing_tbl, missing_id, versions_not_installed, type_without_versions, forbidden, version_not_found, post_only, csrf.

// השוואה בין הגרסה הקודמת למצב הנוכחי, מתוך פאנל
const r = await fetch(admin_url + "content_history/compare?pmode=empg&a=0&b=current", { credentials: "same-origin" });

ב-compare שדה רגיש מגיע עם sensitive: true וערכיו null. ה-conflict מחזיר conflict: false, או conflict: true עם by_name, at, minutes_ago, changed ו-compare (a, b) לפתיחת מסך ההשוואה. הוא מתעלם מגרסאות של המנהל עצמו, ומגרסאות בסיס.

מה כבר יש במסכים

ב-panel_table של סוג תוכן עם גרסאות יש מגירת היסטוריה בכל שורה ולשונית "נמחקו לאחרונה" (כבויה עם $history = false על הרשימה). אזהרת העריכה המקבילה (conflict) קיימת בצד השרת, וחיבורה לטופס יגיע בגרסה מאוחרת יותר.

מתודות של CONTENT_VERSIONS#

כל המתודות סטטיות ולא זורקות חריגה.

CONTENT_VERSIONS::ready()#

CONTENT_VERSIONS::ready(): bool

האם הטבלה CRM_content_versions קיימת באתר. התוצאה נשמרת לבקשה.

CONTENT_VERSIONS::enabled_type()#

CONTENT_VERSIONS::enabled_type(string $table): array|null

שורת הסוג מ-CRM_content_types כשהטבלה רשומה עם versions=1, אחרת null.

CONTENT_VERSIONS::history()#

CONTENT_VERSIONS::history(string $table, int $id, int $lang, int $limit = 200): array

הגרסאות של רשומה בשפה, החדשות ראשונות, בלי data (בין 1 ל-1000). לכל שורה: id, version_no, kind, admin_id, admin_name, source, changed (מערך), note, created_at. גרסאות המחיקה של הרשומה (שפה 0) כלולות.

CONTENT_VERSIONS::get()#

CONTENT_VERSIONS::get(int $version_id): array|null

גרסה אחת עם data מפוענח למערך, או null.

CONTENT_VERSIONS::last()#

CONTENT_VERSIONS::last(string $table, int $id, int $lang): array|null

הגרסה החדשה ביותר של הרשומה בשפה, בלי data.

CONTENT_VERSIONS::deleted()#

CONTENT_VERSIONS::deleted(string $table, int $limit = 100): array

הרשומות שנמחקו ולא חזרו: גרסת המחיקה האחרונה של כל אחת, עם title מעמודת title_field. החדשות ראשונות, עד 500.

CONTENT_VERSIONS::compare()#

CONTENT_VERSIONS::compare(int|string $a, int|string $b): array

שני מזהי גרסה, או "current" לרשומה כפי שהיא עכשיו. עם $a = 0 מושווה הגרסה שלפני $b. מחזירה success, table, row_id, lang_id, a, b ו-fields. כשל: version_not_found או different_records.

CONTENT_VERSIONS::word_diff()#

CONTENT_VERSIONS::word_diff(string $a, string $b, bool $html = false): array

השוואה ברמת מילה, כרשימת צמדים ["=", "טקסט"], ["-", "טקסט"], ["+", "טקסט"]. עם $html = true מושווה הטקסט בלי תגיות.

CONTENT_VERSIONS::conflict()#

CONTENT_VERSIONS::conflict(string $table, int $id, int $lang, int $base_id, ?int $admin_id = null): array

טופס נפתח על גרסה $base_id (0 = לפני כל גרסה). מחזירה ["conflict" => false, "head" => ...] או conflict: true עם head, by_admin_id, by_name, source, at, minutes_ago, changed ו-compare. שמירה של מנהל אחר, או כל שמירה מ-MCP, נחשבת "של מישהו אחר".

CONTENT_VERSIONS::restore()#

CONTENT_VERSIONS::restore(int $version_id, array $opts = []): array

ראו "שחזור" למעלה. מחזירה success, ובהצלחה גם row_id, langs, version (או undeleted: true, או unchanged: true) ו-not_restored (טקסט שמסביר מה לא חוזר). כשל: error.

CONTENT_VERSIONS::policy()#

CONTENT_VERSIONS::policy(?string $table = null): array

מחזירה ["keep" => int, "days" => int] לפי הסדר שתואר בניקוי.

CONTENT_VERSIONS::cleanup()#

CONTENT_VERSIONS::cleanup(int $max_records = 5000): array

מוחקת גרסאות ישנות לפי המדיניות של כל טבלה, ומחזירה ["records" => נבדקו, "deleted" => נמחקו]. מי שקורא לה בעצמו (למשל ממשימת cron מותאמת) מקבל בדיוק את מה שהמשימה היומית עושה.

גרסאות לקוד שכותב ישירות#

גרסה נכתבת מאירוע. קוד של אתר שכותב ל-DB בלי לעבור בטופס או ב-panel_table צריך לשלוח את האירוע בעצמו, אחרת אין גרסה. הדוגמה המלאה ב-שכבת אירועי התוכן. לפני שמוחקים רשומה בקוד, צלמו אותה בלי שפה (CONTENT::snapshot($table, $id)) ושלחו אירוע delete, כדי ש"נמחקו לאחרונה" ושחזור יחזירו את כל השפות.

ראו גם#

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