为Chrome插件Google Dictionary进行的一次小手术

1. 起因

经常需要阅读英文文档,冷不丁的会碰到一些不懂的单词,之前的做法是打开一个谷歌翻译的网页在一旁放着,有需要就切换过来查单词。但是来回的切换着实有点麻烦,就想着有没有一些Chrome翻译插件,一番搜寻,找了两款比较心仪的Chrome插件:一个是good word guide的Instant Dictionary,它的优点是直接显示英文的释义,因为很多时候直接看英文的释义更容易理解一个单词。例如对于a bank of memory might be assigned to each CPU这句话,bank这单词,不管你套用“银行”、“湖畔”、“岸”等意思,感觉都怪怪的,而如果直接看它的英文释义a set or series of similar things, especially electrical or electronic devices, grouped together in rows,一下子就能明白他说的是一组特性相同的内存;除了Instant Dictionary,另一个就是Google的Google Dictionary,因为我就比较中意谷歌翻译。可是心仪归心仪,这两款插件都有个致命的缺陷:由于众所周知的原因,他们俩都没法链接到他们的服务器。
对于Instant Dictionary,这显然已经没救了,但是对于Google Dictionary,我觉得还可以抢救一下。为什么呢?因为谷歌翻译在中国是可以直接打开的,对应的域名是translate.google.cn,既然都是Google家的东西,连不上translate.google.com,那是否可以让它去连translate.google.cn?查看了Google Dictionary配置选项,并没有切换到国内服务器的选项,这样一来,想在国内用,就只能手动改一改了。

胡适曾经说过:大胆假设,小心求证。我们的假设就是最终单词翻译的请求是通过HTTP进行的,并且域名使用的是translate.google.com

2. 行动

好,说干就干,只要思想不滑坡,方法总比困难多。

2.1. 获取插件

Chrome插件的后缀名是.crx,其实就是一个压缩包。常用的压缩软件一般都能解压,解压出来的是一堆JavaScript文件以及其他相关的一些文件。我有想过它为什么不直接用.zip做后缀?最后得到了一个我自己比较信服的答案,使用.zip等常用后缀就相当于在挑衅:“你来解压我啊!”。总会有好事者解压一探究竟,并且这样逼格也就不那么高了。当然,最后.crx依旧没能阻挡好事者。
但是想解压,那也要先拿到。遗憾的是插件商店在中国正常情况下也是没法访问的,好在你不能访问,别人也不能访问。但是不能访问并不代表需求就消失了,需求始终都在,只有爱会消失。使用必应搜索chrome extension downloader就能找到一堆下载插件的网站,例如https://crxdown.com/。紧接着,虽然我们不能直接访问到插件,但我们可以网上搜索该插件,得到它的确切地址,这样我们就能将它下载下来。例如在搜索Google dictionary后找到来源为Chrome Web Store的结果,右键选择复制链接地址就能得到对应的插件地址。

搜索Google Dictionary的结果

2.2. 找入口

得到了插件,下面我们要做的就是找到入口。正常情况下,出于安全的考虑Chrome是没法安装我们下载好了的插件的,即便是来源正经也不行,我们要进入开发者模式。在浏览器地址栏输入chrome://extensions/,在打开的界面中勾选开发者模式。

打开开发者模式

然后,点击load unpacked加载我们已经解压好了的插件,只需要选择包含manifest.json这层的文件夹就行。
加载解压后的Chrome插件

想要改代码,那就必须先理解代码,想要理解代码,首先要找到一个合适的切入点。理论上,manifest.json是整个插件的元文件,里面肯定会有描述整个插件的入口文件之类的。但这个门槛有点高,使用这种方法应该是对Chrome插件的开发比较熟悉的,我这种门外汉算了。除此之外,还有另一个方法,那就是直接搜索我们能看到的东西。

Google Dictionary弹窗界面

运行该插件之后我们发现,触发翻译的条件是我们输入需要翻译的内容后回车或者点击蓝色按钮,正常情况下两种方法最终都会调用同一个函数。因为我们可以从这两个动作入手。通过在所有文件中搜索Define这个单词,最终我们在browser_action.html发现了这个按钮的标签。

    <div id="form">
      <input type="text" id="query-field"><button id="define-btn" class="btn btn-primary" value="Define">Define</button>
    </div

从代码中看到,这里有个叫qeury-field的输入以及一个叫define-btn的按钮,和我们看到的一致。这个按钮标签定义了id属性却没有定义点击的回调函数,那么很大概率在JavaScript代码中会使用类似get_element_by_id()这类的函数获取该标签并为其绑定回调函数,因此我们接着使用它的id define-btn进行搜索。

