# מניעת קריסה בלחיצה על קישור חיצוני מתוך תוסף

## הבעיה

תוספים שמציגים תוכן HTML חופשי בתוך `iframe` (למשל תוכן שמגיע מקובץ ZIM, מ-Markdown שהומר ל-HTML, מ-RSS וכו') חשופים למצב שבו התוכן מכיל קישורי `<a href="https://...">` לאתרים חיצוניים. כשמשתמש לוחץ על קישור כזה בתוך ה-`iframe`, הדפדפן/ה-WebView מנסה לנווט אליו — ובתוך סביבת ה-WebView של אוצריא (origin=null, בלי גישה לרשת חיצונית מהחלון הראשי) הניווט הזה לא "נכשל בשקט", אלא מוביל להתנהגות בלתי צפויה שקורסת את התוסף.

**דרך שגויה שנפוצה ולא עוזרת בפועל:** הוספת `target="_blank"` לקישורים חיצוניים, בתקווה ש"ה-sandbox יחסום את זה". בפועל, `target="_blank"` מפעיל נתיב קוד ברמת האפליקציה המארחת ("open new window"), ונתיב הזה הוא בדיוק מה שקורס. הפתרון הנכון הוא לא לנסות לנווט כלל — למנוע את הפעולה ברמת ה-JS ולהציג הודעה במקום.

## הפתרון

חסימת הניווט הישיר היא רק חצי מהפתרון — משתמשים לוחצים על קישורים כאלה גם בטעות (למשל חושבים שזה קישור לדף אחר בתוך הארכיון), אז חסימה שקטה או הודעת שגיאה גרידא מרגישה תקועה. הגישה הנוחה יותר: לשאול את המשתמש אם הוא בכלל מתכוון לצאת מהתוסף, ורק אם הוא מאשר — לפתוח את הקישור **בדפדפן המערכת** (לא בתוך ה-`iframe` ולא בחלון חדש בתוך האפליקציה) דרך ה-API הרשמי `app.openUrl`.

הרעיון: כשמעבדים את ה-HTML לפני הזרקתו ל-`iframe` (למשל דרך `srcdoc`), מסמנים כל קישור חיצוני בתכונת `data-*` ייעודית שמכילה את הכתובת המוחלטת שלו (ולא רק דגל בוליאני) — **בלי** לגעת ב-`target` ובלי להסיר את ה-`href` המקורי (כדי שריחוף עם העכבר עדיין יראה את הכתובת האמיתית). ואז, ב-listener אחד ל-`click` על ה-`document` של ה-`iframe` (בשלב `capture`, כדי לתפוס את הלחיצה לפני שהדפדפן מבצע ניווט), בודקים את התכונה הזו, מונעים את הניווט, ומציגים דיאלוג אישור; רק לחיצה על "אישור" פותחת בפועל.

### שלב 0 — דרישות במניפסט

`app.openUrl` דורש הרשאה `app.open_url` וגם `minAppVersion` של לפחות 0.9.95 (סקריפט האריזה הרשמי חוסם אריזה אם `minAppVersion` נמוך מהגרסה שבה ה-API שאתם קוראים לו הופיע — בדקו בטבלת הגרסאות ב-`API_REFERENCE.md`).

```json
"minAppVersion": "0.9.95",
"permissions": ["ui.feedback", "app.open_url"]
```

### שלב 1 — סימון קישורים חיצוניים בעת עיבוד ה-HTML, עם הכתובת המוחלטת

```js
doc.querySelectorAll('a[href]').forEach((el) => {
  const href = el.getAttribute('href');
  if (!href) return;
  if (href.startsWith('#')) return; // קישור עוגן בתוך הדף — להשאיר כמו שהוא

  // resolveInternalPath(href) הוא פונקציה משלכם שמחזירה נתיב פנימי אם
  // הקישור מפנה למשאב בתוך התוסף/הארכיון, או null אם הוא חיצוני
  // (למשל דרך new URL(href, base) ובדיקת ה-host)
  const internal = resolveInternalPath(href);
  if (!internal) {
    let abs = '';
    try {
      const u = new URL(href, 'http://internal.local/');   // בסיס סינתטי לפענוח קישורים יחסיים
      if (u.protocol === 'http:' || u.protocol === 'https:') abs = u.href;
    } catch (_) { /* קישור שלא ניתן לפענח (mailto:, javascript: וכו') */ }
    el.setAttribute('data-external-link', abs);   // ריק = אין מה לפתוח בדפדפן
    return;
  }

  // כאן ממשיכים בטיפול הרגיל בקישורים פנימיים...
});
```

### שלב 2 — חסימת הלחיצה + דיאלוג אישור + פתיחה בדפדפן המערכת

```js
async function confirmOpenExternal(url) {
  let hostname = url;
  try { hostname = new URL(url).hostname || url; } catch (_) {}
  const c = await Otzaria.call('ui.showConfirm', {
    title: 'קישור לאתר חיצוני',
    content: 'הקישור הזה מוביל אל האתר "' + hostname + '" — מחוץ לארכיון הפתוח ומחוץ לתוסף. ' +
             'האם לפתוח אותו בדפדפן המערכת שלכם?'
  }).catch(() => null);
  if (!c || !c.success || !c.data || c.data.confirmed !== true) return;   // ביטול = לא עושים כלום
  const r = await Otzaria.call('app.openUrl', { url: url }).catch(() => null);
  if (!r || !r.success) {
    Otzaria.call('ui.showError', { message: 'פתיחת הקישור נכשלה.' });
  }
}

iframe.contentDocument.addEventListener('click', (ev) => {
  let target = ev.target;
  while (target && target.tagName !== 'A') target = target.parentNode;
  if (!target) return;

  if (target.hasAttribute('data-external-link')) {
    ev.preventDefault();
    const url = target.getAttribute('data-external-link');
    if (url) confirmOpenExternal(url);
    else Otzaria.call('ui.showError', { message: 'קישור זה אינו נגיש מתוך התוסף.' });
    return;
  }

  // טיפול בקישורים פנימיים (כרגיל)...
}, true); // true = capture — קריטי, כדי לתפוס את הלחיצה לפני הניווט הדפדפני
```

זהו. אין צורך ב-`sandbox` מיוחד ב-`iframe`, אין צורך להסיר `href`, ואין צורך ב-`target="_blank"` בכלל — ה-`preventDefault()` ב-listener שרץ ב-capture חוסם את הניווט המקומי לחלוטין, ו-`app.openUrl` הוא הערוץ התקני והבטוח לפתיחה בפועל מחוץ לתוסף (מקבל רק כתובות `http`/`https`).

**אם `app.openUrl` לא זמין (גרסת אוצריא ישנה, או שהתוסף לא ביקש את ההרשאה):** אפשר עדיין להציג הודעת שגיאה פשוטה בלי אפשרות פתיחה — עדיף על קריסה, גם אם פחות נוח.

## הערה נלווית: פענוח קישורים עם תווים שמורים (`:`, `/` וכו')

אם אתם ממירים קישורים יחסיים לנתיב פנימי (למשל כדי להשוות למפתח בארכיון או ברשימת דפים), שימו לב **לא** להשתמש ב-`decodeURI()` לצורך זה — היא במתכוון *לא* מפענחת תווים שמורים כמו `:` ו-`/` (הם נשארים כ-`%3A`, `%2F` וכו'), כי היא נועדה לפענוח URI מלא ולא של מזהה בודד. אם כותרת התוכן מכילה נקודתיים (נפוץ מאוד בתוכן מבוסס-MediaWiki, למשל "פרשני:עמוד ראשי" או "קטגוריה:..."), הקישור לא יתאים לנתיב האמיתי והחיפוש יחזיר "לא נמצא". הפתרון: להשתמש ב-`decodeURIComponent()` במקום, שמפענחת את כל התווים.

```js
// שגוי — ':' יישאר '%3A' ולא יתאים לנתיב האמיתי
const path = decodeURI(url.pathname.replace(/^\//, ''));

// נכון
const path = decodeURIComponent(url.pathname.replace(/^\//, ''));
```
