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

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

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

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

تعالج واجهة برمجة التطبيقات Async Clipboard API هذه المشاكل، إذ تقدّم نموذج أذونات محدّدًا جيدًا لا يؤدي إلى حظر الصفحة. تقتصر واجهة 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);
  }
}

دعم المتصفح

  • Chrome: 66
  • الحافة: 79.
  • Firefox: 63
  • ‫Safari: 13.1

المصدر

كتابة()

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

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

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

بعد ذلك، مرِّر مصفوفة من عناصر ClipboardItem كمَعلمة إلى write() الطريقة. يمكنك حاليًا إرسال صورة واحدة فقط في كل مرة، ولكن نأمل أن نتيح استخدام صور متعددة في المستقبل. تأخذ دالة ClipboardItem عنصرًا يحتوي على نوع MIME للصورة كمفتاح و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);
}

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

  • Chrome: 76
  • ‫Edge: 79
  • Firefox: 127.
  • ‫Safari: 13.1

المصدر

حدث النسخ

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

<!-- 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:

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

  • Chrome: 1-
  • الحافة: 12.
  • Firefox: 22.
  • Safari: 3-

المصدر

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

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

  • Chrome: 76
  • الحافة: 79.
  • Firefox: 127.
  • ‫Safari: 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);
  }
}

دعم المتصفح

  • Chrome: 66
  • ‫Edge: 79
  • ‫Firefox: 125
  • ‫Safari: 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);
  }
}

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

  • Chrome: 76
  • ‫Edge: 79
  • Firefox: 127.
  • ‫Safari: 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());
});

دعم المتصفح

  • Chrome: 3.
  • Edge: 12.
  • Firefox: 3.6
  • Safari: 4.

المصدر

حدث اللصق

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

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

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

  • Chrome: 1-
  • Edge: 12.
  • Firefox: 22.
  • Safari: 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 Clipboard API، كان هناك مزيج من عمليات التنفيذ المختلفة للنسخ واللصق في متصفّحات الويب. في معظم المتصفّحات، يمكن استخدام الترميزَين 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 فقط هي المتوافقة وفي عدد قليل من المتصفّحات فقط.

الشكر والتقدير

نفَّذ داروين هونغ وغاري كاچمارتشيك واجهة برمجة التطبيقات Async Clipboard API. قدّم "داروين" أيضًا العرض الترويجي. نتوجّه بالشكر إلى كياريك ونشكرك مجددًا على "غاري كاتشمارشيك" على مراجعة أجزاء من هذه المقالة.

الصورة الرئيسية تقدّمها ماركوس وينكلر على Unsplash.