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

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

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

لا بأس بذلك بالنسبة إلى أجزاء صغيرة من النص، ولكن في العديد من الحالات، يؤدي حظر الصفحة من أجل نقل المحتوى إلى الحافظة إلى تقديم تجربة سيئة للمستخدم. قد يكون من الضروري إجراء عملية تنظيف تستغرق وقتًا طويلاً أو فك ترميز الصورة قبل أن تتمكّن من لصق المحتوى بأمان. قد يحتاج المتصفّح إلى تحميل الموارد المرتبطة أو تضمينها من مستند تم لصقه. سيؤدي ذلك إلى حظر الصفحة أثناء انتظار القرص أو الشبكة. تخيّل إضافة أذونات إلى المزيج، ما يتطلّب أن يحظر المتصفّح الصفحة أثناء طلب الوصول إلى الحافظة. في الوقت نفسه، فإنّ الأذونات التي تم وضعها بشأن 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);
  }
}

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 63.
  • Safari: 13.1.

Source

write()

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

لكتابة صورة إلى الحافظة، يجب أن تكون الصورة بتنسيق 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);
}

Browser Support

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

Source

حدث النسخ

في حال بدأ المستخدم عملية نسخ إلى الحافظة ولم يستدعِ الدالة 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:

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

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

Browser Support

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

Source

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

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);
  }
}

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 125.
  • Safari: 13.1.

Source

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);
  }
}

Browser Support

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

Source

العمل باستخدام الملفات التي تم لصقها

من المفيد أن يتمكّن المستخدمون من استخدام اختصارات لوحة المفاتيح الخاصة بالحافظة، مثل 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());
});

Browser Support

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

Source

حدث اللصق

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

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

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

التعامل مع أنواع 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 في العروض التوضيحية أدناه. يوضّح المثال الأول كيفية نقل النص من الحافظة وإليها.

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

الإقرارات

تم تنفيذ واجهة برمجة التطبيقات Asynchronous Clipboard API من قِبل داروين هوانغ وغاري كاتشمارسيك. قدّم "داروين" أيضًا العرض التوضيحي. نشكر Kyarik و&quot;غاري كاتشمارسيك&quot; مرة أخرى على مراجعة أجزاء من هذه المقالة.

الصورة الرئيسية من ماركوس وينكلر على Unsplash