惊呆了,没主动复制就触发复制事件(document.oncopy )?原来是这样

theme: cyanosis

背景:我们某系统,有一个禁止复制的功能,如果没有复制权限,复制的时候会弹出一个toast。本质上就是document.oncopy = () => { ... }。有一天,有一个用户反馈说,她一进页面就无限弹出禁止复制的toast,而且是动一下就弹一下,系统根本无法使用

现场复现

用户只是简单的说了几句,大家都表示不可思议,都表示这不可能。最后屏幕共享的时候,果然如此,简直让人怀疑人生。一用鼠标选中了文本,页面就弹出不能复制,大概是这样的表现:

de.gif

选择文字的时候不手动复制都会触发copy

当时的录屏因为保密原因,不能对外公开。大概的情况就是这样,上面是我知道怎么复现自己本地跑demo录的屏,接下来会用同样的方式以第三者视角来逐步复述当时的问题排查过程

远程控制排查问题

首先打开控制台,把document.copy改写一下

代码语言:javascript
复制
const cp =  document.oncopy.bind(document);
document.oncopy = () => {
    console.log(1);
    cp();
    return false;
}

结果发现,选一下竟然真的还是弹出toast和打印了1

image.png

接下来加了个断点,还是会触发,一样的过程,看起来也没啥区别。于是,开始怀疑用户的插件,瞄了一眼,没有任何可疑的插件,然后把她的Chrome扩展全部关掉,依然会复现

初步结论:oncopy行为的触发,和插件无关

此时想起一句话:90%可以通过重启解决,9%可以通过重装解决,1%只能通过买新电脑解决

电脑重启了,继续远程控制。还是一样的问题,我说要不你多打开其他网站试试,任何网站都行。小姐姐还是很耐心的一个个操作给我看:“你看什么页面都ok,就你这有问题”。于是我随便试了几个页面,打开控制台输入oncopy,然后就立刻复现问题:

代码语言:javascript
复制
document.oncopy = () => {
    console.log(1);
    return false;
}

"你看呐,所有的页面都有同样的问题~ 你再随便试多几个页面看看" ——其实我在查资料,看看是不是有我知识盲区之外的

第二步结论:任何页面都会有问题,所以问题的根源不在于页面层面的,是更高层面上的

资料也没查到类似的问题,大概无奈的看着她操作了几分钟,我也一句话都没有说,对着电脑发呆。突然萌生一个念头:系统上的个性化设定

check了一下输入法,搜狗,应该无影响。但是,在对方频繁操作中,有一个若隐若现的小logo引起我注意

🧑‍🔧:“我发现你这有一个小logo,是干嘛的”

👩‍🦰:“一个翻译工具”

🧑‍🔧:“多动动看看,我想看清楚一点”

👩‍🦰:“你看,放在这里,它就会翻译屏幕上的单词”

🧑‍🔧:“那你试一下翻译其他软件如ppt呢”

👩‍🦰:“居然也可以喔”

🧑‍🔧:“那关掉这个翻译软件,再回来看看页面呢”

👩‍🦰:“好像没问题了”

🧑‍🔧:“嗯,那就是这个软件的问题。我看有一个自动翻译你鼠标所在的英文的功能,这个功能的实现方式可能是:你鼠标放到英文上,它会触发系统的copy事件,可能是直接帮你复制或者是背后帮你按下按键。你再打开这个应用,先把这个功能关了吧”

👩‍🦰:“哦,我知道了,有一个划词搜索的功能,应该跟他有关”

关掉后,问题是解决了,还是很好奇:你这软件叫什么,我也下载来玩玩

真凶就是《欧路词典》,它会在你选中文本的时候自动触发复制,拿到英文文案去搜索那个单词的信息——顾名思义划词搜索

下载来玩玩

下载回来开启,自己写了一个简单的demo,果然都复现了

代码语言:javascript
复制
const C: React.FC = () => {
  const ref = React.useRef<HTMLDivElement>(null);
  React.useEffect(() => {
    document.oncopy = (): boolean => {
      Toast.error('禁止复制');// 仅仅一个toast,随便找个ui库吧
      return false;
    };
    document.onclick = (): void => {
      ref.current!.innerHTML += '你点击了页面<br />';
    };
    const handleKeydown = (e): void => {
      ref.current!.innerHTML += `你按下了${e.key}<br />`;
    };
    document.addEventListener('keydown', handleKeydown);
    return (): void => {
      document.oncopy = null;
      document.onclick = null;
      document.removeEventListener('keydown', handleKeydown);
    };
  }, []);
  return (
    <>
      <pre>
        我说 你是人间的四月天; 笑响点亮了四面风; 轻灵在春的光艳中交舞着变。 你是四月早天里的云烟,
        黄昏吹着风的软,星子在 无意中闪,细雨点洒在花前。
      </pre>
      <div>
        操作记录
        <div ref={ref} />
      </div>
    </>
  );
};

按照预期,如果不开欧路词典,我们复制页面的内容,将会弹出toast禁止复制,如下:

开启了欧路词典,表现是这样:

问题因此转化为,如何区分出欧路词典的copy

解决方案

我们使用一种最简单的方式,按下command(key为Meta)不弹起的时候,生产key的队列,当最后一个按下的是c,则消费生产者队列,往前搜索有没有按过command

代码语言:javascript
复制
const Cpn: React.FC = () => {
  const ref = React.useRef<HTMLDivElement>(null);
  const providerQuene = React.useRef<string[]>([]);

const triggerCopy = React.useCallback(() => {
// 消费生产者队列的数据
const last = providerQuene.current.pop();
// 如果最后按下的是c,而且键盘不弹起,往前找是不是按下过command
if (last === 'c' && providerQuene.current.includes('Meta')) {
Toast.error('禁止复制');
}
}, [providerQuene]);
React.useEffect(() => {
document.oncopy = (): boolean => {
triggerCopy();
return false;
};
document.onclick = (): void => {
ref.current!.innerHTML += '你点击了页面<br />';
};
const handleKeydown = (e: KeyboardEvent): void => {
if (e.key === 'Meta') {
providerQuene.current.push('Meta');
}
if (e.key === 'c') {
providerQuene.current.push('c');
}
ref.current!.innerHTML += 你按下了${e.key}&lt;br /&gt;;
};
const handleKeyUp = ({ key }: KeyboardEvent): void => {
key === 'Meta' && (providerQuene.current = []); // meta键弹起,清理生产者队列
};
document.addEventListener('keydown', handleKeydown);
document.addEventListener('keyup', handleKeyUp);
return (): void => {
document.oncopy = null;
document.onclick = null;
document.removeEventListener('keydown', handleKeydown);
document.removeEventListener('keyup', handleKeyUp);
};
}, []);
return (
<>
<pre>
我说 你是人间的四月天; 笑响点亮了四面风; 轻灵在春的光艳中交舞着变。 你是四月早天里的云烟,
黄昏吹着风的软,星子在 无意中闪,细雨点洒在花前。
</pre>
<div>
操作记录
<div ref={ref} />
</div>
</>
);
};

以上所有的操作都是在mac的Chrome浏览器下,safari看起来没问题。其他浏览器或者windows的兼容性问题,都可以用类似的方式去处理