搜索define-btn结果

一共搜索到三条内容,一条在html文件中,也就是我们刚看到的,一条位于css文件中,说明是设置显示样式的不用管,剩下一条在js文件中,果然和我们想的一样。打开该文件,返现代码已经经过混淆挤作一团了,正常人估计没几个能这么读代码,因此需要稍加处理。
简直乱码

处理方式很简单,随便找个JavaScript代码美化网站进行下格式调整,这里我使用的是https://beautifier.io/。进过美化,代码变成了下面的样子。

    d = document.getElementById("define-btn");
    e = document.getElementById("query-field");
    f = document.getElementById("status-box");
    g = document.getElementById("status-msg");
    h = document.getElementById("status-search-link");
    k = document.getElementById("usage-tip");
    n = document.getElementById("meaning");
    k.display = "block";
    k.innerText = "Tip: Select text on any webpage, then click the Google Dictionary button to view the definition of your selection.";
    document.getElementById("year").innerText = (new Date).getFullYear();
    p(h);
    p(document.getElementById("options-link"));
    e.focus();
    d.addEventListener("click", r, !1);
    e.addEventListener("keydown", function(a) {
        13 === a.keyCode && r()
    }, !1);

可以看到,在这里它找到了输入框,将它命名为e,找到了按钮将它命名为d
这样,我们就算摸到门了。

2.3. 代码梳理

经过前面的探寻我们已经找到了代码入口,可以看到,当我们点击按钮,最终会调用一个名为r的函数。好吧,让我们看看这个r长啥样。

        r = function() {
            var a;
            if (a = e.value.replace(/^\s+|\s+$/g, "")) g.innerHTML = "Searching...", f.style.display = "block", h.style.display = "none", k.style.display = "none", n.style.display = "none", d.disabled = !0, c++, chrome.runtime.sendMessage({
                type: "fetch_html",
                eventKey: c,
                query: a
            }, q)
        },

从代码中看到,首先这个r函数对输入框中的字符做了简单的处理,最终传递给了chrome.runtime.sendMessage()。代码到此戛然而止,在源代码中再也找不到chrome.runtime.sendMessage()的定义了,既然源代码中找不到,那么只能是别的库中的API或者是系统API。从字面上我们知道它把参数发了出去,但是发给谁了呢?发给了服务器?没道理啊。我们可以断定的是变量a中肯定只包含了需要查询的字符串,而c的值是数字1,这些参数不足以告诉别的API你的目的。

既然猜测是外部API,那就去搜索引擎搜索chrome.runtime.sendMessage吧。
最终搜索得到完全匹配的结果都位于Google域名之下,很遗憾没法访问,但是在MDN Web Docs上看到了runtime.sendMessage的介绍。

image.png

最后显示Chrome支持了这个API,那么八九不离十,就是它了。从介绍中我么知道,当使用了runtime.sendMessage之后,会有一个叫runtime.onMessage的API对它进行响应,我们需要接着搜索。

最后发现两个文件使用runtime.onMessage,它们分别是backgrpund.min.js以及content.min.js。同样的,我们对它们的内容进行了美化。

搜索结果

对它们一个一个的梳理,最终确定了chrome.runtime.onMessage.addListener(G)这条语句中注册的G函数最终会响应之前点击按钮后调用的sendMessage()函数,因为每个注册的函数都会先通过type参数判定这是不是他们该响应的,从中我们看到G函数判定的是fetch_raw以及fetch_html,恰好我们之前看到的sendMessage()函数中传递进来的参数是fetch_html

image.png

最终,在梳理G函数的过程中,见到了我们梦寐以求一个字符串https://translate.google.com/translate_a/t?client=dict-chrome-ex&sl=,并且看到了XMLHttpRequest的使用,证明我们的假设是对的。最后一番苦寻之后,只是将.com改成.cn。暗自祈祷,希望能成功。

        F = function(a, c, b) {
            a = "https://translate.google.cn/translate_a/t?client=dict-chrome-ex&sl=" + c + "&tl=" + q.language + "&q=" + encodeURIComponent(a);
            var d = new XMLHttpRequest;
            d.open("GET", a, !0);
            d.onload = function() {
                var f = null;
                if (200 === this.status) try {
                    f = JSON.parse(d.response)
                } catch (l) {}
                return b(f)
            };
            d.send()
        },

遗憾的是,事情并没有想象的那么顺利,插件没能查出词来。

2.4. Debug

怎么回事?一开始就猜错了么?

