הפעלה עקבית של משתמשים בכל ממשקי ה-API

Mustaq Ahmed
Joe Medley
Joe Medley

כדי למנוע מסקריפטים זדוניים לעשות שימוש לרעה בממשקי API רגישים כמו חלונות קופצים, מסך מלא וכו', הדפדפנים שולטים בגישה לממשקי ה-API האלה באמצעות הפעלת משתמשים. הפעלת משתמש היא המצב של סשן הגלישה ביחס לפעולות של המשתמשים: מצב 'פעיל' מרמז בדרך כלל על כך שהמשתמש מבצע אינטראקציה כרגע עם הדף, או שהוא השלים אינטראקציה מאז טעינת הדף. תנועת משתמש היא מונח פופולרי אך מטעה שקשור לאותו רעיון. לדוגמה, תנועת החלקה או החלקה של משתמש לא מפעילה דף, ולכן מנקודת מבט של התסריט, לא הפעלת משתמש.

בדפדפנים העיקריים כיום, יש התנהגות שונה באופן משמעותי לגבי האופן שבו הפעלת המשתמשים שולטת בממשקי ה-API המוגבלים להפעלה. ב-Chrome ההטמעה התבססה על מודל מבוסס אסימונים שהסתגל כי הוא מורכב מדי מכדי להגדיר התנהגות עקבית בכל ממשקי ה-API שמוגבלים להפעלה. לדוגמה, Chrome אפשר לגשת גישה חלקית לממשקי API עם הגבלת הפעלה באמצעות postMessage() וsetTimeout() קריאות; והפעלה של משתמשים לא נתמכה באמצעות Promises, XHR, אינטראקציה עם גיימפליי וכו'. שימו לב שחלק מהבאגים האלה פופולריים אבל ותיקים.

בגרסה 72, Chrome כולל את גרסה 2 של הפעלת המשתמש, כך שהזמינות של הפעלת משתמשים תהיה מלאה בכל ממשקי ה-API שמחייבים הפעלה. הפעולה הזו פותרת את חוסר העקביות שהוזכרו למעלה (ועוד כמה, כמו MessageChannels), שאנחנו סבורים שיקל על פיתוח האינטרנט סביב הפעלת משתמשים. בנוסף, בהטמעה החדשה אפשר למצוא קובצי עזר למפרט חדש, שמטרתו לאחד את כל הדפדפנים יחד בטווח הארוך.

איך פועלת גרסה 2 של הפעלת המשתמש?

ה-API החדש שומר על מצב הפעלה של משתמש בשתי ביט בכל אובייקט window בהיררכיית הפריימים: ביט דביק למצב ההפעלה ההיסטורי של המשתמש (אם אי פעם התרחשה הפעלה של משתמש בפריים), וביט זמני למצב הנוכחי (אם זוהתה הפעלה של משתמש תוך כשנייה בפריים). הביט הדביק אף פעם לא מתאפס במהלך משך החיים של הפריים לאחר הגדרתו. הביט הזמני מוגדר לכל אינטראקציה של משתמש, והוא מתאפס אחרי מרווח תפוגה (בערך שנייה) או באמצעות קריאה ל-API שצורך הפעלה (למשל, window.open()).

שימו לב שממשקי API שונים עם הגבלת הפעלה מסתמכים על הפעלת משתמשים בדרכים שונות. ה-API החדש לא משנה אף אחת מההתנהגויות האלה שספציפיות ל-API. למשל, בכל הפעלה של משתמש יכול להיות רק חלון קופץ אחד כי window.open() צורך הפעלה של משתמש כפי שהייתה בעבר, Navigator.prototype.vibrate() ממשיך לפעול אם פריים מסוים (או תת-מסגרות שלו) נצפה אי פעם פעולה של משתמש, וכן הלאה.

מה משתנה?

  • גרסה 2 של הפעלת המשתמש מפשטת את המשמעות של חשיפת ההפעלה של משתמשים בכל גבולות המסגרת: אינטראקציה של משתמש עם מסגרת מסוימת תפעיל עכשיו את כל הפריימים שמכילים (ורק את המסגרות האלה) ללא קשר למקור שלהם. (ב-Chrome 72 יש לנו פתרון זמני להרחבת החשיפה לכל הפריימים ממקור זהה. נסיר את הפתרון הזה ברגע שתהיה לנו דרך להעביר באופן מפורש את הפעלת המשתמש למסגרות משנה.)
  • כשנשלחת קריאה ל-API עם הגבלת הפעלה ממסגרת מופעלת, אבל מחוץ לקוד של הגורם המטפל באירועים, הוא יפעל כל עוד מצב ההפעלה של המשתמש הוא 'פעיל' (למשל, לא פג התוקף של הקוד הזה ולא נעשה בו שימוש). לפני הפעלת המשתמש גרסה 2, היא הייתה תיכשל ללא תנאי.
  • אינטראקציות מרובות של משתמשים שלא נמצאות בשימוש במהלך מרווח זמן התפוגה מתמזגות להפעלה אחת שתואמת לאינטראקציה האחרונה.

דוגמאות לעקביות בממשקי API עם הגבלות הפעלה

הנה שתי דוגמאות עם חלונות קופצים (שנפתחו באמצעות window.open()) שמראים איך הפעלת גרסה 2 של המשתמש הופכת את ההתנהגות של ממשקי API עם הגבלות הפעלה באופן עקבי.

setTimeout() שיחות כרוכות בשרשרת

הדוגמה הזו נלקחת מההדגמה של setTimeout(). אם handler של click מנסה לפתוח חלון קופץ תוך שנייה, הוא צפוי להצליח ללא קשר לאופן שבו הקוד "מחבר" את ההשהיה. גרסה 2 של הפעלת המשתמש עומדת בציפיות האלה, ולכן כל אחד מהגורמים המטפלים באירועים הבאים פותח חלון קופץ ב-click (בעיכוב של 100 אלפיות שנייה):

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

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

שיחות postMessage() בכמה דומיינים

הנה דוגמה מההדגמה של postMessage(). נניח ש-handler של click בתת-מסגרת ממקורות שונים שולח שתי הודעות ישירות למסגרת ההורה. כשמקבלים אחת מההודעות הבאות (אבל לא את שניהם): במסגרת ההורה צריכה להיות אפשרות לפתוח חלון קופץ:

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

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

אפשר להפעיל אותו גם בגרסה 2 של הפעלת המשתמש, גם בגרסה המקורית וגם בשרשרת.