TA的每日心情 | 慵懒 2022-3-8 11:41 |
---|
签到天数: 2 天 [LV.1]初来乍到
荣誉开发者
- 积分
- 1379
|
本帖最后由 cxxjackie 于 2021-10-7 16:12 编辑
FBI WARNING
由于油猴的一些古怪特性造成预料之外的问题,请临时新建以下脚本来复现文章中的操作:
- // ==UserScript==
- // @name 教程专用测试脚本
- // @namespace ...
- // @author ...
- // @version 1.0
- // @include https://docs.qq.com/doc/DQ1BMTk1BWlVWcndF
- // @grant unsafeWindow
- // @run-at document-start
- // ==/UserScript==
- document;
复制代码
其实大部分腾讯文档是没有限制的,只有作者设置了复制权限才会无法复制,本文将以这篇文档为例进行实战。腾讯文档的防复制手段与道客巴巴很相似,都是将文字绘制在canvas里(详情请看上文)。首先从事件监听入手,发现页面中存在copy事件,这可方便了不少(也可以从click事件或按键事件入手,但是事件很多分析起来要麻烦亿点)。copy事件逐个点进去看一眼,发现这样一段比较有趣的代码:
翻译一下就是判断然后执行a.onCopyByEvent(t),而a.onCopyByEvent就在这个函数的下面,继续往下看,可以大致看出这是复制函数的相关逻辑,开头用if判断了一下,我们在if这里打个断点,然后选一段文字ctrl c,成功断了下来。由于if条件内包含函数,这里要按F10继续(step over,调试图标第二个,逐行运行代码,但不进入子函数),结果if内的代码被跳过,说明条件不成立。观察if的条件,发现a.context.isCopyable()很可疑,可以把鼠标放上去跟进去看,也可以在控制台中输入a.context.isCopyable查看,函数内容很简单,就一句话:
- return this._docEnv.copyable;
复制代码 由于isCopyable是a.context的属性,所以这里的this指的是a.context,即a.context.isCopyable函数读取的是a.context._docEnv.copyable的值。控制台查看一下这个值,发现是false,把他改成true看看,取消断点,再次ctrl c复制,结果成功了。现在的问题是怎么得到这个copyable的路径,我们当然不可能在代码中直接写a.context._docEnv.copyable,因为这个a是打了断点以后获得的局部引用,在全局环境中是没有a这个东西的。这就要祭出我做的这个脚本了(自卖自夸),注入函数,然后在全局环境中搜索copyable这个属性:
只找到一条路径,要验证路径是否正确,可以再执行一次断点,然后控制台判断一下:
- a.context._docEnv === window['App']['CollabRoom']['collabRoomOptions']['notificationCenter']['_docEnv']
复制代码 结果为true,说明路径是正确的。这个判断的原理是copyable是前面那个对象的属性,我们只要证明两个引用属于同一个对象,那么改动其中一个必然会影响另一个。其实不是每个属性都能找到全局路径,这里算是比较取巧的做法,找不到的话就只能慢慢去跟代码逻辑了。接下来可以刷新页面,验证一下路径有没有变,控制台输入:
- window['App']['CollabRoom']['collabRoomOptions']['notificationCenter']['_docEnv']['copyable'] = true;
复制代码 ctrl c复制一下看看,发现成功了。至此我们已经可以写出一个脚本,用这一行代码来解决问题。接下来进行一些优化操作,当我们按ctrl c时,虽然能复制成功,但还是会弹出权限被限制的提示,点击调试工具右上角的3个点,选择search,在所有js中搜索提示文字:文档作者限制了(后面可以省略),找到一堆乱七八糟的东西,逐个点开看看,可以发现大部分都只是一堆字符串,只有public-doc-pc开头的这个文件比较可疑:
显然p.qO.getInstance().canCopy决定了是否出现提示,这里打个断点,ctrl c后断下来,控制台输入p.qO.getInstance().canCopy = true,继续运行,结果发现并没有效果。这种情况往往是因为该属性通过Object.defineProperty修改了getter造成的,即我们访问这个属性时,实际上访问的是别的东西。要验证这一点,可以断点后在控制台输入p.qO.getInstance(),展开这个对象看看,发现canCopy这个属性是灰色的,这就是通过getter访问的属性。怎么查看他的getter呢?有一种简单的方法:
与前面相似,可以得出读取的是p.qO.getInstance().privilege.canCopy的值(isPublish是undefined),这次我们输入p.qO.getInstance().privilege.canCopy = true看看,结果成功屏蔽了提示。接下来的流程就跟前面一样,$searchKey('canCopy'),找到2个路径,验证一下可以得出window['pad']['permissionCtrl']['privilege']['canCopy']是正确的,然后在脚本中加入这行代码就行了:
- window['pad']['permissionCtrl']['privilege']['canCopy'] = true;
复制代码 最后是解锁右键的复制,可以看到右键菜单中的复制是灰色的,一个简单的思路是监听节点插入(即监听右键菜单出现),获取元素后修改css,把复制这一项改成黑色,然后给他加一个click事件,模拟ctrl c按键即可。但是我们前面既然都能通过修改全局属性来达到目的,这里能不能也这么做呢?答案是可以的。
我们先定位到这个复制元素,查看他的click事件,可以看到一大堆,其实这些事件大部分是重复的,只是绑定在不同节点上,逐个点进去看一眼,可以找到这段代码:
从代码上可以推测出,t.props.disabled判断元素是否禁用,从而决定后续代码是否执行。虽然可以通过修改disabled的值来验证我们的想法,但由于这个事件绑定的节点非常多,每次t都不一样,调试起来比较困难。我们可以试着$searchKey('disabled')看看,找到6个结果,除了两个jquery的明显不对,其他4个是非常明显的React路径(实际上不止4个,因为我脚本设置了最大搜索深度,而React路径通常非常长),一堆return、child、stateNode,以及最关键的__reactInternalInstance这个字段。为了验证我们的想法,可以打个断点,鼠标放到t上看看,可以看到context、props、state、updater等属性,这就是典型的React对象,说明这些元素是用React库构建的。这里要用到一个chrome插件:React Developer Tools(链接就不提供了,请自行搜索下载),可以调试React,安装后会在调试工具处出现一个React标签。定位到右键菜单的元素后切换至React标签,可以看到这些东西:
注意这里我选中了copy组件,在右边的Props中出现了disabled: true这一项,前面还有个勾,去掉勾试试,disabled变成了false,右键菜单中的复制变成了黑色!点击一下,成功复制到了文字(如果你刷新过页面,别忘了前面的2行代码)。这个disabled要在脚本中修改是比较复杂的,首先copy不是页面中实际存在的节点,但他的上一级组件是(可以看到ul标签,className属性),在ul组件上右键选择"Find the DOM node"会跳到页面中的节点,此时切换到控制台,输入$0.__reactInternalInstance,后面还有一串像乱码一样的东西会自动补齐。在控制台中$0是我们当前选中的节点(这也是一个调试小技巧),__reactInternalInstance是React注入的,后面那一串东西是标识符,每次刷新页面都会变。展开这个对象,由于我们要找的copy是他的子组件,要从child里找,展开child后可以看到key是cut,这是第一个组件,我们要找的copy是第二个,路径就是child.sibling(sibling代表下一个),展开sibling后看到key是copy,他的props位于stateNode中,所以完整路径就是$0.__reactInternalInstance$标识符.child.sibling.stateNode.props.disabled(我知道这有点绕,但React就这尿性)。我们姑且用一个sn来存储stateNode,将disabled修改为false:
- const sn = $0.__reactInternalInstance$标识符.child.sibling.stateNode;
- sn.props.disabled = false;
复制代码 此时点击复制已经有效果了,但复制还是灰色的,这是因为组件没有更新,再加一行代码即可:
- sn.updater.enqueueForceUpdate(sn);
复制代码 由于每次右键菜单都会重新生成,调试起来比较麻烦。要在脚本中实现,具体步骤就是监听节点插入,获取右键菜单(即$0),然后遍历这个元素的属性,找到__reactInternalInstance开头的属性(用正则或者startsWith),之后的代码就跟上面一样了。嗯,听上去不比自己绑一个click事件简单多少(这其实是在讲怎么处理React),好吧,让我们回归初心。我们已经知道这个组件的key是"copy",那么在设置disabled的地方修改判断逻辑不就行了吗?
在所有文件中搜索copy,找到一大堆,这是当然的,页面中的copy事件也在搜索范围内,还有其他一些奇奇怪怪的copy,分析起来很麻烦。换个思路,切换到React观察一下,可以看到右键菜单中还有一个“插入链接”,他的key是"insert_link",这个应该不常见,搜索一下看看,只有2个结果(注意带:formatted的是我们点击了格式化后产生的副本),点开第一个按ctrl f,点一下右边的.*,搜索copy.*disabled(这是用正则搜索的意思,搜索所有文件也可以正则,但是会很慢),没有结果,搜索第二个,找到了这样一个有趣的东西:
这里显然是设置disabled的地方,不过代码经过混淆,读起来比较费劲,我可以稍微整理一下,让他看起来可读性好一点:
- la([
- (0, ci.v)("cut", "shouldDisabled"),
- (0, sa.x)("cut", "shouldDisabled"),
- da(0, (0, ji.M)()),
- pa("design:type", Function),
- pa("design:paramtypes", ["function" == typeof (t = void 0 !== Fe.z && Fe.z) ? t : Object]),
- pa("design:returntype", void 0)
- ], e.prototype, "cannotCut", null),
- la([
- (0, ci.v)("copy", "shouldDisabled"),
- (0, sa.x)("copy", "shouldDisabled"),
- da(0, (0, ji.M)()),
- pa("design:type", Function),
- pa("design:paramtypes", ["function" == typeof (n = void 0 !== Fe.z && Fe.z) ? n : Object]),
- pa("design:returntype", void 0)
- ], e.prototype, "cannotCopy", null),
复制代码
对于这种混淆过的代码,不必去纠结每一步的含义,可以从前后文推敲。比较上下两段代码的差异,区别在于最后的"cannotCut"和"cannotCopy",猜测这就是设置disabled的关键。这次我们搜索cannotCopy,只找到一个文件(就是这段代码所在的文件),点开找到cannotCopy的定义处,看到有一个notCopyable函数,这里我们可以打个断点,右键一下试试能不能断下来,成功断下来后跟进这个notCopyable函数看看:
可以看出e.permission.isCopyable是关键属性,在return前打个断点,运行到这后鼠标放上去,看到是false,控制台改成true试试,结果复制还是灰的,点击也没效果。这并不是说我们的方向错了,还记得前面说过的getter吗?鼠标放到e.permission上看看,发现isCopyable属性是灰的,控制台输入:
- e.permission.__lookupGetter__('isCopyable')
复制代码 这次的函数不太直观,可以点一下跟进去看看:
这里的代码就是判断是否为编辑状态(isEditable)或复制是否被禁用(view_forbid_copy_print),isEditable其实跟isCopyable一样是个判断函数,我们去跟这个有点绕,直接从后面入手。还是在return前打个断点断下来,看一下e.view_forbid_copy_print是1,所以等于1是true,加上前面的!,返回的结果就是false。我们在控制台把e.view_forbid_copy_print改成0试试(这次记得看一眼e确定view_forbid_copy_print属性不是灰的),复制变成了黑色!接下来取消断点,多划几个地方试试,每次右键菜单的复制都是黑色的,而且点击也都能生效,说明改对了地方。接下来就是传统艺能了,搜索view_forbid_copy_print的全局路径,最终确定为window['App']['CollabRoom']['collabRoomOptions']['clientVars']['advPolicy']['view_forbid_copy_print']。至此,给我们的脚本加上第3行,就算完美收工了:
- window['App']['CollabRoom']['collabRoomOptions']['clientVars']['advPolicy']['view_forbid_copy_print'] = 0;
复制代码
结语
虽然本文的废话洋洋洒洒写了一大篇,但最后的代码只有3行,用简单的代码解决复杂的需求,这正是编程的艺术啊~
|
-
查看全部评分
总评分:油猫币 +4
|