جارٍ إزالة حظر الوصول إلى الحافظة

إمكانية وصول النصوص والصور إلى الحافظة بطريقة أكثر أمانًا وبدون حظر

كانت الطريقة التقليدية للوصول إلى حافظة النظام هي من خلال document.execCommand() للتفاعلات مع الحافظة. على الرغم من اعتمادها على نطاق واسع، فإن طريقة القص واللصق هذه كانت مكلفًا، فإن الوصول إلى الحافظة كان متزامنًا، ولا يمكنه القراءة والكتابة إلا في DOM.

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

تعالج واجهة برمجة تطبيقات الحافظة غير المتزامنة هذه المشاكل عن طريق توفير نموذج أذونات محدد جيدًا ولا يحظر الصفحة. تقتصر واجهة برمجة التطبيقات Async Clipboard API على معالجة النصوص والصور في معظم المتصفحات، ولكن تختلف إتاحة هذه الواجهة. احرص على دراسة النظرة العامة حول توافق المتصفح بعناية في كل قسم من الأقسام التالية.

النسخ: كتابة البيانات في الحافظة

writeText()

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

async function copyPageUrl() {
  try {
    await navigator.clipboard.writeText(location.href);
    console.log('Page URL copied to clipboard');
  } catch (err) {
    console.error('Failed to copy: ', err);
  }
}

التوافق مع المتصفح

  • 66
  • 79
  • 63
  • 13.1

المصدر

() write

في الواقع، writeText() هي طريقة ملائمة لطريقة write() العامة، والتي تتيح لك أيضًا نسخ الصور إلى الحافظة. مثل writeText()، إنّها غير متزامنة وتعرض وعدًا.

لكتابة صورة في الحافظة، تحتاج إلى الصورة على أنّها blob. وإحدى الطرق المتاحة لتنفيذ ذلك هي طلب الصورة من خادم باستخدام fetch()، ثم استدعاء blob() عند الاستجابة.

قد لا يكون طلب صورة من الخادم مرغوبًا أو ممكنًا لأسباب متنوعة. لحسن الحظ، يمكنك أيضًا رسم الصورة إلى لوحة واستدعاء طريقة toBlob() للوحة الرسم.

بعد ذلك، مرر مصفوفة من كائنات ClipboardItem كمعلمة إلى الطريقة write(). في الوقت الحالي، يمكنك تمرير صورة واحدة فقط في كل مرة، ولكننا نأمل أن نتمكّن من إتاحة استخدام صور متعددة في المستقبل. يأخذ ClipboardItem كائنًا من نوع MIME للصورة كمفتاح والكائن blob كقيمة. بالنسبة إلى كائنات blob التي تم الحصول عليها من fetch() أو canvas.toBlob()، تحتوي السمة blob.type تلقائيًا على نوع MIME الصحيح لصورة.

