جعل عملية تنشيط المستخدمين متسقة عبر واجهات برمجة التطبيقات

"مصتحق أحمد"
جو ميدلي
جو ميدلي

لمنع النصوص البرمجية الضارة من إساءة استخدام واجهات برمجة التطبيقات الحساسة، مثل النوافذ المنبثقة وملء الشاشة وما إلى ذلك، تتحكّم المتصفحات في الوصول إلى واجهات برمجة التطبيقات هذه من خلال تفعيل المستخدم. تفعيل المستخدم هو حالة جلسة تصفّح في ما يتعلق بإجراءات المستخدم: تشير الحالة "نشط" عادةً إلى أنّ المستخدم يتفاعل مع الصفحة حاليًا أو أنّه أكمل تفاعلاً منذ تحميل الصفحة. إيماءة المستخدم هي مصطلح شائع ومضلِّل للفكرة نفسها. على سبيل المثال، لا تؤدي إيماءة التمرير السريع أو التحريك من قِبل المستخدم إلى تنشيط الصفحة، وبالتالي لا يتم تنشيطها من منظور النص البرمجي.

تظهر المتصفحات الرئيسية اليوم سلوكًا متفاوتًا على نطاق واسع حول كيفية تحكم عملية التفعيل من جانب المستخدم في واجهات برمجة التطبيقات المحظورة على عمليات التفعيل. أمّا في Chrome، فيعتمد التنفيذ على نموذج قائم على الرموز المميزة، وتبيّن أنّه معقد للغاية، وبالتالي لا يمكن تحديد سلوك متسق على مستوى جميع واجهات برمجة التطبيقات المحصورة بالتفعيل. على سبيل المثال، يسمح Chrome بالوصول غير الكامل إلى واجهات برمجة التطبيقات المرتبطة بالتفعيل من خلال طلبات postMessage() وsetTimeout()، ولم يكن تفعيل المستخدم متوافقًا مع التعهدات وXHR وتفاعل لوحة الألعاب وما إلى ذلك. وتجدُر الإشارة إلى أنّ بعض هذه الأخطاء شائعة ولكنها قديمة جدًا.

في الإصدار 72، يشحن Chrome الإصدار الثاني من "تفعيل المستخدم" الذي يتأكد من اكتمال إتاحة تفعيل المستخدم لجميع واجهات برمجة التطبيقات المحصورة بالتفعيل. يحلّ ذلك نقاط التناقض المذكورة أعلاه (وغيرها من التناقضات، مثل MessageChannels)، والتي نعتقد أنّها ستسهّل عملية تطوير الويب بشأن تفعيل المستخدمين. إضافةً إلى ذلك، توفّر طريقة التنفيذ الجديدة تنفيذًا مرجعيًا لمواصفات جديدة مُقترَحة تهدف إلى جمع كل المتصفّحات معًا على المدى الطويل.

كيف يعمل الإصدار 2 من تفعيل حساب المستخدم؟

تحافظ واجهة برمجة التطبيقات الجديدة على حالة تفعيل ثنائية البت في كل عنصر window في التسلسل الهرمي للإطارات: وحدة بت ثابتة لحالة تفعيل المستخدم السابقة (إذا كان الإطار قد رصد تفعيل المستخدم من قبل)، ووحدة بت مؤقتة للحالة الحالية (إذا رصد إطار عملية تفعيل للمستخدم خلال ثانية تقريبًا). لا تتم إعادة تعيين البت اللاصق أبدًا خلال عمر الإطار بعد تعيينه. يتم ضبط البت العابر على كل تفاعل يجريه المستخدم، وتتم إعادة ضبطه إما بعد انتهاء صلاحية (ثانية تقريبًا) أو من خلال طلب واجهة برمجة تطبيقات تستهلك قدرًا كبيرًا من التفعيل (مثل window.open()).

