اذهب إلى المحتوى

التخاطب بين نوافذ المتصفح عبر جافاسكربت


ابراهيم الخضور

تقيِّد سياسة الأصل المشترك same origin أو سياسة الموقع المشترك same site من إمكانية وصول النوافذ أو الإطارات إلى بعضها إن لم تكن ضمن الموقع نفسه، فلو فتح مستخدم صفحتين، بحيث الأولى مصدرها الموقع academy.hsoub.com، والأخرى مصدرها الموقع gmail.com؛ فلا نريد هنا بالطبع لأية شيفرة من الصفحة academy.hsoub.com أن تقرأ بريده الإلكتروني الذي تعرضه الصفحة gmail.com، فالغاية إذًا من سياسة الأصل المشترك هي حماية المستخدم من لصوص المعلومات.

الأصل المشترك

نقول أنّ لعنواني URL أصلًا مشتركًا إن كان لهما نفس البروتوكول، والنطاق domain، والمنفذ. لذا يمكن القول أنّ للعناوين التالية أصلًا مشتركًا:

بينما لا تشترك العناوين التالية مع السابقة بالأصل:

وتنص سياسة الأصل المشترك على مايلي:

  • إذا وُجِد مرجع إلى نافذة أخرى، مثل: النوافذ المنبثقة المُنشأة باستخدام الأمر window.open، أو الموجودة داخل وسم النافذة الضمنية <iframe>، وكان للنافذتين الأصل نفسه، فسيمنحنا ذلك إمكانية الوصول الكامل إلى تلك النافذة.
  • إذا لم يكن للنافذتين الأصل نفسه، فلا يمكن لإحداهما الوصول إلى محتوى الأخرى، بما في ذلك المتغيرات والمستندات، ويُستثنى من ذلك موضع النافذة location، إذ يمكننا تغييره (وبالتالي إعادة توجيه المستخدم)، دون القدرة على قراءة موضع النافذة (وبالتالي لا يمكننا معرفة مكان المستخدم الآن، وبالتالي لن تتسرب المعلومات).

العمل مع النوافذ الضمنية iframe

يستضيف الوسم iframe نافذةً ضمنيةً منفصلةً عن النافذة الأصلية، ولها كائنان، هما document وwindow مستقلان، ويمكن الوصول إليهما بالشكل التالي:

  • iframe.contentWindow للحصول على الكائن window الموجودة ضمن الإطار iframe.
  • iframe.contentDocument للحصول على الكائنdocument ضمن الإطار iframe، وهو اختصار للأمر iframe.contentWindow.document.

سيتحقق المتصفح أنّ للإطار والنافذة الأصل نفسه عندما نَلِج إلى شيء ما داخل نافذة مضمّنة، وسيرفض الوصول إن لم يتحقق ذلك (تُستنثنى هنا عملية تغيير الخاصية location فلا تزال مسموحة).

لنحاول مثلًا القراءة والكتابة إلى الإطار iframe من مورد لا يشترك معه بالأصل:

<iframe src="https://example.com" id="iframe"></iframe>

<script>
  iframe.onload = function() {
    // يمكننا الحصول على مرجع إلى النافذة الداخلية
    let iframeWindow = iframe.contentWindow; // صحيح
    try {
      // ...لكن ليس للمستند الموجود ضمنه
      let doc = iframe.contentDocument; // خطأ
    } catch(e) {
      alert(e); // (خطأ أمان (أصل مختلف 
    }

    // لايمكننا أيضًا قراءة العنوان ضمن الإطار
    try {
      //  Location لا يمكن قراءة عنوان الموقع من كائن الموضع
      let href = iframe.contentWindow.location.href; // خطأ
    } catch(e) {
      alert(e); // Security Error
    }
    // يمكن الكتابة ضمن خاصية الموضع وبالتالي تحميل شيء آخر ضمن الإطار

    iframe.contentWindow.location = '/'; // صحيح

    iframe.onload = null; //امسح معالج الحدث، لا تنفذه بعد تغيير الموضع

  };
</script>

ستعطي الشيفرة السابقة أخطاءً عند تنفيذها عدا حالتي:

  • الحصول على مرجع إلى النافذة الداخلية iframe.contentWindow فهو أمر مسموح.
  • تغيير الموضع location.

