最新更新

现在已经将其改成了blogging-tool,增加了一些主题特定支持的snippets和预览功能,但是fake-referrer仍然是核心功能之一。

项目页:https://github.com/JeffersonQin/vscode-blogging-tool

Preface

我还记得一个月前?半个月前?反正就是刚放寒假那会,给一个腾讯云COS的插件改了bug,修复了Windows下的路劲问题,现在才姑且能用。我的想法其实是想把腾讯云的COS作为图床(毕竟费用还可以)(确信)。在设置了防盗链之后,我发现VSCode是无法正常渲染图片的(毕竟就算是基于Chromium也怎么可能会设置referrer嘛...),所以就想着魔改一把市面上已有的插件来实现此功能。

所以最终魔改后的定位:可以图片请求伪造referrermarkdown preview插件

锁定魔改的插件

第一步便是要锁定需要魔改的插件,但是整个过程十分曲折迂回。一开始安装的插件是Markdown All in One: https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one

但是读代码的时候一直觉得很奇怪,那就是即便有个文件叫做preview.ts,仍然找不到WebView的相关代码。一度以为可能和html有关系,便找到了这里:

最后却发现这个只是将文件导出成html(大悲:

仔细一想,我禁用了插件,但却发现VS Code仍然可以进行Markdown的preview,这才发现原来是VS Code本身的功能(我当时直接怒摔键盘(划掉。

所以在这之后,锁定了现在的这个插件:Markdown Preview Enhancedhttps://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced

Clone代码,运行

使用GitHub Desktop,Clone到了桌面(过程中开着ssr否则速度吃不消),首先尝试直接通过命令行移动:

$ wsl
$ mv vscode-markdown-preview-enhanced/ ~

但是发现:

mv: setting attribute 'system.wsl_case_sensitive' for 'system.wsl_case_sensitive': Invalid argument

好像是和wsl大小写不敏感有关,于是乎不搞这些花里胡哨的,准备通过UI界面直接移动文件夹。进入wsl子系统的用户目录:

explorer .

在Windows文件资源管理器内打开这个目录,然后直接拖动文件夹,移动完毕。

用VSCode直接打开这个文件夹:

code .

发现由于在linux环境内,换行符的问题导致所有文件git管理都显示modified,为了消除不必要的困扰以及为了以后一目了然知道哪里是修改过的代码,先commit一次:

git commit -a -m "Linux Update"

首先尝试npm install,第一次尝试使用淘宝源:

cnpm install

表面上好像并没有什么问题,但是下一步进行编译(因为项目使用的是TypeScript):

npm run compile

发现报错:

初步判断可能是因为包老,所以执行了一下Update:

cnpm update

权限不够,报错,所以加上sudo:

sudo cnpm update

仍然报错,版本冲突:

经搜索引擎搜索后无果,遂放弃。

直接选择删除outnode_modules文件夹,重新使用npm安装依赖,不使用cnpm

proxychains4 npm install

由于速度太慢,所以使用了proxychains4,有需要的可以看之前的文章。

可以看到安装成功,但不知道问什么出现了淘宝的地址,大概是装了cnpm的原因吧(我也不太懂,毕竟只是为了改个功能并非深挖nodejs,如果有了解的欢迎评论)

开始编译:

编译没有问题,所以开始调试:

虽然是在WSL内,但是还是成功地跑起来了,这里感叹一下巨硬牛逼(!

搞清大致的结构

快速浏览了一下源码,开始思考切入点:

  • 什么时候Preview会更新
  • 更新是如何触发的
  • 更新的代码写在哪里
  • 如何拦截更新

凭着之前写插件的经验,找到了extension.ts内:

测试了一下,Console确实有输出:

看到后面的一句:

contentProvider.update(event.document.uri);

contentProvider在之前定义了,是MarkdownPreviewEnhancedView的实例,暂且不去管它,event.document.uri估计是当前正在编辑的窗口的估计指针的类似的东西(纯属口胡,如有误解欢迎指正)

按下Control键单击update定位到函数,并增加两条调试语句:

调试后发现:

  • 在没有开启preview的情况下指输出here 1
  • 开启preview的情况同时输出here 2

得到第一个if只是排除了没有预览窗口的情况的结论。开始阅读第二个if的代码。

大概可以推测出thiswaiting的含义,估计只是在编辑之后有一段延时再进行渲染,所以主要着眼于:

this.updateMarkdown(sourceUri);

毕竟怎么看都是这句是关键。

继续如法炮制,通过打log证实了我的猜想:

看回代码:

第一个和第二个if估计只是业务逻辑上的一些判空,只有后面做这给的注释presentation mode我也不知道是啥,所以我姑且就没有理他,通过log的输出定位到了here 5的地方。现在看之后的代码:

// not presentation mode
vscode.workspace.openTextDocument(sourceUri).then((document) => {
    const text = document.getText();
    this.previewPostMessage(sourceUri, {
        command: "startParsingMarkdown",
    });

    const preview = this.getPreview(sourceUri);

    engine
        .parseMD(text, {
            isForPreview: true,
            useRelativeFilePath: false,
            hideFrontMatter: false,
            triggeredBySave,
            vscodePreviewPanel: preview,
        })
        .then(({ markdown, html, tocHTML, JSAndCssFiles, yamlConfig }) => {
            // check JSAndCssFiles
            if (
                JSON.stringify(JSAndCssFiles) !==
                    JSON.stringify(this.jsAndCssFilesMaps[sourceUri.fsPath]) ||
                yamlConfig["isPresentationMode"]
            ) {
                this.jsAndCssFilesMaps[sourceUri.fsPath] = JSAndCssFiles;
                // restart iframe
                this.refreshPreview(sourceUri);
            } else {
                this.previewPostMessage(sourceUri, {
                    command: "updateHTML",
                    html,
                    tocHTML,
                    totalLineCount: document.lineCount,
                    sourceUri: sourceUri.toString(),
                    id: yamlConfig.id || "",
                    class: yamlConfig.class || "",
                });
            }
        });
});

看到前两行的textpreview,我也不知道这是在干什么,所以我们需要大胆猜测。胆大心细做好备份,正是API猜测大法的方法论,世界观和方法论是统一的(dbq 胡言乱语ing)。但是通过作者的变量命名,我们发现:startParsingMarkdownstart这个词好像别有深意!所以我们大胆猜测,这大概就是一个signal,和我们要干的事情可能关系不大。为了初步印证我的猜想,我们不妨查看一下previewPostMessage到底写了啥:

里面关键调用了VS CodeWebViewpostMessage方法。扯一句题外话,关于WebView,大部分VSC插件都是通过WebView来实现界面的。可以通过Shift + Ctrl (Command) + P输入WebView来呼出调试器。想要确定一个Panel是不是WebView也很简单:呼得出就是,呼不出就不是。

好,所以好像并没有啥关系,我们不妨输出一下textpreview

所以text就是文档内容,preview倒没看出来啥,啥都不是?(逃

继续看engine那行代码,一开始的.parseMD好像并不是我们要找的,只是在解析MD构建语法树?后面的.then()貌似是在干正事。虽然目测估计正常情况下是执行else的代码,但还是打一下log确定一下执行的位置:

好,我们离成功只差最后一步了(!因为只有一行代码了(确信

this.previewPostMessage(sourceUri, {
    command: "updateHTML",
    html,
    tocHTML,
    totalLineCount: document.lineCount,
    sourceUri: sourceUri.toString(),
    id: yamlConfig.id || "",
    class: yamlConfig.class || "",
});

我现在非常的迷惑,看到这么一堆参数,但我觉得重要的只有两个:htmltocHTML,盲猜后面的是目录的html?いみふめいです。好,输出不就行了。(顺便整了点花活)

测试文件:

Console输出:

tocHTML的输出也应证了我的目录猜想。但是到这里还没完,为了保险起见,我们还需要打开WebView的调试器进行进一步的确认:

和先前的结果确实吻合,为了确认这里就是html的控制语句,我们不妨把htmltocHTML都替换成空字符串,看一下渲染效果:

内容确实消失了。

明确魔改需求

在搞清楚代码结构以及魔改方法之后,我们就需要明确我们的魔改需求:可以将腾讯云COS中的图片显示出来(之前显示不出来是因为腾讯云我开了防盗链,禁止没有Referrer的请求,而VSCode对图片的请求中并没有Referrer),所以初步的想法有两种:

  • 第一种:给请求加上Referrer,或者改变Refer-Policy
  • 第二种:发现有这个网站的图片直接下载到本地,并在渲染前更改渲染的html换为本地的。

对于第一种情况:我们不妨直接在编写markdown的时候使用img标签,里面加入referrerpolicy="origin"的属性。但是很遗憾,在WebView调试其中我发现,Referrer仍然为空,即便有了这个属性:

注:这里图片请求成功了是因为我暂时关掉了防盗链。

之后,思考到会不会是<meta>标签内有这样的信息,但是在查看了主程序的htmlWebViewhtml后都无果,遂放弃了这条思路:

对于第二种方法,我们可以绕开浏览器,通过本地的网络请求来进行实现(powershell, curl, etc.)

开始魔改

首先明确这个插件将图片渲染成html的格式:

发现格式为:

<img src="<link_addr>" alt="<alt_name>">

本来想着直接替换的,但是转念一想想到了更好的处理方法:使用现成的库来解析html不就得了。

使用搜索引擎搜索了一番之后,我锁定了cheerio这个包,正准备安装的时候,看了一下node_modules里发现作者貌似已经用了:

于是我们现在是要筛选出<img>标签中,src属性以特定字符串开头的:

import * as cheerio from "cheerio";

...

const $ = cheerio.load(html);
$("img").each((_index, element) => {
    if (element.attribs["src"].startsWith("<prefix>")) {
        element.attribs["src"] = "需要改成的东西";
    }
});

html = $.html();

反正这里还有非常多的东西需要修改,但是已经初具雏形。接下来要实现的非常重要的功能便是下载功能。由于主力生产在Windows上,所以第一反应是powershell,然而经过几次的尝试,并未成功(后来发现其实只是忘记开始了而已)

所以最后选择的思路是使用cURL。本来我以为cURL只有Linux上有,在Windows上要采取别的实现方式,但是突然之间我发现我多虑了:

接下来就是思考如何通过cURL的参数来绕过referrer的限制:

最后采取的方法:

curl -o <file_dir> -e "<referrer>" <link>

其中file_dir为保存的位置,referrer字面意思,link为下载链接。在Windows里跑了一下,发现也能运行,于是乎我就放心大胆地上了。由于原理和之前失败的powershell一样,都是通过child_process,所以这次仔细查阅了文档,得到了下面的代码:(至于console.log大家忽略就好了(悲,其实那一块就是管道的重定向,问题不是很大)

更新:突然想起来,好像也可能是因为npm忘记装child_process了(我自裁):

npm install child_process

如果觉得太慢,可以前面加上proxychains4

import { spawn } from 'child_process'

...

const curl = spawn('curl', ['-o', path.join(fileDir, path.basename(link)), '-e', '"<referrer>"', link]);
curl.stdout.on('data', (data) => {
    console.log(`stdout: ${data}`);
});
curl.stderr.on('data', (data) => {
    console.log(`stderr: ${data}`);
});
curl.on('close', (code) => {
    console.log(`child process exited with code ${code}`);
});

第一次跑的时候由于没有像上面那样设置好了path所以出现了write permission denied的情况。我百思不得其解,于是乎当时就直接跑了一下pwd命令(使用同样的方法):

const pwd = spawn('pwd');
pwd.stdout.on('data', (data) => {
    console.log(`stdout: ${data}`);
});
pwd.stderr.on('data', (data) => {
    console.log(`stderr: ${data}`);
});
pwd.on('close', (code) => {
    console.log(`child process exited with code ${code}`);
});

得到的是Program Files里面的路径,于是乎就解释地通了。

现在我的想法是,针对每一个文件,将里面地图片下载在同目录的同名文件夹内。经过搜索和先前的经验,现给出如下代码:

// Get file directory and file name
let fileDir = vscode.window.activeTextEditor.document.fileName;
let fileName = path.basename(fileDir);

fileDir = fileDir.substr(0, fileDir.length - 3);
fileName = fileName.substr(0, fileName.length - 3);

if (!fs.existsSync(fileDir)) {
    fs.mkdirSync(fileDir);
}

注意,随后实在判空,如果文件夹不存在就直接创建新文件夹。做完这部之后其实剩下的只需要拼接起来即可,但是没想到这仍然有问题。

我们会发现,我们最后html<img>标签的src属性应该是下载下来的文件路径。我一开始看的是VSCode官方是如何实现的:

我发现他是直接用相对路径的。天真无知的我欣喜若狂,以为事情就这样结束了。我如法炮制:一开始给出的路径是:

`./${fileName}/path.basename(link)`

但是运行之后却发现渲染不出来。我当时注意到,文件名之间有空格:

我当时便认定大概是空格的问题,为了验证这个想法,我随便新建了一个文件,尝试在图片路径中加入空格,果然:

无法顺利渲染。可能是夜晚使人脑子清醒,我想起了浏览器当中会自动把空格转换成%20,于是顺着这个思路去尝试了一番:

可以正确显示图片,随后我便认定了是空格的原因。搜索后得到了encodeURI的处理方法,我将前面的表达式更新为:

encodeURI(`./${fileName}/path.basename(link)`)

但是,调试后仍然发现不行。为了查明问题所在,我决定使用这个插件查看正常情况下图片是如何显示的:

真相逐渐浮出水面,是WebView里面文件协议的问题。以前开发LaTeX插件的时候我记得教程里提过这个问题,便去搜索,顺利找到了原文出处:

按照前面的方法试了一下,仍然不行,并且发现协议为vscode-resource://,而非vscode-webview-resource://。综合了一下教程发布的时间,得到了上述协议已经失效的(大概)的结论。

梅开二度,继续搜索,去VSCodeGithub上面的issues底下找到了答案:https://github.com/microsoft/vscode/issues/102959

注意到,前文使用的vscode-resource://协议为old way,发布时间为2020年,印证了我的猜想。使用了新方法尝试之后仍然不行。思考后决定把之前加上的encodeURI删去,才顺利显示,最终这部分的代码:

// Get file directory and file name
let fileDir = vscode.window.activeTextEditor.document.fileName;
let fileName = path.basename(fileDir);

fileDir = fileDir.substr(0, fileDir.length - 3);
fileName = fileName.substr(0, fileName.length - 3);

if (!fs.existsSync(fileDir)) {
    fs.mkdirSync(fileDir);
}
// replace the image attributes
const $ = cheerio.load(html);
$("img").each((_index, element) => {
    let link = element.attribs["src"];
    if (link.startsWith("<prefix>")) {
        element.attribs["src"] = this.getPreview(sourceUri).webview.asWebviewUri(vscode.Uri.file(path.join(fileDir, path.basename(link)))).toString();
        if (!fs.existsSync(path.join(fileDir, path.basename(link)))) {
            const curl = spawn('curl', ['-o', path.join(fileDir, path.basename(link)), '-e', '"<referrer>"', link]);
            curl.stdout.on('data', (data) => {
                console.log(`stdout: ${data}`);
            });
            curl.stderr.on('data', (data) => {
                console.log(`stderr: ${data}`);
            });
            curl.on('close', (code) => {
                console.log(`child process exited with code ${code}`);
            });
        }
    }
});

html = $.html();

至此,魔改告一段落,主要功能基本全部实现:

也许之后可能也会做一个Configuration的界面?大概吧,还是看心情。

打包安装

在一切准备完毕之后,由于里面涉及到太多个人链接,所以懒得做设置界面和发布了,直接通过vsix打包。如果没有安装过vsix的话:

npm install vsix

安装完毕之后执行打包命令:

vsix package

发现报错:

所以带上--no-yarn的参数再次打包:

再次报错,通过报错信息定位到preview-content-provider.ts文件内,发现问题所在,删除该行:

估计是之前手抖不小心加的(裂。

最后再次打包,成功:

由于我需要在Windows内安装(等会Linux内也装一把),反正把文件移到别处,然后再在如下位置安装:

真香,添加设置界面

转念一想,好像添加设置没什么难的,于是就做了。打开package.json,找到configuration字段,在末尾添加:

"markdown-preview-enhanced.fake-referrer": {
    "description": "The referrer used to download images from restricted servers.",
    "default": "",
    "type": "string"
},
"markdown-preview-enhanced.restricted-prefixes": {
    "description": "If the image link has one of the following prefixes, it will be downloaded using referrer previously configured.",
    "default": [],
    "type": "array"
}

看到作者有专门给config写过config.ts,所以直接到里面去改:

// referrer config
public readonly fakeReferrer: string;
public readonly restrictedPrefixes: string[];

...

this.fakeReferrer = config.get<string>("fake-referrer");
this.restrictedPrefixes = config.get<string[]>("restricted-prefixes");

然后去改一下代码:

$("img").each((_index, element) => {
    let link = element.attribs["src"];
    this.config.restrictedPrefixes.forEach((prefix) => {
        if (link.startsWith(prefix)) {
            element.attribs["src"] = this.getPreview(sourceUri).webview.asWebviewUri(vscode.Uri.file(path.join(fileDir, path.basename(link)))).toString();
            if (!fs.existsSync(path.join(fileDir, path.basename(link)))) {
                const curl = spawn('curl', ['-o', path.join(fileDir, path.basename(link)), '-e', `"${this.config.fakeReferrer}"`, link]);
                curl.stdout.on('data', (data) => {
                    console.log(`stdout: ${data}`);
                });
                curl.stderr.on('data', (data) => {
                    console.log(`stderr: ${data}`);
                });
                curl.on('close', (code) => {
                    console.log(`child process exited with code ${code}`);
                });
            }
        }
    });
});

设置界面:

准备再次发布

首先把这个项目开源了问题也不大,所以就找到原作者的GitHubfork一把:然后再clone到本地,中途用一下proxychains4:

package.json里做一些些修改:

再对LICENSE和其他设置谁做修改就可以进行发布了:

vsce publish --no-yarn

Marketplace的链接:https://marketplace.visualstudio.com/items?itemName=JeffersonQin.blogging-tool

一件小事

用这个插件的时候,右键想要导出pdf发现提示:

搜索,安装:https://www.princexml.com/

但是仍然出现相同提示,转念一想,可能是PATH的原因。打开安装目录,找到路径,然后将C:\Program Files (x86)\Prince\engine\bin添加进PATH,成功解决问题。

结束语

这种项目以后谁爱做谁做去,虽然不是特别难,但是和原作者斗智斗勇真的血腥(逃)。而且完全不熟悉前端(可能只是因为我菜),靠着搜索引擎和感觉来写代码(这真的是好文明嘛?)。不过感觉,与其最后只记录答案是如何得到的,不如像现在这样把失败的过程记录下来,反思思考,写下自己脑中的推断过程,可能才会学到更多的东西,帮助到更多的人(大概吧)

最后修改:2021 年 02 月 05 日 11 : 16 AM
真的不买杯奶茶嘛....qwq