修改了域名之后,并没有顺利的得到结果。很沮丧,很无奈,但是既然都到这份上了,不搞它一搞又心有不甘。于是乎,打开了调试窗口(鼠标移动到插件图标上右键选择inspect popup)。一番调试下拉,发现代码根本没有跳转进入关键的F函数当中,而使得代码能够执行F函数最重要的一个名叫p的变量的值始终是false。问题的关键就是这个p什么时候会变成true。继续梳理代码,发现当一个叫initBackgroundPageAsync的函数执行的时候,p就有可能被赋值true,并且这是p唯一变成true的地方。

    window.initBackgroundPageAsync = function(a) {
        gapi.config.update("googleapis.config/root", "https://dictionaryextension-pa.googleapis.com");
        gapi.client.setApiKey("AIzaSyA6EEtrDCfBkHV8uU2lgGY-N383ZgAOo7Y");
        var c = function() {
                2 > Object.keys(r).length || (p = !0, a && a())
            },
            b = function(d) {
                Mustache.parse(d);
                return function(f) {
                    return Mustache.render(d, f)
                }
            };
        Q("templates/browser_action_dict.html", function(d) {
            r.browser_action_dict = b(d);
            c()
        });
        Q("templates/browser_action_tran.html", function(d) {
            r.browser_action_tran = b(d);
            c()
        })
    };

问题又变成了查看该函数何时被调用。最终发现一个名叫background.html的文件加载https://apis.google.com/js/client.js这个文件完成后会执行。

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script type="text/javascript" src="lang_map.min.js"></script>
    <script type="text/javascript" src="mustache.js"></script>
    
    <script type="text/javascript" src="background.min.js"></script>
    <script type="text/javascript"
            src="https://apis.google.com/js/client.js?onload=initBackgroundPageAsync">
    </script>
    <script type="text/javascript" src="ga.js"></script>
  </head>
  <body>
  </body>
</html>

查了一下,https://apis.google.com/js/client.js这个文件用于使用Google全家桶的,在中国其实没啥用并且会带来麻烦,因为根本访问不。由于访问不了,就不可能加载成功;而加载不成功就不会执行initBackgroundPageAsync,因此决定手动执行initBackgroundPageAsync。其实也就是在background.min.js文件的末尾增加一行代码:

window.initBackgroundPageAsync();

本以为到此大功告成,可是现实还是狠狠地给了一巴掌,虽然代码终于执行了F函数,但依旧没有得到想要的结果。嘿我这暴脾气,跟它杠上了。

这次调试返现,代码在奇怪的地方卡住了,定睛一看,在一个叫D的函数里面出不来了。

        D = function(a, c, b) {
            var d = c;
            "en-uk" == c && (d = "en");
            var f = window["gdx.LANG_TO_CORPUS"][c];
            f || (f = c);
            a = {
                path: "v1/dictionaryExtensionData",
                params: {
                    term: a,
                    language: d,
                    corpus: f
                }
            };
            (f = window["gdx.CORPUS_TO_COUNTRY"][f]) && (a.params.country = f);
            gapi.client.request(a).execute(function(l) {
                var e =
                    l.status;
                if (e && 200 != e) b(null);
                else {
                    l = H(l, "dictionaryData[0]");
                    if (!l) return b(null);
                    var m = function(g) {
                        if (!g.senseFamilies) return 0;
                        g = g.senseFamilies;
                        for (var h = g.length, n = 0; n < g.length; n++) g[n].senses && (h += .1 * g[n].senses.length);
                        return h
                    };
                    e = function(g, h) {
                        return m(h) - m(g)
                    };
                    l.entries && (l.entries = l.entries.sort(e));
                    l.webDefinitions && (l.hasWebDefinitions = !0);
                    b(l)
                }
            })
        },

仔细分析了下代码,发现它又使用谷歌的API去请求一些奇奇怪怪的东西,然后调用一个回调函数b。但是有意思的是,当请求失败了,也就是返回码不是200的时候,它依然会调用回调函数b,只不过传了个空参数。既然如此,有一种可能是传递进去的这个参数是锦上添花的。那么我们就假设他次次都请求失败,所以我们将代码改成了下面的样子:

D = function(a, c, b) {
            b(null);
            return;
}

重新加载插件运行,呀,成功了!

image.png

3. 总结

我为什么一开始就没想到直接搜索translate.google.com这个字符串呢?以为抄了近路,最后回过头看还是拐了拐。
代码见文后链接。

欢1迎2关3注4个5人6微7信8公9众0号: 爱码士1024
源码 | 原理 | 语言 | 工具

4. Resources

[1] https://github.com/zmychou/google-dictionary-chrome-extension
[2] https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage
[3] https://beautifier.io/
[4] https://crxdown.com/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容