وبالمقابل، إن كان للإطار iframe نفس أصل النافذة الداخلية، فيمكننا أن نفعل فيها ما نشاء:

<!-- iframe from the same site -->
<iframe src="/" id="iframe"></iframe>

<script>
  iframe.onload = function() {
    // افعل ما تشاء
    iframe.contentDocument.body.prepend("Hello, world!");
  };
</script>

الموازنة بين الحدثين iframe.onload وiframe.contentWindow.onload

يؤدي الحدث iframe.onload على الوسم iframe مبدئيًا، نفس ما يؤديه الحدث iframe.contentWindow.onload على كائن النافذة المضمنة، إذ يقع الحدث عندما تُحمّل النافذة المضمنة بكل مواردها، لكن لا يمكن الوصول إلى الحدث iframe.contentWindow.onload لإطار من آخر ذي أصل مختلف، وبالتالي سنستخدم عندها iframe.onload.

النوافذ ضمن النطاقات الفرعية والخاصية document.domain

وجدنا من التعريف أنّ العناوين المختلفة تقود إلى موارد غير مشتركة بالأصل، لكن لو تشاركت النوافذ بالنطاق الفرعي أو النطاق من المستوى الثاني -مثل النطاقين الفرعيين التاليين: john.site.com وpeter.site.com (المشتركان بالنطاق الفرعي site.com)-، فيمكن حينها أن نطلب من المتصفح أن يتجاهل الفرق، وبالتالي سيعدهما من أصل مشترك، وذلك لأغراض التخاطب بين النوافذ.

ولكي ننفذ ذلك لا بدّ من وجود الشيفرة التالية في كل نافذة:

document.domain = 'site.com';

ستتخاطب النوافذ مع بعضها الآن دون معوقات، ولا بد أن نذكر أن هذا ممكن فقط في الصفحات التي تشترك بنطاق من المستوى الثاني.

مشكلة كائن "document" خاطئ في النوافذ الضمنية

عندما نتعامل مع نافذة ضمنية لها الأصل نفسه، يمكننا الوصول إلى الكائن document الخاص بها، وعندها ستواجهنا مشكلة لا تتعلق بالأصل المشترك -لكن لا بدّ من الإشارة إليها-، وهي أن النافذة الضمنية سيكون لها كائن document مباشرةً بعد إنشائها، لكنه سيختلف عن الكائن الذي سيُحمَّل ضمنها، وبالتالي قد يضيع الأمر الذي ننفِّذه على الكائن document مباشرةً. لنتأمل الشيفرة التالية:

<iframe src="/" id="iframe"></iframe>

<script>
  let oldDoc = iframe.contentDocument;
  iframe.onload = function() {
    let newDoc = iframe.contentDocument;
    // كائن المستند الذي حُمِّل مختلف عن الكائن الأساسي
    alert(oldDoc == newDoc); // false النتيجة 
  };
  </script>

لا ينبغي التعامل مع الكائن document لأي إطار قبل التحميل الكامل لمحتوياته لتجنب التعامل مع الكائن الخاطئ، إذ سيتجاهل المتصفح أية معالجات أحداث قد نستخدمها ضمن الكائن.

لكن كيف نحدد اللحظة التي يجهز فيها الكائن document؟ سيظهر المستند بالتأكيد عند وقوع الحدث iframe.onload، والذي لا يطلق إلا عندما تُحمَّل موارد الإطار كلها. كما يمكن التقاط اللحظة المناسبة بالتحقق المستمر خلال فترات زمنية محددة عبر التعليمة setInterval:

<iframe src="/" id="iframe"></iframe>

<script>
  let oldDoc = iframe.contentDocument;

  // تحقق أن المستند الجديد جاهز كل 100 ميلي ثانية
  let timer = setInterval(() => {
    let newDoc = iframe.contentDocument;
    if (newDoc == oldDoc) return;

    alert("New document is here!");

    clearInterval(timer); //   فلن تحتاجها الآن setInterval  ألغ  
  }, 100);
</script>

استخدام المجموعة window.frames

