clicks = new CloakPilot_Clicks(); $this->cpt = new CloakPilot_CPT(); $this->redirect = new CloakPilot_Redirect( $this->clicks ); $this->autolink = new CloakPilot_Autolink(); $this->reports = new CloakPilot_Reports( $this->clicks ); // בדיקת התקינות והבלוקים שייכים ליכולות בתשלום: קיימים רק בבנייה המלאה (ראו הכותרת בקובץ הראשי). $health = null; $this->rest = new CloakPilot_REST( $this->clicks, $health ); $this->import = new CloakPilot_Import(); $this->vendors = new CloakPilot_Vendors(); $this->positions = new CloakPilot_Positions(); $this->wizard = new CloakPilot_Wizard(); $this->ui = new CloakPilot_UI(); // מסך הקישורים, מסך הקטגוריות, והמסך "הקישור אינו קיים": רושמים hooks בעצמם. new CloakPilot_Links_Screen(); new CloakPilot_Categories_Screen(); new CloakPilot_Gone(); // Fire and forget: registers its own hooks, nothing else calls into it. Full build only: // the WordPress.org copy is updated by WordPress.org (guideline 8). // סוכני AI, שרת ה-MCP וה-OAuth שלו: רק בבנייה המלאה (1.0.0-rc.3, הכרעת מוטי 24.10.2026). // תיבת ההערות הישנה של בודקי הבטא: מסך היסטוריה לקריאה בלבד, רק באתר שכבר יש בו הערות. new CloakPilot_Feedback(); // חלון "Send feedback" בכל מסך של התוסף, נשלח מהשרת ל-cloakpilot.net (1.0.0-rc.1). new CloakPilot_Send_Feedback(); // שכבת הרישוי (30.8.2026): מטפל בטופס הפעלת/כיבוי דגל האכיפה. CloakPilot_Plans עצמה // היא סטטית בלבד ולא נרשמת כאן - אין לה hooks, רק פונקציות עזר. new CloakPilot_License(); // The full build is distributed outside WordPress.org and bundles its translations, so it loads // them. The WordPress.org build does not: WordPress loads translate.wordpress.org files by itself. // מעקב GA4 בצד הגולש (יכולת חינמית: גם הקישור האוטומטי מסמן data-cloakpilot-ga4). עבר לכאן // מ-CloakPilot_Block ב-24.10.2026 כדי שיפעל גם בבנייה ל-WordPress.org, שאין בה בלוקים. add_action( 'wp_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_ga4' ) ); // [cloakpilot_cta] כקישור רגיל כשהבלוקים אינם בבנייה (WordPress.org). בבנייה המלאה // CloakPilot_Block רושם את השורטקוד קודם, ואז זה לא עושה דבר. add_action( 'init', array( __CLASS__, 'register_plain_cta_shortcode' ), 20 ); /* 1.0.0-rc.1 (18.10.2026): סרגל ההערות המרוחק של בודקי הבטא, נתיבי ה-notes session/verify והטוקן הקבוע הוסרו מהחבילה (ממצאי הביקורת SEC-01 ו-SEC-02): שום סקריפט חיצוני לא נטען יותר במסכי הניהול, ושום מפתח רישיון לא יוצא מהאתר. המשוב עובר עכשיו דרך חלון "Send feedback" שנשלח מהשרת (CloakPilot_Send_Feedback). */ add_action( 'plugins_loaded', array( $this, 'maybe_upgrade_db' ) ); // עדכון סכימה שלא הושלם מוצג למנהל עד שהוא מצליח (חוות דעת שנייה, סבב 3). add_action( 'admin_notices', array( __CLASS__, 'db_upgrade_failed_notice' ) ); /* אחרי מחיקה או שחזור, וורדפרס מחזיר לרשימה שלו ולא לרשימה של התוסף (מוטי, 27.8.2026: "כשאני לוחץ על העברה לאשפה מתוך מסך העריכה זה מעביר אותנו לתצוגת וורדפרס"). הכלל בליבה: post.php לוקח את wp_get_referer, וכשההפניה הגיעה מ-post.php הוא נופל ל-edit.php. שני הפילטרים כאן נועלים את היעד לרשימה של התוסף, לכל סוגי התוכן שלנו, מכל מסך שממנו נלחץ הכפתור. */ add_filter( 'get_delete_post_link', array( __CLASS__, 'own_delete_return' ), 10, 3 ); // הורדה והעלאה של קובץ תרגום (מוטי, הערה 217). add_action( 'admin_post_cloakpilot_download_pot', array( __CLASS__, 'handle_download_pot' ) ); add_action( 'admin_post_cloakpilot_upload_translation', array( __CLASS__, 'handle_upload_translation' ) ); // מפתח הפוסטבק ואיפוס ההמרות שייכים למעקב ההכנסות: רק בבנייה המלאה. add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) ); // פס הגלילה של החלון נגזר משורש המסמך ולא מה-body, ולכן color-scheme // חייב לשבת גם שם. אותו שער מסך בדיוק, כדי שלא נשחיר מסכים של אחרים. // 1.0.1: through wp_add_inline_style / wp_add_inline_script on enqueued handles, not echoed tags. add_action( 'admin_enqueue_scripts', array( $this, 'theme_color_scheme' ), 20 ); // FR-ARCH-001: כשה-link_prefix בהגדרות משתנה, כללי rewrite ישנים ששייכים // לתחילית הקודמת (או ששייכים לתחילית החדשה משריד של תוסף אחר) חייבים להתנקות, // לא רק flush_rewrite_rules() תמים. update_option_{option} רץ רק כשהערך // באמת השתנה (השוואה מובנית של וורדפרס), כך שזה לא רץ בכל שמירת הגדרות סתם. add_action( 'update_option_' . self::OPTION_KEY, array( __CLASS__, 'maybe_flush_on_prefix_change' ), 10, 2 ); // REVIEW-SEC.md finding 6: ראה filter_out_foreign_prefix_rules() למטה - הרישום // כאן ולא רק ב-activate() מבטיח שהפילטר פעיל בכל regeneration של הכללים, לא רק // באקטיבציה, כולל flush שמופעל ממקום אחר לגמרי (permalinks נשמרים, תוסף אחר // מפעיל flush_rewrite_rules()). זה מה שהופך את הניקוי לעמיד ולא לחד-פעמי. add_filter( 'rewrite_rules_array', array( __CLASS__, 'filter_out_foreign_prefix_rules' ) ); } /** * מערכת העיצוב של האדמין - נטענת אך ורק במסכים של CloakPilot. * * עד כאן הגיליון נטען גם ב-index.php "בשביל ווידג'ט הדשבורד", והתוצאה נמדדה * בדשבורד של האתר: הכלל body.cloakpilot-rtl .postbox .inside שבגיליון סובב את הווידג'טים * של וורדפרס ושל תוספים אחרים (Quick Draft, במבט מהיר, Rank Math, TinyPNG). * הווידג'ט שלנו לא צריך את הגיליון בכלל - כל העיצוב שלו inline - ולכן הטעינה שם * הוסרה. מקור האמת לזיהוי המסך הוא CloakPilot_Locale::is_plugin_screen(), אותו זיהוי * שקובע את שפת המסך, כדי ששפה, כיווניות ועיצוב לא יוכלו להיפרד זה מזה. */ /** * גרסת נכס שנגזרת מזמן השינוי של הקובץ, ולא ממספר גרסת התוסף. * * בלי זה כל תיקון עיצוב שנפרס בלי העלאת גרסה מוגש מהקאש של הדפדפן, * והבדיקה הוויזואלית מודדת קובץ ישן ומדווחת "לא עובד" על תיקון שכן עבד. * זה קרה פעמיים בריצת העיצוב, ולכן זה נסגר כאן ולא בזהירות ידנית. */ private static function asset_ver( $rel ) { $path = CLOAKPILOT_DIR . $rel; $time = file_exists( $path ) ? filemtime( $path ) : 0; return $time ? CLOAKPILOT_VERSION . '.' . $time : CLOAKPILOT_VERSION; } public function admin_styles( $hook ) { if ( ! CloakPilot_Locale::is_plugin_screen() ) { return; } wp_enqueue_style( 'cloakpilot-admin', CLOAKPILOT_URL . 'assets/admin.css', array(), self::asset_ver( 'assets/admin.css' ) ); // מערכת העיצוב החדשה (עיצוב 22.8.2026, ראה specs/mockup/START-HERE.md). // נטענת אחרי cloakpilot-admin בכוונה: המסכים שכבר הועברו למבנה החדש (CloakPilot_UI::shell_open) // גוברים על מה שנשאר מהעיצוב הישן, עד שכל המסכים יעברו וה-admin.css הישן יוסר. wp_enqueue_style( 'cloakpilot-admin-v2', CLOAKPILOT_URL . 'assets/admin-v2.css', array( 'cloakpilot-admin' ), self::asset_ver( 'assets/admin-v2.css' ) ); wp_enqueue_script( 'cloakpilot-admin-v2', CLOAKPILOT_URL . 'assets/admin-v2.js', array(), self::asset_ver( 'assets/admin-v2.js' ), true ); wp_localize_script( 'cloakpilot-admin-v2', 'CloakPilotL10n', self::admin_v2_l10n() ); // נתיב REST לשמירת ui_theme/ui_lang מהרצועה העליונה (class-cloakpilot-rest.php, // PUT /settings/ui). syncSettingToServer() ב-admin-v2.js קורא לזה בכל // לחיצה על מתג התצוגה או בורר השפה, בכל מסך - לא רק במסך ההגדרות. wp_localize_script( 'cloakpilot-admin-v2', 'cloakpilotSettings', array( 'restUrl' => esc_url_raw( add_query_arg( CloakPilot_Locale::UI_PARAM, '1', rest_url( CloakPilot_REST::NS . '/' ) ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), // מקור האמת לשפה ולמצב התצוגה הוא ההגדרה השמורה בשרת, לא זיכרון // הדפדפן. בלי זה נוצרו שני מקורות אמת: שינוי במסך ההגדרות נשמר // בשרת, ובטעינה הבאה הסקריפט דרס אותו עם הערך הישן שנשאר בדפדפן // (מוטי, 23.8.2026: "יש התנגשות בין מעבר השפות בתפריט לזה שבהגדרות"). 'uiLang' => CloakPilot_UI::lang(), 'uiTheme' => self::theme(), // נונס למעבר השפה האטומי (CloakPilot_Locale::maybe_persist_ui_lang): // הקליק מנווט לאותה כתובת עם ?cloakpilot_set_lang=, השרת מתמיד ומרנדר // מחדש בשפה החדשה בטעינה אחת - כיוון וטקסט יחד. 'langNonce' => wp_create_nonce( 'cloakpilot_set_lang' ), ) ); // מסילת הטאבים האנכית (CloakPilot_UI::tabs_open). נטענת דרך אותו שער בדיוק // כמו הגיליון, ולא מתוך גוף המסך, כדי שהתזמון יהיה זהה לזה שכבר עובד. // 25.8.2026: שני קובצי הסקריפט האלה נטענו לפי מספר גרסת התוסף ולא לפי זמן // השינוי של הקובץ, בעוד הגיליונות כבר עברו ל-asset_ver(). התוצאה: תיקון // שנפרס בלי העלאת גרסה הוגש מהקאש של הדפדפן, ובדיקה חיה החזירה את הקוד // הישן כאילו התיקון נכשל. נתפס בפועל בבדיקת קבוצה ג. wp_enqueue_script( 'cloakpilot-tabs', CLOAKPILOT_URL . 'assets/tabs.js', array(), self::asset_ver( 'assets/tabs.js' ), true ); // הצעת המוכר: בכל מסך שנוצר בו קישור - עריכת קישור, וצעד הקישור הראשון // באשף. אותו קובץ, כדי ששתי ההתנהגויות לא ייפרדו בעוד חודש. $screen = get_current_screen(); $on_link_editor = $screen && 'post' === $screen->base && CloakPilot_CPT::POST_TYPE === $screen->post_type; $on_wizard = isset( $_GET['page'] ) && CloakPilot_Wizard::PAGE === $_GET['page']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $on_link_editor || $on_wizard ) { wp_enqueue_script( 'cloakpilot-vendor-suggest', CLOAKPILOT_URL . 'assets/vendor-suggest.js', array(), self::asset_ver( 'assets/vendor-suggest.js' ), true ); wp_localize_script( 'cloakpilot-vendor-suggest', 'CLOAKPILOT_VENDOR_SUGGEST', array( // בסיס נקי בלי query string. קודם נשלח כאן rest_url עם _cloakpilot_ui=1 כבר // בפנים, והדפדפן שרשר אחריו את שם הנתיב - כך שכל קריאה הגיעה // לכתובת "/v1/?_cloakpilot_ui=1vendor-suggest" ונענתה ברשימת הנתיבים. // הפרמטר מתווסף עכשיו בצד הדפדפן, כחלק מה-query string. 'rest' => rest_url( CloakPilot_REST::NS . '/' ), 'ui' => CloakPilot_Locale::UI_PARAM, 'nonce' => wp_create_nonce( 'wp_rest' ), 'post' => $on_link_editor ? (int) get_the_ID() : 0, 'i18n' => array( 'vendorKnown' => __( 'Vendor:', 'cloakpilot' ), 'vendorCard' => __( "To the vendor's card", 'cloakpilot' ), 'vendorNone' => __( 'No vendor card exists for this address yet.', 'cloakpilot' ), 'vendorCreate' => __( 'Create vendor', 'cloakpilot' ), 'vendorNameLabel' => __( 'Vendor name', 'cloakpilot' ), 'vendorFailed' => __( 'Could not create the vendor.', 'cloakpilot' ), ), ) ); } // מסך עריכת קישור (עיצוב 22.8.2026): התנהגות ייחודית למסך הזה (הזזת שדה // הכותרת/כפתור הפרסום/מחיקה האמיתיים, עריכת סלאג, כללי התזמון, // אזהרת שינויים לא שמורים). ראה assets/link-edit.js. הנתונים המקומיים // (CLOAKPILOT_LINK_EDIT) נרשמים ב-CloakPilot_CPT::render_link_edit_l10n() מתוך תוך // render_details_box(), כי חלקם (isNew) תלויים בפוסט הנוכחי. if ( $on_link_editor ) { wp_enqueue_script( 'cloakpilot-link-edit', CLOAKPILOT_URL . 'assets/link-edit.js', array(), self::asset_ver( 'assets/link-edit.js' ), true ); } } /** * מילון הממשק של admin-v2.js (window.CloakPilotL10n). כל מפתח תואם לקריאת * t('key', 'נפילה בעברית') בקובץ - הנפילה קיימת כדי שהתנהגות המוקאפ לא * תישבר אם מפתח נשמט כאן, אבל המקור לתרגום עצמו הוא תמיד המילון הזה. * * האנגלית נלקחה מילה במילה מ-specs/mockup/i18n-map.json (חוק הזהב 2 * ב-BRIEF-IMPL.md). מחרוזות שלא היה להן שם רשומה במילון (המוקאפ לא * הזדקק להן כי הן מגיעות רק מהתנהגות ה-JS בזמן ריצה, לא מה-HTML הסטטי) * נכתבו כאן לפי אותו מונחון ומדווחות בדוח המסירה של סוכן היסודות. */ public static function admin_v2_l10n() { return array( 'themeToggleDark' => __( 'Appearance: Dark. Switch to light mode', 'cloakpilot' ), 'themeToggleLight' => __( 'Display mode: light. Switch to dark', 'cloakpilot' ), 'copied' => __( 'Copied', 'cloakpilot' ), 'copiedColon' => __( 'Copied:', 'cloakpilot' ), 'copiedAddress' => __( 'URL copied', 'cloakpilot' ), 'copyFailed' => __( 'Copying did not work, you can select and copy manually', 'cloakpilot' ), 'confirmTitle' => __( 'One moment', 'cloakpilot' ), // כותרת וכפתור של חלון הודעה (window.lpAlert ב-admin-v2.js): הודעה // שהמשתמש חייב לאשר שראה, בלי "ביטול" - להבדיל מטוסט חולף. 'noticeTitle' => __( 'Worth knowing', 'cloakpilot' ), 'dialogOk' => __( 'Got it', 'cloakpilot' ), // תוויות כפתור האישור בחלונות שאינם מחיקה. ברירת המחדל של החלון היא // "כן, למחוק", ולכן כל אישור אחר מוסר אותה ונוקב בפעולה שלו עצמו. 'saveAnyway' => __( 'Save anyway', 'cloakpilot' ), 'leaveWithoutSaving' => __( 'Leave without saving', 'cloakpilot' ), 'close' => __( 'Close', 'cloakpilot' ), 'cancel' => __( 'Cancel', 'cloakpilot' ), 'confirmDeleteYes' => __( 'Yes, delete', 'cloakpilot' ), 'deleted' => __( 'Deleted', 'cloakpilot' ), 'pickerSearchVendor' => __( 'Search vendor or domain...', 'cloakpilot' ), 'pickerSearch' => __( 'Search...', 'cloakpilot' ), 'pickerSearchAria' => __( 'Search list', 'cloakpilot' ), 'pickerNoMatch' => __( 'No matches for this search.', 'cloakpilot' ), 'pickerNewVendor' => __( '+ New vendor', 'cloakpilot' ), // לוח השנה של התוסף (הערה 111). הלוח של הדפדפן אינו ניתן לעיצוב, // ולכן הוא מוחלף בלוח שלנו - ראה dateEnhance ב-admin-v2.js. 'dpOpen' => __( 'Open the calendar', 'cloakpilot' ), 'dpPrevMonth' => __( 'Previous month', 'cloakpilot' ), 'dpNextMonth' => __( 'Next month', 'cloakpilot' ), 'dpToday' => __( 'Today', 'cloakpilot' ), 'dpClear' => __( 'Clear', 'cloakpilot' ), 'pickerNewVendorName' => __( 'Vendor name', 'cloakpilot' ), 'pickerNewVendorHost' => __( 'Domain (optional)', 'cloakpilot' ), 'pickerNewVendorSave' => __( 'Create and link', 'cloakpilot' ), // %s = שם המוכר. /* translators: %s: vendor name */ 'pickerVendorCreated' => __( 'Vendor %s was created and linked', 'cloakpilot' ), // %1$s = מספר קישורים, %2$s = מספר קליקים. /* translators: 1: number of links, 2: number of clicks */ 'groupCount' => __( '%1$s links · %2$s clicks', 'cloakpilot' ), // %s = מספר קישורים. /* translators: %s: number of links */ 'countAll' => __( '%s links', 'cloakpilot' ), // %1$s = מספר מסונן, %2$s = סך הכול. /* translators: 1: number of links shown, 2: total number of links */ 'countFiltered' => __( '%1$s links out of %2$s', 'cloakpilot' ), 'pagerNone' => __( 'No links to show', 'cloakpilot' ), // %s = מספר קישורים. // מוטי, 23.8.2026 (הערה 18): "כל 113 הקישורים בעמוד אחד" הוחלף // בספירה פשוטה, ובאותה מחרוזת שבה משתמש countAll. /* translators: %s: number of links */ 'pagerAllOnOnePage' => __( '%s links', 'cloakpilot' ), // %1$s = מוצג, %2$s = סך הכול. /* translators: 1: number of links shown, 2: total number of links */ 'pagerShownOf' => __( 'Showing %1$s of %2$s links', 'cloakpilot' ), // שני המפתחות הבאים משרתים את העימוד בטאב "לפי קישור" במסך הדוחות // (מוקאפ 04-reports.html, "עמוד 1 מתוך 7") - אותן שתי מחרוזות בדיוק // שכבר קיימות ומתורגמות במקומות אחרים בתוסף, בלי מפתח תרגום חדש. 'reportPageWord' => __( 'Page', 'cloakpilot' ), 'reportOfWord' => __( 'of', 'cloakpilot' ), 'qeName' => __( 'Link name', 'cloakpilot' ), 'qeSlug' => __( 'Link slug', 'cloakpilot' ), 'qeDest' => __( 'Destination', 'cloakpilot' ), 'qeDestHint' => __( "The vendor's full URL, including parameters", 'cloakpilot' ), 'update' => __( 'Update', 'cloakpilot' ), 'qeEmptyDestHint' => __( 'Saving with an empty destination field will ask for confirmation, because visitors will land on an error page.', 'cloakpilot' ), 'qeConfirmEmptyDest' => __( 'The "Destination" field is empty - visitors who click this link will land on an error page. Save anyway?', 'cloakpilot' ), 'linkUpdated' => __( 'The link was updated', 'cloakpilot' ), 'catsNewPlaceholder' => __( '+ New category (multiple allowed, comma separated)', 'cloakpilot' ), 'save' => __( 'Save', 'cloakpilot' ), 'saving' => __( 'Saving...', 'cloakpilot' ), 'saveFailed' => __( 'Saving failed', 'cloakpilot' ), 'noCategory' => __( 'Uncategorised', 'cloakpilot' ), 'categoriesSaved' => __( 'Categories saved', 'cloakpilot' ), // %s = שם הקישור. /* translators: %s: link name */ 'rowTrashConfirm' => __( 'Move "%s" to trash? The link will stop working.', 'cloakpilot' ), /* translators: %s: link name */ 'rowTrashed' => __( '"%s" was moved to trash', 'cloakpilot' ), /* translators: %s: link name */ 'rowCacheCleared' => __( 'Cache for "%s" was cleared', 'cloakpilot' ), 'snoozed' => __( "We'll remind you in a week", 'cloakpilot' ), 'delete' => __( 'Delete', 'cloakpilot' ), 'revenueDeleteConfirm' => __( 'Delete this entry? The amount will be removed from the monthly total.', 'cloakpilot' ), 'revenueRecordAdded' => __( 'The record was added', 'cloakpilot' ), 'pasteOneRow' => __( 'One row updated', 'cloakpilot' ), // %s = מספר שורות. /* translators: %s: number of rows */ 'pasteManyRows' => __( '%s rows updated', 'cloakpilot' ), 'pasteUnmatched' => __( 'No vendor found named:', 'cloakpilot' ), 'pasteNoMatch' => __( 'No match was found in the table for any pasted name', 'cloakpilot' ), 'urlEditTitle' => __( 'Edit URL', 'cloakpilot' ), 'urlEditLabel' => __( 'URL', 'cloakpilot' ), 'urlEditHint' => __( 'The full URL, including all parameters.', 'cloakpilot' ), 'urlUpdated' => __( 'URL updated', 'cloakpilot' ), 'noVendor' => __( 'No vendor', 'cloakpilot' ), // %s = מספר נבחרים. /* translators: %s: number of selected items */ 'bulkSelected' => __( '%s selected', 'cloakpilot' ), // ---- מסך המוכרים (03/13) - נוספו במימוש הקוד, 22.8.2026 ---- // %1$s = מספר שורות מוצגות, %2$s = סך הכול. /* translators: 1: number of vendors shown, 2: total number of vendors */ 'vendorsShownOf' => __( '%1$s of %2$s vendors', 'cloakpilot' ), /* translators: %s: number of vendors */ 'vendorsAll' => __( '%s vendors', 'cloakpilot' ), /* translators: %s: number of characters left */ 'charsLeft' => __( '%s left', 'cloakpilot' ), 'utmInvalid' => __( 'Invalid format. Use key=value separated by & (for example utm_source=myblog&utm_medium=affiliate).', 'cloakpilot' ), 'unsavedConfirm' => __( 'You have unsaved changes to this vendor. Move to another vendor without saving?', 'cloakpilot' ), // %1$s = מספר כרטיסים חדשים, %2$s = מספר דומיינים שמצטרפים לכרטיס קיים. /* translators: 1: number of new vendor cards, 2: number of domains joining existing vendor cards */ 'reviewOutcome' => __( '%1$s new vendor cards will be created; %2$s domains will join existing vendor cards.', 'cloakpilot' ), 'reviewOutcomeNone' => __( 'No row is selected, so nothing will be created.', 'cloakpilot' ), // ---- ריצת הלילה 24.8.2026: ממצאי הבודק החיצוני ---- // 1: הסבר על כפתור "החל" שמושבת כשאין בחירה. 'bulkNeedsSelection' => __( 'Select links before running a bulk action', 'cloakpilot' ), // 2: מצב ריק לרשימת שורות גנרית ([data-rowlist]) כשחיפוש לא החזיר כלום. 'rowsNoMatchTitle' => __( 'Nothing matches this search', 'cloakpilot' ), 'rowsNoMatchBody' => __( 'No row in this list matches what you searched for. You can clear the search and see the full list again.', 'cloakpilot' ), 'rowsNoMatchVendors' => __( 'No vendor matches this search', 'cloakpilot' ), 'rowsNoMatchVendorsBody' => __( 'No vendor in this list matches what you searched for. You can clear the search and see all the vendors again.', 'cloakpilot' ), 'rowsClearSearch' => __( 'Clear the search', 'cloakpilot' ), // 5: הודעות ולידציה בעברית, ליד השדה, במקום הבועה של הדפדפן. 'vldRequired' => __( 'This field needs to be filled in', 'cloakpilot' ), 'vldFile' => __( 'A file needs to be chosen', 'cloakpilot' ), 'vldCheck' => __( 'This box needs to be ticked', 'cloakpilot' ), 'vldSelect' => __( 'An option needs to be picked', 'cloakpilot' ), 'vldUrl' => __( 'A full web address is needed, starting with https://', 'cloakpilot' ), 'vldEmail' => __( 'A valid email address is needed', 'cloakpilot' ), 'vldFormat' => __( 'The value is not in the right format', 'cloakpilot' ), 'vldPattern' => __( 'The value is not in the right format', 'cloakpilot' ), // %s = הערך המזערי / המרבי המותר. /* translators: %s: the minimum allowed value */ 'vldMin' => __( 'The value is lower than the minimum (%s)', 'cloakpilot' ), /* translators: %s: the maximum allowed value */ 'vldMax' => __( 'The value is higher than the maximum (%s)', 'cloakpilot' ), 'vldStep' => __( 'The value does not fall on one of the allowed steps', 'cloakpilot' ), 'vldShort' => __( 'The value is too short', 'cloakpilot' ), 'vldLong' => __( 'The value is too long', 'cloakpilot' ), // 4: אותה בדיקת טווח תאריכים שכבר קיימת בטופס הקישור. 'vldEndBeforeStart' => __( 'The end date is earlier than the start date', 'cloakpilot' ), // 6: שדה סכום כסף. 'vldMoney' => __( 'A positive number is needed, with up to two digits after the dot. Leaving it empty is fine too.', 'cloakpilot' ), // ---- בדיקת התצוגה 18.9.2026: מפתחות ש-admin-v2.js מבקש ולא היו כאן, // ולכן הממשק האנגלי הציג את הנפילה העברית (מסנן התאריך ברשימת הקישורים). // הבדיקה AdminV2L10nKeysTest משווה את המפה הזו לכל t('...') בקובץ. ---- 'allDates' => __( 'All dates', 'cloakpilot' ), 'dateFrom' => __( 'From date', 'cloakpilot' ), 'dateTo' => __( 'To date', 'cloakpilot' ), // תחילית קצרה לפני תאריך בודד בכפתור המסנן: "From 18.8.2026" / "Until 18.8.2026". 'dateFromShort' => _x( 'From ', 'date filter button, before a single date', 'cloakpilot' ), 'dateToShort' => _x( 'Until ', 'date filter button, before a single date', 'cloakpilot' ), 'clear' => _x( 'Clear', 'date filter: remove both dates', 'cloakpilot' ), 'unsavedLeaveAny' => __( 'This screen has unsaved changes. Leave without saving?', 'cloakpilot' ), ); } /** * מצב התצוגה שנבחר בהגדרות, מאומת. מקור אמת אחד לכל מי ששואל. */ public static function theme() { $theme = (string) self::get_option( 'ui_theme', 'light' ); return in_array( $theme, array( 'light', 'dark', 'auto' ), true ) ? $theme : 'light'; } /** * color-scheme על שורש המסמך: צובע את פס הגלילה של החלון עצמו. */ public function theme_color_scheme() { if ( ! CloakPilot_Locale::is_plugin_screen() ) { return; } $theme = self::theme(); if ( 'dark' === $theme ) { wp_add_inline_style( 'cloakpilot-admin-v2', 'html{color-scheme:dark}' ); } elseif ( 'auto' === $theme ) { wp_add_inline_style( 'cloakpilot-admin-v2', 'html{color-scheme:light dark}' ); } // קביעת data-theme לפני הצביעה הראשונה, כדי למנוע הבהוב של המצב הבהיר // לפני שהכהה נטען. הצבעים בגיליון תלויים ב-[data-theme="dark"] על שורש // המסמך, וה-attribute הזה נקבע עד היום רק ב-admin-v2.js שרץ בפוטר - כלומר // אחרי שהעמוד כבר נצבע בהיר (מוטי, 23.8.2026: "הבהוב של הגרסה הבהירה"). // admin_head רץ בתוך
, לפני , ולכן ה-attribute מוכן לצביעה // הראשונה. admin-v2.js קובע שוב את אותו ערך בהמשך - אידמפוטנטי, בלי הבהוב. // A script handle without a file, printed in (in_footer false), carries the line. wp_register_script( 'cloakpilot-theme', false, array(), CLOAKPILOT_VERSION, false ); wp_enqueue_script( 'cloakpilot-theme' ); if ( 'auto' === $theme ) { wp_add_inline_script( 'cloakpilot-theme', '(function(){try{var d=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches;document.documentElement.setAttribute("data-theme",d?"dark":"light");}catch(e){document.documentElement.setAttribute("data-theme","light");}})();' ); } else { wp_add_inline_script( 'cloakpilot-theme', 'document.documentElement.setAttribute("data-theme",' . wp_json_encode( $theme ) . ');' ); } } /** * מקצה גוון זהות לכל ספק שעדיין אין לו, לפי סדר יצירתו. * * לפי סדר ולא לפי crc32 של השם, כדי שהמצב הראשוני יהיה מפוזר על שמונת * הגוונים ולא מקובץ במקרה על שניים או שלושה. */ public static function assign_vendor_hues() { $ids = get_posts( array( 'post_type' => CloakPilot_Vendors::POST_TYPE, 'post_status' => 'any', 'posts_per_page' => -1, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ids', 'no_found_rows' => true, ) ); $i = 0; foreach ( $ids as $id ) { if ( '' !== get_post_meta( $id, '_cloakpilot_hue', true ) ) { continue; } update_post_meta( $id, '_cloakpilot_hue', ( $i % 8 ) + 1 ); $i++; } return $i; } /* ---------- אפשרויות ---------- */ public static function default_settings() { return array( 'default_redirect_type' => 301, 'link_prefix' => CLOAKPILOT_LINK_PREFIX, // 'go'. שינוי לא שובר קישורים שכבר פורסמו: הישנה נזכרת ב-link_prefix_history 'link_prefix_history' => array(), // כל התחיליות הקודמות; מנוע ההפניה עונה גם תחתן (ראה old_prefixes) 'max_autolinks' => 3, 'autolink_enabled' => 1, 'defer_to_thirsty' => 1, // בזמן ש-ThirstyAffiliates פעיל, CloakPilot לא חוטף את /go/ - מעבר אוטומטי בכיבוי Thirsty. 'ga4_events' => 0, 'broken_fallback' => 0, // קישור שבור: הפנה לדף הבית של הספק במקום להפיל את הגולש 'alert_email' => '', // מייל להתראות אנומליה (ריק = בלי מייל, ההתראות עדיין ב-REST/אדמין) 'ga4_measurement_id' => '', // G-XXXXXXX - לאירועי צד-שרת (Measurement Protocol) 'ga4_api_secret' => '', // GA4 Admin → Data Streams → Measurement Protocol API secrets 'health_enabled' => 1, 'author_read_access' => 0, // המתג הישן. נשאר לתאימות אחורה, ומתורגם ב-role_caps() 'role_caps' => array(), // טבלת ההרשאות: תפקיד => [view, edit] (מוטי, הערה 228) 'postback_key' => '', // המפתח הסודי בכתובת הפוסטבק (מוטי, הערה 209) 'default_vendor_currency' => 'USD', // מטבע פתיחה למוכר חדש (nmt9xezhwh6ry) 'ui_lang' => '', // שפת ממשק האדמין של CloakPilot (he/en) - קובעת גם RTL/LTR. ריק = עדיין לא נבחרה, ואז CloakPilot_UI::lang() נותן את שפת האתר (default_ui_lang, 18.9.2026) 'ui_theme' => 'light', // מצב תצוגה של מסכי CloakPilot: light / dark / auto 'revenue_goal' => 0, // יעד הכנסה חודשי (0 = לא הוגדר, ואז מסך הדוחות מציע להגדיר) 'report_currency' => 'usd', // מטבע התצוגה של יעד ההכנסה החודשי בלבד (usd/eur/ils) - תיוג בלבד, אינו ממיר שום סכום אחר באתר 'eur_usd_rate' => 1.08, // שער המרה EUR->USD, מוצג כהקשר ליד היעד. אין עדיין חישוב שמשתמש בו בפועל (ראה הערה במסך ההגדרות) 'setup_done' => 0, // אשף ההתקנה הושלם / דולג 'deals_colors' => array(), // overrides לצבעי רשת המבצעים (ראה CloakPilot_Deals::default_colors) ); } public static function get_settings() { $saved = get_option( self::OPTION_KEY, array() ); if ( ! is_array( $saved ) ) { $saved = array(); } return array_merge( self::default_settings(), $saved ); } /** * שפת הממשק של אתר שעדיין לא בחר שפה: לפי שפת האתר עצמו. אתר בעברית מקבל * עברית, כל אתר אחר אנגלית. עד beta.22 ברירת המחדל הייתה עברית תמיד, ובודק * מחוץ לישראל היה רואה את אשף ההתקנה והתפריט בעברית (נמצא בבדיקת התקנה על * וורדפרס נקי באנגלית, 18.9.2026). ההגדרה השמורה ui_lang גוברת תמיד; זו רק * ברירת המחדל כשאין אחת. * * @return string 'he' או 'en'. */ public static function default_ui_lang() { $locale = function_exists( 'get_locale' ) ? (string) get_locale() : ''; $short = strtolower( substr( $locale, 0, 2 ) ); // pt_PT מקבל את התרגום הברזילאי, כמו שוורדפרס עצמה נוהגת כשאין תרגום מקומי if ( class_exists( 'CloakPilot_Locale' ) && isset( CloakPilot_Locale::LOCALES[ $short ] ) ) { return $short; } return 'en'; } public static function get_option( $key, $default = null ) { $s = self::get_settings(); return array_key_exists( $key, $s ) ? $s[ $key ] : $default; } /** * תחילית ה-URL של הקישורים (/go/ כברירת מחדל). ניתנת להגדרה בהגדרות. * קבוע CLOAKPILOT_LINK_PREFIX ב-wp-config גובר (escape hatch). */ public static function prefix() { if ( defined( 'CLOAKPILOT_LINK_PREFIX_OVERRIDE' ) ) { return CLOAKPILOT_LINK_PREFIX_OVERRIDE; } $p = sanitize_title( (string) self::get_option( 'link_prefix', CLOAKPILOT_LINK_PREFIX ) ); return '' !== $p ? $p : CLOAKPILOT_LINK_PREFIX; } /** * כל התחיליות הקודמות שהאתר אי פעם שמר (בקשת מוטי, 5.9.2026, STATE.md סעיף 8 * פריט 14). קישור שיצא החוצה תחת /go/ ממשיך לעבוד גם אחרי שהתחילית הפכה * ל-/out/, כי מנוע ההפניה עונה תחת כל אחת מהן. הרשימה יושבת בתוך מערך * ההגדרות שכבר נטען בכל בקשה, ולכן הקריאה כאן לא מוסיפה שאילתה לנתיב החם. * התחילית הנוכחית לעולם לא ברשימה - גם כשהיא מגיעה מהקבוע העוקף ב-wp-config. * * @return string[] תחיליות מנוקות, ייחודיות, בלי הנוכחית. */ public static function old_prefixes() { $current = self::prefix(); $history = self::get_option( 'link_prefix_history', array() ); if ( ! is_array( $history ) || empty( $history ) ) { return array(); } $out = array(); foreach ( $history as $p ) { $p = sanitize_title( (string) $p ); if ( '' !== $p && $p !== $current && ! in_array( $p, $out, true ) ) { $out[] = $p; } } return $out; } /** * מעדכן את רשימת התחיליות הקודמות בתוך מערך הגדרות שעומד להישמר: מוסיף את * התחילית שיצאה משימוש, ומוציא מהרשימה את זו שנכנסת לשימוש (אם חוזרים * מ-/out/ ל-/go/, "go" יוצא מההיסטוריה כי הוא שוב הנוכחי). בלי כפילויות. * פונקציה טהורה - מי ששומר (מסך ההגדרות, האשף) קורא לה לפני הכתיבה. * * @param array $settings ההגדרות כפי שהן עומדות להישמר (עם link_prefix החדש). * @param string $old_prefix התחילית שהייתה בתוקף עד עכשיו. * @return array אותן הגדרות עם link_prefix_history מעודכן. */ public static function remember_old_prefix( array $settings, $old_prefix ) { $new_prefix = isset( $settings['link_prefix'] ) ? sanitize_title( (string) $settings['link_prefix'] ) : ''; $old_prefix = sanitize_title( (string) $old_prefix ); $history = isset( $settings['link_prefix_history'] ) && is_array( $settings['link_prefix_history'] ) ? $settings['link_prefix_history'] : array(); if ( '' !== $old_prefix ) { $history[] = $old_prefix; } $clean = array(); foreach ( $history as $p ) { $p = sanitize_title( (string) $p ); if ( '' !== $p && $p !== $new_prefix && ! in_array( $p, $clean, true ) ) { $clean[] = $p; } } $settings['link_prefix_history'] = $clean; return $settings; } public static function update_settings( array $values ) { $current = self::get_settings(); $merged = array_merge( $current, $values ); // שינוי תחילית דרך האשף עובר באותו מנגנון זיכרון כמו מסך ההגדרות. if ( isset( $values['link_prefix'] ) && sanitize_title( (string) $values['link_prefix'] ) !== sanitize_title( (string) $current['link_prefix'] ) ) { $merged = self::remember_old_prefix( $merged, (string) $current['link_prefix'] ); } update_option( self::OPTION_KEY, $merged ); return $merged; } /* ---------- זיהוי ThirstyAffiliates ---------- */ /** * האם ThirstyAffiliates פעיל כרגע. משמש כדי לא לחטוף את /go/ בזמן הרצה מקבילה. */ public static function is_thirsty_active() { if ( defined( 'THIRSTY_AFF_VERSION' ) || class_exists( 'ThirstyAffiliates' ) || function_exists( 'Thirsty_Affiliates' ) ) { return true; } if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return is_plugin_active( 'thirstyaffiliates/thirstyaffiliates.php' ); } /** * האם CloakPilot אמור לשרת redirects עכשיו. מכבד את defer_to_thirsty. */ public static function should_serve_redirects() { if ( self::get_option( 'defer_to_thirsty', 1 ) && self::is_thirsty_active() ) { return false; } return true; } /* ---------- Activation / Deactivation ---------- */ public static function activate() { // התקנה חדשה (בלי גרסת מסד נתונים) נאכפת מיד; אתר שעודכן מהגישה המוקדמת מקבל // תקופת חסד ב-plugins_loaded (CloakPilot_Plans::maybe_schedule_enforcement). require_once CLOAKPILOT_DIR . 'includes/class-cloakpilot-plans.php'; CloakPilot_Plans::mark_fresh_install( self::DB_VERSION_KEY ); require_once CLOAKPILOT_DIR . 'includes/class-cloakpilot-legacy.php'; CloakPilot_Legacy::maybe_migrate(); require_once CLOAKPILOT_DIR . 'includes/class-cloakpilot-clicks.php'; require_once CLOAKPILOT_DIR . 'includes/class-cloakpilot-positions.php'; CloakPilot_Clicks::create_table(); CloakPilot_Positions::create_table(); require_once CLOAKPILOT_DIR . 'includes/class-cloakpilot-feedback.php'; CloakPilot_Feedback::create_table(); CloakPilot_Feedback::backfill_screen_keys(); // דואג לאינדקס ה-day גם כשה-CREATE TABLE החדש (עם KEY day מובנה) לא מספיק - // dbDelta לא אמין להוספת KEY לטבלה שכבר קיימת (reactivate/reinstall על DB ישן // שנשאר; ה-uninstall השמרני ברירת המחדל לא מוחק טבלאות). אידמפוטנטי, בטוח // להריץ גם כשה-CREATE TABLE כבר יצר את האינדקס. self::maybe_add_day_index( CloakPilot_Clicks::impressions_table() ); self::maybe_add_day_index( CloakPilot_Clicks::product_impressions_table() ); self::record_schema_result(); if ( false === get_option( self::OPTION_KEY, false ) ) { update_option( self::OPTION_KEY, self::default_settings() ); } // רישום ה-CPT לפני flush כדי שכללי ה-rewrite ייכתבו. CloakPilot_CPT::register_post_type(); CloakPilot_CPT::register_taxonomy(); // FR-ARCH-001: flush_rewrite_rules() בונה את הכללים מחדש מכל התוספים הפעילים // כרגע ומריץ אותם דרך filter_out_foreign_prefix_rules() (רשום ב-__construct, // ראה שם) - כך ששריד rewrite שתואם את התחילית שלנו, גם אם הוא מגיע מתוסף // שעדיין פעיל בפועל (למשל ThirstyAffiliates מותקן-אך-כבוי), לא נכנס לכללים // שנשמרים. אין כאן קריאה נפרדת לניקוי: הפילטר הוא ההגנה, לא צעד ידני אחריו. // חשוב: נכונות ה-404 עצמה (class-cloakpilot-redirect.php) לא תלויה בהצלחת הניקוי הזה // בכלל - היא רצה ב-parse_request לפני שהשאילתה נפתרת, בלי קשר לכללי ה-rewrite. flush_rewrite_rules(); CloakPilot_CPT::rebuild_link_map(); // הפניה לאשף ההתקנה בכניסה הבאה לאדמין - רק בהתקנה טרייה (אין קישורים). if ( ! self::get_option( 'setup_done', 0 ) && 0 === (int) wp_count_posts( CloakPilot_CPT::POST_TYPE )->publish ) { set_transient( 'cloakpilot_activation_redirect', 1, MINUTE_IN_SECONDS ); } } public static function deactivate() { flush_rewrite_rules(); } /** * FR-ARCH-001: כשה-link_prefix בהגדרות משתנה, כללי ה-rewrite חייבים להתרענן כדי * שהתחילית החדשה תיכנס לתוקף. הניקוי עצמו (הסרת שרידים של תוספים אחרים על * התחילית) לא מתבצע כאן יותר בנפרד - filter_out_foreign_prefix_rules() למטה רשום * כפילטר קבוע על 'rewrite_rules_array' ורץ אוטומטית בכל regeneration, כולל זה * שה-flush הבא גורם לו, וקורא את התחילית הנוכחית בזמן אמת דרך self::prefix() - * אין צורך "לזכור" תחילית ישנה בנפרד. מחובר ל-update_option_cloakpilot_settings (הבנאי * למעלה), שרץ רק כשהערך המאוחסן באמת השתנה - שינוי בשדה אחר בהגדרות שלא נוגע * לתחילית לא מפעיל את זה בכלל. */ public static function maybe_flush_on_prefix_change( $old_value, $new_value ) { $old_prefix = isset( $old_value['link_prefix'] ) ? sanitize_title( (string) $old_value['link_prefix'] ) : ''; $new_prefix = isset( $new_value['link_prefix'] ) ? sanitize_title( (string) $new_value['link_prefix'] ) : ''; // גם שינוי ברשימת התחיליות הקודמות (link_prefix_history) מרענן, כי מנוע // ההפניה עונה תחת כולן. בפועל הרשימה משתנה רק יחד עם התחילית, אבל הבדיקה // כאן לא מניחה את זה. $old_history = isset( $old_value['link_prefix_history'] ) && is_array( $old_value['link_prefix_history'] ) ? array_values( $old_value['link_prefix_history'] ) : array(); $new_history = isset( $new_value['link_prefix_history'] ) && is_array( $new_value['link_prefix_history'] ) ? array_values( $new_value['link_prefix_history'] ) : array(); if ( ( '' === $old_prefix || $old_prefix === $new_prefix ) && $old_history === $new_history ) { return; } flush_rewrite_rules(); } /** * פילטר על 'rewrite_rules_array' (רשום כקבוע ב-__construct, לא רק באקטיבציה) - * מסיר מכללי ה-rewrite שנוצרים עכשיו כל כלל שהתבנית שלו מתחילה בתחילית הנוכחית. * CloakPilot עצמו לא תורם אף כלל rewrite לתחילית הזו - שלושת סוגי התוכן שלו רשומים * עם rewrite => false, ו-/go/{slug} מטופל ידנית ב-CloakPilot_Redirect::maybe_redirect(). * לכן כל כלל שנמצא כאן ותואם את התחילית הוא, בהגדרה, שריד של תוסף אחר (למשל * ThirstyAffiliates 3.12.0 מותקן-אך-כבוי - ראה FR-ARCH-001 בספק). * * למה פילטר ולא כתיבה ישירה ל-option('rewrite_rules') (הגרסה הקודמת, * REVIEW-SEC.md finding 6): כתיבה ישירה נמחקת על ידי ה-flush_rewrite_rules() * הבא של כל תוסף/תבנית/שמירת permalinks, שמבנה מחדש את ה-option מהכללים * הרשומים ומחזיר בדיוק את מה שהוסר. היא גם לא משפיעה על הבקשה הנוכחית, כי רק * ה-option משתנה בעוד $wp_rewrite->rules בזיכרון נשארים כפי שהיו. והיא הרסנית: * היא לא יכולה להבחין בין שריד מת לכלל פעיל שתוסף אחר עדיין תלוי בו. הפילטר הזה * פותר את שלושתם: הוא רץ בכל פעם שהכללים נבנים מחדש (ולכן עמיד, לא חד-פעמי), * הוא חלק מהבנייה עצמה (ולכן משפיע גם על הבקשה הנוכחית שקוראת ל-flush), והוא * הפיך לגמרי (remove_filter, בלי שום כתיבה ל-DB). זו הגנת עומק בלבד: נכונות * ה-404 בפועל (class-cloakpilot-redirect.php) לא תלויה בהצלחת הפעולה הזו כלל - היא רצה * ב-parse_request לפני שהשאילתה נפתרת, בלי קשר לכללי ה-rewrite שנשארו או הוסרו. */ public static function filter_out_foreign_prefix_rules( $rules ) { if ( ! is_array( $rules ) || empty( $rules ) ) { return $rules; } /* במצב ויתור ל-ThirstyAffiliates (defer_to_thirsty דולק והוא פעיל) הכללים תחת התחילית הם שלו והוא תלוי בהם: הוא מזהה קישור רק דרך כלל ה-rewrite שלו (Models/Rewrites_Redirection.php בקוד שלו). מחיקתם הייתה מפילה את /go/ אצל שני התוספים בבת אחת - CloakPilot מוותר וגם Thirsty אינו יכול להגיש (חוות דעת שנייה, 3.9.2026). ברגע שהוא מכובה, ה-flush הבא מנקה את השרידים כרגיל. */ if ( ! self::should_serve_redirects() ) { return $rules; } $prefix = self::prefix(); if ( '' === $prefix ) { return $rules; } // תואם תבנית שמתחילה ב-^ אופציונלי, אחריו התחילית, ואחריה סוף המחרוזת או // '/' (עם או בלי escape) - כך ש-"go" תואם אבל "google" (או כל תחילית אחרת // שמתחילה באותם תווים) לא. $pattern = '#^\^?' . preg_quote( $prefix, '#' ) . '(?:\\\\?/|$)#i'; foreach ( $rules as $regex => $query ) { if ( preg_match( $pattern, (string) $regex ) ) { unset( $rules[ $regex ] ); } } return $rules; } /** * מסך התוסף שאליו שייך סוג תוכן, או '' לסוג שאינו שלנו. */ /** * הורדת קובץ המקור לתרגום (מוטי, הערה 217). מגישים את ה-POT כפי שהוא, * בלי לייצר אותו מחדש: הוא נבנה בזמן האריזה ותמיד תואם לגרסה שרצה. */ public static function handle_download_pot() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'cloakpilot' ) ); } check_admin_referer( 'cloakpilot_download_pot' ); $file = CLOAKPILOT_DIR . 'languages/cloakpilot.pot'; if ( ! file_exists( $file ) ) { wp_die( esc_html__( 'The source translation file is missing from the plugin folder.', 'cloakpilot' ) ); } nocache_headers(); header( 'Content-Type: text/x-gettext-translation; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="cloakpilot.pot"' ); header( 'Content-Length: ' . filesize( $file ) ); readfile( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- הגשת קובץ להורדה. exit; } /** * העלאת תרגום מוכן (מוטי, הערה 217). נשמר בתיקיית השפות של וורדפרס ולא * בתוך התוסף, כדי שעדכון גרסה לא ימחק אותו. רק MO ו-PO, רק בשם שוורדפרס * באמת טוען, ורק למנהל אתר. */ public static function handle_upload_translation() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'cloakpilot' ) ); } check_admin_referer( 'cloakpilot_upload_translation' ); $back = admin_url( 'edit.php?post_type=' . CloakPilot_CPT::POST_TYPE . '&page=cloakpilot-settings&tab=interface' ); // Only the fields used below, each sanitized; the file itself goes through wp_handle_upload(). if ( ! isset( $_FILES['cloakpilot_translation']['error'], $_FILES['cloakpilot_translation']['name'], $_FILES['cloakpilot_translation']['size'] ) || UPLOAD_ERR_OK !== absint( $_FILES['cloakpilot_translation']['error'] ) ) { wp_safe_redirect( add_query_arg( 'cloakpilot_tr', 'err', $back ) ); exit; } $name = sanitize_file_name( sanitize_text_field( $_FILES['cloakpilot_translation']['name'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- $_FILES is never slashed by WordPress. $ext = strtolower( (string) pathinfo( $name, PATHINFO_EXTENSION ) ); /* שם הקובץ הוא מה שקובע איזו שפה נטענת, ולכן הוא נבדק ולא מנוחש: cloakpilot-%s