تجدر الإشارة إلى أنّ واجهات برمجة التطبيقات المختلفة المحظورة ببوابة، تعتمد على طريقة تفعيل المستخدمين بطرق مختلفة، مع العلم أنّ واجهة برمجة التطبيقات الجديدة لا تغيّر أيًا من السلوكيات الخاصة بواجهة برمجة التطبيقات هذه. على سبيل المثال، يُسمح بنافذة منبثقة واحدة فقط لكل عملية تفعيل من جانب المستخدم لأنّ window.open() يستخدم عملية تفعيل المستخدم كما كان في السابق، ويستمر تنفيذ Navigator.prototype.vibrate() إذا رصد إطار (أو أي من إطاراته الفرعية) إجراءً من قِبل المستخدم، وهكذا.

ما الذي سيتغيّر؟

  • ويرسخ الإصدار الثاني من تفعيل المستخدم فكرة رؤية تفعيل المستخدم عبر حدود الإطارات: سيفعِّل تفاعل المستخدم مع إطار معيّن الآن جميع الإطارات التي تحتوي على تلك الإطارات (وتلك الإطارات فقط) بغض النظر عن مصدرها. (في Chrome 72، لدينا حل بديل مؤقت لزيادة مستوى الرؤية لجميع الإطارات من المصدر نفسه. وسنزيل هذا الحل البديل عندما تتوفّر لدينا طريقة لتمرير تفعيل المستخدم صراحةً إلى الإطارات الفرعية.)
  • عند استدعاء واجهة برمجة تطبيقات محظورة للتفعيل من إطار تم تفعيلها ولكن من خارج رمز معالج الأحداث، ستعمل ما دامت حالة تفعيل المستخدم "نشطة" (على سبيل المثال، لم تنتهِ صلاحيتها أو لم يتم استهلاكها). قبل تفعيل الإصدار 2 من تفعيل المستخدم، سيفشل بدون أي شروط.
  • تندمج تفاعلات المستخدم المتعددة غير المستخدمة خلال الفاصل الزمني لانتهاء الصلاحية في عملية تفعيل واحدة متوافقة مع التفاعل الأخير.

أمثلة على التناسق في واجهات برمجة التطبيقات المحصورة بالتفعيل

في ما يلي مثالان يحتويان على نوافذ منبثقة (تم فتحها باستخدام window.open()) يوضّحان كيف يساعد الإصدار 2 من ميزة "تفعيل المستخدم" في الحفاظ على اتساق سلوك واجهات برمجة التطبيقات المحصورة بالتفعيل.

مكالمات setTimeout() المتسلسلة

هذا المثال من عرضنا التوضيحي لـ setTimeout(). إذا حاول معالِج click فتح نافذة منبثقة خلال ثانية واحدة، من المتوقّع أن ينجح هذا الإجراء بغض النظر عن كيفية "إنشاء" الرمز البرمجي للتأخير. يلبّي الإصدار الثاني من تفعيل المستخدمين هذا التوقّع، لذلك يفتح كل معالِجات الأحداث التالية نافذة منبثقة على click (مع تأخير لمدة 100 ملي ثانية):

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

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

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

وبدون الإصدار 2 من تنشيط المستخدم، يفشل معالج الحدث الثاني في جميع المتصفحات التي اختبرناها. (حتى الخطأ الأول فشل في بعض الحالات.)

مكالمات postMessage() عبر النطاقات

إليك مثال من عرضنا التوضيحي لـ postMessage(). لنفترض أنّ معالج 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 من تنشيط المستخدم، لا يمكن للإطار الرئيسي فتح نافذة منبثقة عند استلام الرسالة الثانية. حتى الرسالة الأولى تفشل إذا كانت "متسلسلة" إلى إطار آخر من مصادر متعددة (بمعنى آخر، إذا أعاد المتلقي الأول توجيه الرسالة إلى أخرى).

يتوافق هذا الإجراء مع الإصدار الثاني من "تفعيل المستخدمين"، سواء بالشكل الأصلي أو السلسلة.