يمكن استخدام طريقة بديلة للوصول إلى الكائن window الخاص بالنافذة الضمنية iframe عبر المجموعة window.frames:

  • باستخدام الرقم: إذ سيعطي الأمر [window.frames[0 كائن window الخاص بأول إطار في المستند.
  • باستخدام الاسم: إذ يعطي الأمر window.frames.iframeName كائن window الخاص بالإطار الذي يحمل الاسم "name="iframeName.

مثلًا:

<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>

<script>
  alert(iframe.contentWindow == frames[0]); // true
  alert(iframe.contentWindow == frames.win); // true
</script>

قد تحوي نافذة ضمنية ما نوافذ ضمنيةً أخرى، وعندها ستكون الكائنات window الناتجة عن الانتقال الشجري بين الإطارات هي:

  • window.frames: وتمثل مجموعة النوافذ الأبناء للإطارات المتداخلة nested.
  • window.parent: المرجع إلى النافذة الأم المباشرة (الخارجية).
  • window.top: المرجع إلى النافذة الأم الأعلى ترتيبًا.

مثلًا:

window.frames[0].parent === window; // محقق

يمكن استخدام الخاصية top للتحقق من أنّ المستند الحالي مفتوح ضمن إطار أم لا:

if (window == top) { // current window == window.top?
  alert('The script is in the topmost window, not in a frame');
} else {
  alert('The script runs in a frame!');
}

الخاصية sandbox المتعلقة بالنافذة الضمنية iframe

تسمح الخاصية sandbox باستبعاد تنفيذ أفعال معينة داخل النافذة الضمنية <iframe>، وذلك لمنع تنفيذ الشيفرة غير الموثوقة، إذ تقيَّد الخاصية sandbox للإطار كونه آتيًا من أصل مختلف مع/أو بالإضافة إلى قيود أخرى.

تتوفر عدة تقييدات افتراضية ضمن الأمر <"..."=iframe sandbox src>، ويفضل أن نترك فراغًا بين عناصر قائمة القيود التي لا نريد تطبيقها كالتالي:

<iframe sandbox="allow-forms allow-popups">

ستطبّق أكثر التقييدات صرامةً عندما تكون الخاصية sandbox فارغة "دون قائمة قيود" ، ويمكننا رفع ما نشاء منها بذكرها ضمن قائمة يفصل بين عناصرها فراغات.

وإليك قائمة الخيارات المتعلقة بالتقييدات:

  • allow-same-origin: تجبر الخاصية sandbox المتصفح على احتساب النافذة الضمنية من أصل مختلف تلقائيًا، حتى لو دلت الصفة src على الأصل نفسه، لكن هذا الخيار يزيل التطبيق الافتراضي لهذه الميزة.
  • allow-top-navigation: يسمح هذا الخيار للنافذة الضمنية بتغيير قيمة parent.location.
  • allow-forms: يسمح هذا الخيار بإرسال نماذج forms انطلاقًا من الإطار.
  • allow-scripts: يسمح هذا الخيار بتشغيل سكربتات انطلاقًا من الإطار.
  • allow-popups: يسمح بفتح نوافذ منبثقة باستخدام الأمر window.open انطلاقًا من الإطار.

اطلع على تعليمات التنفيذ إن أردت التعرف على المزيد.

يُظهر المثال التالي إطارًا مقيَّدًا بمجموعة القيود الافتراضية <"..."=iframe sandbox src>، ويتضمن المثال شيفرةً ونموذجًا وملفين، وستلاحظ أنّ الشيفرة لن تعمل نظرًا لصرامة القيود الافتراضية.

الشيفرة الموجودة في الملف "index.html":

<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  <div>The iframe below has the <code>sandbox</code> attribute.</div>

  <iframe sandbox src="sandboxed.html" style="height:60px;width:90%"></iframe>

</body>
</html>

وفي الملف "sandbox.html":

<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  <button onclick="alert(123)">Click to run a script (doesn't work)</button>

  <form action="http://google.com">
    <input type="text">
    <input type="submit" value="Submit (doesn't work)">
  </form>

</body>
</html>

وستكون النتيجة:

ملاحظة: إنّ الغاية من وجود الصفة sandbox هي إضافة المزيد من القيود فقط، إذ لا يمكنها إزالة هذه القيود، لكنها تخفف بعض القيود المتعلقة بالأصل المشترك إن لم يكن للإطار الأصل نفسه.

تبادل الرسائل بين النوافذ

تسمح واجهة التخاطب postMessage للنوافذ بالتخاطب مع بعضها أيًا كان أصلها، فهي إذًا أسلوب للالتفاف على سياسة "الأصل المشترك"، إذ يمكن لنافذة من النطاق academy.hsoub.comأن تتخاطب مع النافذةgmail.com` وتتبادل المعلومات معها، لكن بعد موافقة كلتا النافذتين ووجود دوال JavaScript الملائمة فيهما، وهذا بالطبع أكثر أمانًا للمستخدم.

تتكون الواجهة من قسمين رئيسيين:

القسم المتعلق بالتابع postMessage

تستدعي النافذة التي تريد إرسال رسالةٍ التابع postMessage العائد للنافذة الهدف، أي إن أردنا إرسال رسالة إلى win، فعلينا تنفيذ الأمر (win.postMessage(data, targetOrigin، وإليك توضيح وسطاء هذا التابع:

  • data: وهي البيانات المراد إرسالها، والتي قد تكون أي كائن. حيث تُنسخ البيانات باستخدام خوارزمية التفكيك البنيوي "structured serialization". لا يدعم المتصفح Internet Explorer سوى النصوص، لذلك لا بدّ من استخدام التابع JSON.stringify مع الكائنات المركبة التي سنرسلها لدعم هذا المتصفح.
  • targetOrigin: يُحدد أصل النافذة الهدف، وبالتالي ستصل الرسالة إلى نافذة من أصل محدد فقط، ويعَدّ هذا مقياسًا للأمان، فعندما تأتي النافذة الهدف من أصل مختلف، فلا يمكن للنافذة المرسِلة قراءة موقعها location، ولن نعرف بالتأكيد الموقع الذي سيُفتح حاليًا في النافذة المحددة، وقد ينجرف المستخدم بعيدًا دون علم النافذة المرسِلة. إذًا سيضمن تحديد الوسيط targetOrigin أن النافذة الهدف ستستقبل البيانات ما دامت في الموقع الصحيح، وهذا ضروري عندما تكون البيانات على درجة من الحساسية.

في هذا المثال ستستقبل النافذة الرسالة إن كان مستندها (الكائن document الخاص بها) منتميًا إلى الموقع "http://example.com":

<iframe src="http://example.com" name="example">

<script>
  let win = window.frames.example;

  win.postMessage("message", "http://example.com");
</script>

يمكن إلغاء التحقق من الأصل بإسناد القيمة "*" إلى الوسيط targetOrigin:

<iframe src="http://example.com" name="example">

<script>
  let win = window.frames.example;

  win.postMessage("message", "*");
</script>

القسم المتعلق بالحدث onmessage

ينبغي أن تمتلك النافذة التي ستستقبل الرسالة، معالجًا للحدث message، والذي يقع عند استدعاء التابع postMessage (ويجري التحقق بنجاح من قيمة الوسيط targetOrigin).

لكائن الحدث خصائص مميزة، هي:

  • data: وهي البيانات التي يحضرها التابع postMessage.
  • origin: أصل المرسل، مثلًا: http://javascript.info.
  • source: المرجع إلى النافذة المرسِلة، إذ يمكننا الرد مباشرة بالشكل التالي (...)source.postMessage إن أردنا.

ولتحديد معالج الحدث السابق لا بدّ من استخدام addEventListener، إذ لن يعمل الأمر دونه، فمثلًا:

window.addEventListener("message", function(event) {
  if (event.origin != 'http://javascript.info') {
    // كائن من نطاق مختلف، سنتجاهله
    return;
  }

  alert( "received: " + event.data );

  // يمكن إعادة الإرسال باستخدام event.source.postMessage(...)
});

يتكون المثال من الملف "iframe.html":

<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  Receiving iframe.
  <script>
    window.addEventListener('message', function(event) {
      alert(`Received ${event.data} from ${event.origin}`);
    });
  </script>

</body>
</html>

والملف "index.html":

<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  <form id="form">
    <input type="text" placeholder="Enter message" name="message">
    <input type="submit" value="Click to send">
  </form>

  <iframe src="iframe.html" id="iframe" style="display:block;height:60px"></iframe>

  <script>
    form.onsubmit = function() {
      iframe.contentWindow.postMessage(this.message.value, '*');
      return false;
    };
  </script>

</body>
</html>

وستكون النتيجة كالتالي:

الخلاصة

لاستدعاء التوابع التي تسمح بالوصول إلى نوافذ أخرى، لا بدّ من وجود مرجع إليها، حيث نحصل على هذا المرجع بالنسبة للنوافذ المنبثقة باستخدام:

  • window.open: يُنفَّذ ضمن النافذة الأساسية، وسيفتح نافذة جديدة ويعيد مرجعًا إليها.
  • window.opener: يُنفّذ ضمن النافذة المنبثقة، ويعطي مرجعًا إلى النافذة التي أنشأتها.

يمكن الوصول إلى النوافذ الآباء والأبناء ضمن النوافذ الضمنية <iframe> بالشكل التالي:

  • window.frames: تمثل مجموعة من الكائنات window المتداخلة.
  • window.parent وwindow.top: يمثلان مرجعين إلى النافذة الأم المباشرة والنافذة الأم العليا.
  • iframe.contentWindow: تمثل النافذة الموجودة داخل وسم النافذة الضمنية iframe.

إذا كان لنافذتين الأصل نفسه (البرتوكول والمنفذ والنطاق)، فيمكنهما التحكم ببعضهما كليًا أما إن لم يكن لهما أصل مشترك، فيمكن إجراء مايلي:

  • تغيير موقع location نافذة أخرى (سماحية للكتابة فقط).
  • إرسال رسالة إليها.

لكن توجد بعض الاستثناءات:

  • إذا كان لنافذتين النطاق الفرعي (نطاق من المستوى الثاني) ذاته، مثلًا: a.site.com وb.site.com، فيمكن أن ندفع المتصفح لأن يعدّهما من الأصل ذاته بإضافة الأمر 'document.domain='site.com' في كلتيهما.
  • إن امتلك الإطار الخاصية sandbox، فسيُعَدّ قسرًا من أصل مختلف، إلا في الحالة التي نستخدم فيها القيمة allow-same-origin لهذه الخاصية، ويستخدَم ذلك لتشغيل شيفرة غير موثوقة ضمن نافذة ضمنية على الموقع نفسه.

تسمح الواجهة postMessage لنافذتين -ليس لهما بالضرورة الأصل ذاته- بالتخاطب مع بعضهما، حيث:

  1. تستدعي النافذة المرسِلة التابع (targetWin.postMessage(data, targetOrigin
  2. إن لم يكن للوسيط targetOrigin القيمة "*"، فسيتحقق المتصفح من أن للنافذة الهدف نفس الأصل المحدد كقيمة له.
  3. إن كان للنافذتين نفس الأصل أو لم يأخذ الوسيط targetOrigin القيمة "*"، فسيقع الحدث message الذي يمتلك الخصائص التالية:
  • origin: نفس أصل النافذة المرسِلة مثل "http://my.site.com".
  • source: وهو المرجع إلى النافذة المرسِلة.
  • data: البيانات المرسَلة، ويمكن أن تكون كائنًا من أي نوع، عدا في المتصفح Internet Explorer الذي لا يقبل سوى الكائنات النصية.

ولا بدّ لنا أيضًا من استخدام addEventListener لضبط معالج هذا الحدث داخل النافذة الهدف.

ترجمة -وبتصرف- للفصل cross-window communication من سلسلة The Modern JavaScript Tutorial.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

بتاريخ On 23/06/2021 at 17:55 قال واثق الشويطر:

هناك خطأ في الرابط عند

 

مرحبًا أخي @واثق الشويطر،

نشكرك على اهتمامك بما يُنشر بالأكاديمية، كما نشكرك على التنبيه إلى المشكلة التي صادفتك. لقد كان ثمة خلل على مستوى الرابط، وقد صُحِّح للتو.

نتمنى لك تجربة قراءة جيدة، والحصول على الفائدة المرجوة من المقال.

رابط هذا التعليق
شارك على الشبكات الإجتماعية



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...