try {
  const imgURL = '/images/generic/file.png';
  const data = await fetch(imgURL);
  const blob = await data.blob();
  await navigator.clipboard.write([
    new ClipboardItem({
      // The key is determined dynamically based on the blob's type.
      [blob.type]: blob
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

يمكنك بدلاً من ذلك كتابة وعد في الكائن ClipboardItem. بالنسبة لهذا النمط، تحتاج إلى معرفة نوع MIME للبيانات مسبقًا.

try {
  const imgURL = '/images/generic/file.png';
  await navigator.clipboard.write([
    new ClipboardItem({
      // Set the key beforehand and write a promise as the value.
      'image/png': fetch(imgURL).then(response => response.blob()),
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

التوافق مع المتصفح

  • 66
  • 79
  • 13.1

المصدر

حدث النسخ

في حال بدأ المستخدم نسخة من الحافظة ولم يطلب preventDefault()، يتضمّن حدث copy سمة clipboardData مع العناصر بالتنسيق الصحيح. إذا كنت تريد تنفيذ منطقك الخاص، عليك استدعاء preventDefault() لمنع السلوك التلقائي لصالح التنفيذ الخاص بك. وفي هذه الحالة، سيكون الحقل "clipboardData" فارغًا. بالنظر إلى صفحة تحتوي على نص وصورة، وعندما يختار المستخدم الكل ويبدأ نسخة من الحافظة، ينبغي أن يتجاهل الحل المخصّص النص وينسخ الصورة فقط. يمكنك تحقيق ذلك كما هو موضح في نموذج التعليمات البرمجية أدناه. ما لم يتم تناوله في هذا المثال هو كيفية الرجوع إلى واجهات برمجة التطبيقات السابقة عندما لا تكون واجهة برمجة تطبيقات الحافظة متاحة.

<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
  // Prevent the default behavior.
  e.preventDefault();
  try {
    // Prepare an array for the clipboard items.
    let clipboardItems = [];
    // Assume `blob` is the blob representation of `kitten.webp`.
    clipboardItems.push(
      new ClipboardItem({
        [blob.type]: blob,
      })
    );
    await navigator.clipboard.write(clipboardItems);
    console.log("Image copied, text ignored.");
  } catch (err) {
    console.error(err.name, err.message);
  }
});

بالنسبة إلى حدث "copy":

التوافق مع المتصفح

  • 1
  • 12
  • 22
  • 3

المصدر

بالنسبة إلى اللغة ClipboardItem:

التوافق مع المتصفح

  • 76
  • 79
  • 13.1

المصدر

اللصق: قراءة البيانات من الحافظة

readText()

لقراءة النص من الحافظة، اتصل بـ navigator.clipboard.readText() وانتظِر حتى يتم حل الوعد الذي تم إرجاعه:

async function getClipboardContents() {
  try {
    const text = await navigator.clipboard.readText();
    console.log('Pasted content: ', text);
  } catch (err) {
    console.error('Failed to read clipboard contents: ', err);
  }
}

التوافق مع المتصفح

  • 66
  • 79
  • 13.1

المصدر

read()

تعتبر طريقة navigator.clipboard.read() أيضًا غير متزامنة وتعرض واعدًا. لقراءة صورة من الحافظة، احصل على قائمة بكائنات ClipboardItem، ثم نفِّذ التكرار فوقها.

يمكن أن تحتفظ كل ClipboardItem بمحتوياتها بأنواع مختلفة، لذا ستحتاج إلى التكرار على قائمة الأنواع، باستخدام التكرار الحلقي for...of. بالنسبة إلى كل نوع، استدعِ الطريقة getType() مع النوع الحالي كوسيطة للحصول على كائن فقاعة البيانات المقابل. وعلى النحو السابق، لا ترتبط هذه التعليمة البرمجية بالصور، وستعمل مع أنواع الملفات الأخرى في المستقبل.

async function getClipboardContents() {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      for (const type of clipboardItem.types) {
        const blob = await clipboardItem.getType(type);
        console.log(URL.createObjectURL(blob));
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
}

التوافق مع المتصفح

  • 66
  • 79
  • 13.1

المصدر

التعامل مع الملفات التي تم لصقها

من المفيد للمستخدمين أن يتمكنوا من استخدام اختصارات لوحة مفاتيح الحافظة، مثل ctrl+c وctrl+v. يعرض Chromium ملفات للقراءة فقط في الحافظة كما هو موضح أدناه. يتم تشغيل هذا الإجراء عندما ينقر المستخدم على اختصار اللصق التلقائي في نظام التشغيل أو عندما ينقر المستخدم على تعديل ثم لصق في شريط القوائم في المتصفّح. ليست هناك حاجة إلى أي تعليمات برمجية إضافية حول السباكة.

document.addEventListener("paste", async e => {
  e.preventDefault();
  if (!e.clipboardData.files.length) {
    return;
  }
  const file = e.clipboardData.files[0];
  // Read the file's contents, assuming it's a text file.
  // There is no way to write back to it.
  console.log(await file.text());
});

التوافق مع المتصفح

  • 3
  • 12
  • 3.6
  • 4

المصدر

حدث اللصق

كما ذكرنا سابقًا، هناك خطط لتوفير أحداث تتوافق مع Clipboard API، ولكن يمكنك حاليًا استخدام حدث paste الحالي. فهي تعمل بشكل جيد مع الطرق الجديدة غير المتزامنة لقراءة نص الحافظة. كما هي الحال في فعالية copy، لا تنسَ الاتصال برقم preventDefault().

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  const text = await navigator.clipboard.readText();
  console.log('Pasted text: ', text);
});

التوافق مع المتصفح

  • 1
  • 12
  • 22
  • 3

المصدر

التعامل مع أنواع MIME متعدّدة

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

يوضح المثال التالي كيفية القيام بذلك. يستخدم هذا المثال السمة fetch() للحصول على بيانات الصور، ولكن قد يكون مصدرها أيضًا <canvas> أو File System Access API.

async function copy() {
  const image = await fetch('kitten.png').then(response => response.blob());
  const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
  const item = new ClipboardItem({
    'text/plain': text,
    'image/png': image
  });
  await navigator.clipboard.write([item]);
}

الأمان والأذونات

لطالما كان الوصول إلى الحافظة مخاوف أمنية للمتصفّحات. وبدون الأذونات المناسبة، يمكن للصفحة نسخ جميع أنواع المحتوى الضار بشكل غير ملحوظ إلى حافظة المستخدم، ما قد ينتج عنه نتائج كارثية عند لصقها. تخيل صفحة ويب تنسخ rm -rf / أو صورة قنبلة فك ضغط إلى الحافظة.

إشعار في المتصفِّح يطلب من المستخدم الحصول على إذن الحافظة
طلب الإذن لـ Clipboard API.

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

كما هي الحال في العديد من واجهات برمجة التطبيقات الجديدة، لا تتوافق واجهة برمجة تطبيقات Clipboard API إلا مع الصفحات التي يتم عرضها عبر HTTPS. للمساعدة في منع إساءة الاستخدام، لا يُسمح بالوصول إلى الحافظة إلا عندما تكون الصفحة علامة التبويب النشطة. يمكن للصفحات الموجودة في علامات التبويب النشطة الكتابة إلى الحافظة بدون طلب إذن، ولكن القراءة من الحافظة تتطلب دائمًا إذنًا.

تمت إضافة أذونات النسخ واللصق إلى Permissions API. يتم منح إذن clipboard-write تلقائيًا للصفحات عندما تكون علامة التبويب النشطة. يجب طلب إذن clipboard-read، ويمكنك تنفيذه من خلال محاولة قراءة البيانات من الحافظة. توضح التعليمة البرمجية أدناه الأخير:

const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);

// Listen for changes to the permission state
permissionStatus.onchange = () => {
  console.log(permissionStatus.state);
};

يمكنك أيضًا التحكّم في ما إذا كانت إيماءة المستخدم مطلوبة لاستدعاء القطع أو اللصق باستخدام الخيار allowWithoutGesture. ويختلف الإعداد الافتراضي لهذه القيمة حسب المتصفح، لذا يجب عليك دومًا تضمينها.

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

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

setTimeout(async () => {
  const text = await navigator.clipboard.readText();
  console.log(text);
}, 2000);

دمج سياسة الأذونات

لاستخدام واجهة برمجة التطبيقات في إطارات iframe، عليك تفعيلها من خلال سياسة الأذونات، التي تحدّد آلية تتيح لك تفعيل وإيقاف ميزات المتصفّح وواجهات برمجة التطبيقات المتنوعة بشكل انتقائي. وفي الواقع، عليك اجتياز إما الطريقتين clipboard-read أو clipboard-write أو كليهما، وفقًا لاحتياجات تطبيقك.

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

رصد الميزات

لاستخدام واجهة برمجة التطبيقات Async Clipboard API مع جميع المتصفّحات، اختبِر navigator.clipboard وارجع إلى الطرق السابقة. على سبيل المثال، إليك كيفية تنفيذ اللصق لتضمين متصفحات أخرى.

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  let text;
  if (navigator.clipboard) {
    text = await navigator.clipboard.readText();
  }
  else {
    text = e.clipboardData.getData('text/plain');
  }
  console.log('Got pasted text: ', text);
});

هذه ليست القصة الكاملة. قبل واجهة برمجة تطبيقات حافظة Async، كان هناك مزيج من عمليات تنفيذ النسخ واللصق المختلفة عبر متصفحات الويب. في معظم المتصفّحات، يمكن تشغيل النسخ واللصق في المتصفّح باستخدام document.execCommand('copy') وdocument.execCommand('paste'). إذا كان النص المراد نسخه سلسلة غير متوفرة في نموذج العناصر في المستند (DOM)، يجب إدخاله في نموذج كائن المستند (DOM) وتحديده:

button.addEventListener('click', (e) => {
  const input = document.createElement('input');
  input.style.display = 'none';
  document.body.appendChild(input);
  input.value = text;
  input.focus();
  input.select();
  const result = document.execCommand('copy');
  if (result === 'unsuccessful') {
    console.error('Failed to copy text.');
  }
  input.remove();
});

إصدارات تجريبية

يمكنك اللعب باستخدام واجهة برمجة التطبيقات Async Clipboard API في الإصدارات التجريبية أدناه. يمكنك على Glitch إعادة مزج العرض التوضيحي النصي أو العرض التوضيحي للصورة لتجربتهما.

يوضح المثال الأول النص المتحركة داخل الحافظة وخارجها.

لتجربة واجهة برمجة التطبيقات مع الصور، يمكنك استخدام هذا العرض التوضيحي. وتذكّر أنّه يمكن استخدام ملفات PNG فقط في عدد قليل من المتصفحات.

شكر وتقدير

نفّذ داروين هوانغ وغاري كاتشيمارشيك واجهة برمجة التطبيقات غير المتزامنة للحافظة. وقدّم داروين أيضًا العرض التوضيحي. شكرًا لكل من Kyarik وغاري مرة أخرى على مراجعة أجزاء من هذه المقالة.

صورة رئيسية من تصوير ماركوس وينكلر على قناة Unسبلاش