אתר שעובר מ-WordPress מביא איתו קובץ ייצוא אחד (WXR, "כלים > ייצוא" בניהול של WordPress). WIZZO CMS קורא את הקובץ, מראה מה יש בו, ומכניס את הרשומות לסוגי התוכן של האתר: פוסטים, עמודים, מוצרים וכל סוג מותאם. המדיה מועתקת לאתר, הכתובות בתוכן נכתבות מחדש, ולכל כתובת ישנה נכתבת הפניית 301.
בעמוד הזה system/ הוא תיקיית הליבה הפרוסה (api/core בקוד המקור). {admin} הוא CONFIG::$admin_url של האתר. המנוע זמין מליבה 5.0.152 והאשף מליבה 5.0.173, עם בדיקות ההרשאה שבעמוד הזה מליבה 5.0.176 (בדקו ב-system/core/version.txt).
שלוש דלתות, מנוע אחד#
| דלת | למי | איפה |
|---|---|---|
| אשף הייבוא | מנהל האתר | ניהול > "ייבוא מ-WordPress" (הפאנל wp_import, כתובת {admin}/wp_import) |
הכלים import_analyze, import_run, import_status | קרינגל או סקריפט | agent_mcp, ראו נוהל לקרינגל |
המחלקה wxr_import | קוד של אתר | system/libraries/wxr_import.php, הפעלים analyze(), run(), status() |
שלוש הדלתות קוראות לאותו מנוע. ההבדל ביניהן הוא מי בודק הרשאות: האשף בודק את ההרשאות של המנהל המחובר, הכלים עובדים בשם מנהל הבוט של agent_mcp.
מה האשף עושה, צעד אחרי צעד#
- קובץ. העלאה במנות של 4MB (עד 512MB לקובץ), או כתובת של קובץ ייצוא. הקובץ נשמר בשם אקראי, וקובץ עם
DOCTYPEאוENTITYנדחה לפני שמפענח XML נוגע בו. - ניתוח. דוח שלא כותב שום תוכן: כמה רשומות מכל סוג, מה הסטטוסים, אילו קודים קצרים (shortcodes) ובלוקים של Gutenberg יש, מה לא יעבור, ולכל סוג תוכן מבנה מוצע עם השדות, השדות המותאמים (postmeta) ודוגמאות מהם.
- יעד לכל סוג. סוג תוכן קיים באתר, או סוג חדש שהאשף בונה מהמבנה המוצע (אחרי שהמנהל ערך שמות, סוגי שדות ומקורות). סוג שלא בוחרים לו יעד מדולג.
- מיפוי שדות. איזה שדה בטופס מקבל איזה מקור מ-WordPress. האשף ממלא ניחוש לפי שמות השדות, והמנהל מתקן.
- הרצה. מנה אחרי מנה, עם התקדמות חיה. אפשר לעצור ולהמשיך מאותה נקודה, גם אחרי סגירת הדפדפן.
- דוח סיום. כמה נוצרו, עודכנו, דולגו ונכשלו, לכל סוג, ולכל כישלון הסיבה שלו. מכאן אפשר למחוק את קובץ הייצוא מהשרת.
מה קורה לכל רשומה#
- דרך הטופס. כל רשומה נשמרת דרך הטופס של סוג התוכן (
admin_mcp_tools::save_record), כך שכל שדה עושה את העבודה שלו (תמונה עוברת דרך האחסון, עורך מנקה HTML). שכבת אירועי התוכן פולטת אירוע עםsource=import, ויומן הביקורת רושם אותו תחתimport. - סטטוס. הסטטוס של WordPress הופך ל-
wz_status(סטטוס וטיוטות):publishהואpublished,draftהואdraft,pendingהואreview,futureהואscheduled,privateהואhidden. התאריך המקורי נכנס ל-wz_published_at. - מדיה. כל קובץ מהאתר הישן מועתק דרך
STORAGE::add(עד 50MB לקובץ), והכתובות בתוכן מוחלפות בכתובות החדשות. כתובות מדיה מ-CDN נוסף נכתבות ב-media_hosts. - תוכן. הקודים הקצרים
caption,gallery,embed,videoו-audioמומרים ל-HTML. קוד שעלול לרוץ באתר (סקריפט,on*, iframe ממקור לא מוכר) מנוקה, והרשומה מקבלת אזהרה שאומרת מה נוקה. - הפניות. לכל כתובת ישנה נכתבת הפניית 301 ב-
CRM_redirections(ראו הפניות). הפניה לא נכתבת מכתובת שהאתר עונה עליה בעצמו: כתובת שתפוסה כבר בעמוד, השורש של בקר פעיל ב-CRM_modules(למשל/contact) או כל כתובת תחתsystem/. הרשומה מקבלת אזהרה שאומרת למה. - בלי כפילויות. הטבלה
CRM_import_itemsשומרת לכל פריט מקור את היעד שלו. הרצה שנייה, או ייצוא חדש של אותו אתר, מעדכנת את מה שכבר יובא ולא יוצרת עותק.
סוגי המערכת של WordPress (attachment, nav_menu_item, revision וחבריהם) לא מיובאים כרשומות. הקבצים המצורפים מגיעים כמדיה של הרשומות שמשתמשות בהם.
מי רשאי#
- פתיחת האשף היא בדיקת הפאנל הרגילה של הניהול (השורה
wp_importבתפריט, ראו הרשאות). - ייבוא לסוג תוכן דורש את ההרשאה
importעל הפאנל של אותו סוג (ROLES::can("import", $panel)) והרשאת עריכה בפאנל (ADMIN::has_perms("edit", $panel)): קבוצה עם צפייה בלבד לא מייבאת. בתפקידים המובנים, לבעלים ולעורך יש אותה ולכותב אין. האשף מציג כיעדים רק סוגים שהמנהל רשאי לייבא אליהם, וכל פעולה בודקת שוב בצד השרת. - פרסום. מי שאין לו
publishבפאנל מייבא אליו רק טיוטות ופריטים שממתינים לאישור. אם סומנו בהרצה סטטוסים אחרים, ההרצה לא מתחילה (no_publish) וההודעה אומרת מה לסמן. - שדות חסומים. שדה שנמצא ב-
ROLES::fields_denyשל הפאנל לקבוצה של המנהל לא מתמלא בייבוא, והרשומה מקבלת אזהרה. האשף מעביר את הרשימה ל-run()בארגומנטdeny_fields([panel => [field]]). - בניית סוג תוכן חדש (טבלה, פאנל, שורה בתפריט) מותרת רק לבעלים או למפתח.
- יומן. כל ניתוח, כל סוג שנבנה, כל התחלה ועצירה של הרצה וכל סיום נרשמים ביומן הביקורת עם
action=import.
קובץ המיפוי#
המיפוי נשמר על משימת הייבוא (CRM_import_jobs.mapping), ולכן הרצה שממשיכה לא צריכה לשלוח אותו שוב. האשף בונה אותו בשבילכם. בכלים ובקוד כותבים אותו כך:
{
"types": {
"post": {
"adapter": "generic",
"panel": "articles",
"form_page": "insert",
"auto": true,
"url": "blog/{id}",
"url_mode": "slug",
"fields": { "title": "title", "text": "content", "image": "featured_image", "price": "meta:price" }
},
"page": { "adapter": "pages", "panel": "pages" },
"revision_log": { "skip": true }
},
"statuses": ["publish", "draft", "future"],
"media": true,
"redirects": true,
"media_hosts": ["cdn.example.com"]
}
| מפתח | משמעות |
|---|---|
adapter | generic (ברירת מחדל), pages, או השם של מתאם של האתר |
panel | הפאנל של סוג התוכן ביעד |
form_page | המתודה של הפאנל שמחזיקה את הטופס, ברירת מחדל insert |
auto | ברירת מחדל true: שדות שהמיפוי לא מזכיר מקבלים מקור לפי שם (title, text, image, tags...), ושדה מותאם שהשם שלו זהה לשם שדה בטופס נכנס אליו |
url | תבנית הכתובת החדשה, לבניית ההפניות |
url_mode | slug (ברירת מחדל): הכתובת החדשה בנויה מה-slug. keep: הנתיב הישן כולו נשמר כ-SeoUrl |
fields | {שדה בטופס: מקור} |
skip | true מדלג על הסוג |
המקורות#
| מקור | מה נכנס |
|---|---|
title, content, excerpt | הכותרת, התוכן אחרי המרה וניקוי, התקציר |
date | תאריך הפרסום המקורי |
featured_image | התמונה הראשית, מועתקת לאחסון |
categories, tags, term:<taxonomy> | שמות המונחים. שדה בחירה מקבל את האפשרות שהתווית שלה זהה לשם |
author | השם המוצג של הכותב |
slug, link, wp_id | ה-slug, הכתובת הישנה, המספר ב-WordPress |
seo_title, seo_description | הכותרת והתיאור של Yoast או של Rank Math |
meta:<key> | שדה מותאם (postmeta). תאריך בצורת YYYYMMDD (ACF) או חותמת זמן של יוניקס (תאריכי המבצע של WooCommerce) הופך לתאריך, ומספר בשדה קובץ הופך לקובץ המצורף |
value:<טקסט> | ערך קבוע לכל הרשומות |
שדות מותאמים שמתחילים בקו תחתון#
מפתח שמתחיל בקו תחתון שייך בדרך כלל לתוסף עצמו, והניתוח לא מציע אותו. יוצאים מזה המפתחות המוכרים של WooCommerce ושל The Events Calendar: הניתוח מציע אותם כשדות בשם קריא, והמיפוי האוטומטי מחבר אותם לשדה בשם הזה כשהוא קיים בטופס:
| מפתח | שדה מוצע | סוג |
|---|---|---|
_price, _regular_price, _sale_price | price, regular_price, sale_price | מספר |
_sale_price_dates_from, _sale_price_dates_to | sale_from, sale_to | תאריך |
_sku, _stock_status, _manage_stock, _virtual, _downloadable | sku, stock_status, manage_stock, virtual, downloadable | טקסט |
_stock | stock | מספר שלם |
_weight, _length, _width, _height | weight, length, width, height | מספר |
_product_image_gallery | gallery | טקסט: מזהי קבצים מופרדים בפסיק, גלריה של קבצים צריכה מתאם |
_EventStartDate, _EventEndDate | event_start, event_end | תאריך |
כל מפתח אחר, גם עם קו תחתון, אפשר למפות במפורש עם meta:<key>. המפתחות של ACF שמתחילים בקו תחתון מחזיקים רק את ההפניה לשדה (field_...); הערך עצמו נמצא במפתח בלי הקו, והוא מוצע כמו כל שדה מותאם.
מתאם ייבוא לסוג תוכן של אתר#
רוב סוגי התוכן לא צריכים קוד: generic ממפה לפי שמות השדות בטופס, ומה שהוא לא מנחש נכתב ב-fields. כותבים מתאם כשהסוג צריך משהו שמיפוי לא נותן:
- קטגוריות או כותבים בטבלה משלהם, כשהשדה שומר מספר ולא שם;
- ערך שמחושב מכמה שדות (למשל מחיר המבצע כשהוא קיים, ואחרת המחיר הרגיל);
- המרה של מבנה מיוחד בתוכן (קוד קצר של תוסף).
איפה ובאיזה שם#
הקובץ הוא application/libraries/wxr_adapter_<name>.php, המחלקה wxr_adapter_<name>, והיא יורשת את wxr_adapter_generic. המנוע טוען אותה לפי השם שבמיפוי ("adapter": "<name>"), ומחלקה או קובץ שחסרים הם שגיאה של ההרצה כולה ולא של רשומה אחת.
שלוש המתודות#
| מתודה | מחזירה |
|---|---|
map_fields($item) | [שדה בטופס => ערך] לרשומה אחת |
taxonomy($item, $taxonomy, $field) | הערך של שדה מונחים. $taxonomy הוא category, post_tag או אחר |
author($item, $field) | הערך של שדה כותב |
$item הוא פריט אחד מהקובץ: id, type, status, title, content, excerpt, date, slug, link, author (שם המשתמש), meta (לכל מפתח, רשימת ערכים) ו-terms (רשימה של {taxonomy, slug, name}). $field הוא תיאור השדה מהטופס (name, type, select_options...).
במתאם זמינים: $this->fields (שדות הטופס לפי שם), $this->map (המיפוי של הסוג), $this->engine (המנוע: convert_content(), attachment_storage(), author_name(), item_lookup()), $this->warnings[] (אזהרה שתוצג על הרשומה בדוח), ו-$this->dry (הרצת ניסיון, בלי כתיבה).
דוגמה: מוצרים של WooCommerce#
<?php
// application/libraries/wxr_adapter_shop.php
class wxr_adapter_shop extends wxr_adapter_generic
{
function map_fields($item)
{
$out = parent::map_fields($item); // title, text, image, sku ושאר המיפוי
$m = $item["meta"];
$price = $m["_sale_price"][0] ?? "";
if ($price === "") $price = $m["_regular_price"][0] ?? "";
if ($price !== "") $out["price"] = (float)$price; // המחיר בפועל: המבצע כשיש, אחרת הרגיל
return $out;
}
// הקטגוריות של האתר בטבלה משלהן: השדה שומר מספרים
function taxonomy($item, $taxonomy, $field)
{
$ids = [];
foreach ($item["terms"] as $t)
{
if ($t["taxonomy"] !== $taxonomy) continue;
$id = DB::get_val("shop_categories", ["slug" => $t["slug"]], "id");
if ($id) $ids[] = (int)$id;
else $this->warnings[] = "אין באתר קטגוריה " . $t["name"];
}
return $ids ?: null;
}
}
ובמיפוי: "product": { "adapter": "shop", "panel": "products", "fields": { "categories": "term:product_cat" } }.
import_run עם dry_run: true מחזיר מה הטופס היה מקבל לשלוש הרשומות הראשונות, בלי לשמור ובלי להוריד מדיה. כך בודקים מתאם חדש לפני הרצה אמיתית.מה לא לעשות במתאם#
- לא לכתוב לטבלאות התוכן ישירות. הערך חוזר מ-
map_fields, והטופס שומר אותו, כדי שהאירועים, הגרסאות והיומן יעבדו. - לא להוריד קבצים בעצמכם.
attachment_storage()ו-convert_content()מורידים רק ממקורות האתר הישן, בגבולות הגודל, ונמנעים מכפילויות. - לא להחזיר HTML גולמי מהקובץ לשדה עורך.
convert_content()הוא מה שמנקה אותו.
בניית סוג תוכן חדש#
כשאין באתר סוג שמתאים, האשף בונה אחד דרך content_type_builder (system/libraries/content_type_builder.php): הטבלה CRM_<t> והטבלה CRM_<t>_content עם עמודות הסטטוס, קובץ פאנל application/admin/<t>.php על content_type_panel, השורה ב-CRM_content_types (CONTENT::register) והשורה בתפריט הניהול. סוגי השדות האפשריים: text, editor, textarea, datetime, image, tags, url, number. שם שכבר תפוס באתר (טבלה או פאנל) נדחה.
הבנייה מחזירה גם רשומת מיפוי מוכנה (adapter: generic, השדות והמקורות שלהם), והאשף ממשיך איתה ישר למיפוי.
ההפניות נכתבות לכתובת לפי url (למשל products/{id}). הכתובת עונה באתר רק כשיש בקר אתר לחלק הראשון שלה (שורה ב-CRM_modules). content_type_builder::build() מחזיר אז front_ready: false, וההפניות נשמרות ומחכות לבקר שיבנו בשביל הסוג.