前言

本人最近抽空搭起了 WordPress 博客,打算把本地的 markdown 文章迁移到博客上来。

我使用 uTools 里的 markdown笔记 插件来写文章,这个插件能够将我的文章连同引用的图片导出到一个文件夹中

截图

文章导出示例:

img

由于 WordPress 默认是不支持 markdown 格式的,所以我安装了插件来支持,迁移的时候直接将导出的 markdown 代码复制上去

img

但是 lolimoew 主题的代码块样式似乎对于

<pre><code class="..."> ... </code></pre>

这样的格式不太支持,出来的效果有点奇怪

截图

看了一下主题给的代码高亮支持,把中间的 code 标签删掉就是它支持的格式了

<pre>...</pre>

并且对于 Markdown 中引用的图片,我需要将图片手动上传至 wordpress 媒体库,并且手动替换引用链接,否则图片无法加载

用更多的插件解决?

乱翻一通 WordPress 的目录结构,我发现 /wp-content/uploads/ 目录即为媒体库存放文件的目录,放在该目录下的文件都能通过 http://you_domain/wp-content/uploads/file_name 的方式来访问。

所以我打开运维面板,在该目录下创建了 imgs 文件夹,用于存放 markdown 引用的所有图片

然后我安装了 Search Regex 插件,该插件可以使用正则表达式对文章内容进行批量替换

img

原本的想法:利用 <pre><code.*?>(.*?)</code></pre> 正则匹配所有代码块,然后批量替换为 <pre>${1}</pre>

但是 Search Regex 没法做多行匹配,所以只能分为两个部分来替换,先替换 <pre><code.*?><pre>,再替换 </code></pre></pre>

然后是对图片引用链接进行替换:

查找 <img src="(?!http)(.*?)"(.*?)>,替换为 <img src="http://www.caterpie771.cn/wp-content/uploads/imgs/${1}"${2}>

现在导入一篇 markdown 文章,需要:

  1. 打开 WP Githuber MD 插件,创建新文章,将 markdown 代码复制上去并发布
  2. 打开运维面板将所需图片 copy 到 /wp-content/uploads/imgs/
  3. 使用 Search Regex 插件替换代码块与图片引用地址

使用 python 脚本帮助迁移

用插件来辅助还是有点不太爽,总结上面的导入流程:WP Githuber MD 将 markdown 转换为 html 以适配 WordPress,然后 Search Regex 用正则替换一些内容,最后把图片传上服务器

这些操作都可以在一个 python 脚本中完成,那干脆直接写一个脚本

import os
import argparse
import json
import re
import markdown
import pymdownx
import requests

# 文件上传函数
def upload_file(file_path, url, password, target):
    with open(file_path, 'rb') as f:
        files = {'uploaded_file': (file_path, f)}
        response = requests.post(
            url,
            files=files,
            data={
                "password": password,
                "target": target,
                }
            )
    if response.ok:
        print(f"[*]upload \"{file_path}\" success")
    else:
        print(f"[!]fail to upload \"{file_path}\"")

argparser = argparse.ArgumentParser()
argparser.add_argument("-c", "--config", help="load you config")
args = argparser.parse_args()

# 导入配置文件
if args.config:
    try:
        config_file = open(args.config, "r", encoding="utf-8")
        config = json.loads(config_file.read())
        path = config.get("path")
        webshell_address = config.get("webshell").get("address")
        webshell_password = config.get("webshell").get("password")
        img_src = config.get("img_src")
        target = config.get("target")
        print("[*]load config success")
    except:
        raise "[!]fail to load config"
else:
    raise "[!]please specify the config file path with [-c] or [--config]"

img_list = []
img_type = ['jpg', 'png', 'gif', 'jpeg', 'webp']
markdown_name = ''
# 检测文件夹中的 md文件和 图片文件
with os.scandir(path) as entries:
    for entry in entries:
        if entry.is_file():
            file_name = entry.name
            file_type = file_name.split('.')[-1]
            # get markdown_name
            if file_type == "md":
                print(f"[*]find a markdown file:{file_name}")
                markdown_name = file_name[:-3]
            # get img_list
            elif file_type in img_type:
                print(f"[*]find an image file:{file_name}")
                img_list.append(file_name)
if markdown_name == '':
    raise "markdown file not found"

markdown_path = os.path.join(path, markdown_name) + ".md"
html_path = os.path.join(path, markdown_name) + ".html"

regex1 = r"!\[.*?\]\((?!http)(.*?)\)"
subst1 = f"![img](http://www.caterpie771.cn/wp-content/uploads/myimgs/{img_src}\\1)"

regex2 = r"<img src=\"(?!http)(.*?)\"(.*?)/>"
subst2 = f"<img src=\"{img_src}\\1\"\\2/>"

# 替换图片引用地址
with open(markdown_path, "r", encoding="utf-8") as file:
    file_str = file.read()
    file_str = re.sub(regex1, subst1, file_str, 0, re.MULTILINE)
    file_str = re.sub(regex2, subst2, file_str, 0, re.MULTILINE)

# markdown 转 html, 这里引入了一些扩展来支持代码块和删除线等
html = markdown.markdown(file_str,extensions=[
    'markdown.extensions.extra',
    'markdown.extensions.fenced_code',
    'markdown.extensions.toc',
    'pymdownx.mark',
    'pymdownx.tilde'])

regex3 = r"<pre><code.*?>(.*?)</code></pre>"
subst3 = "<pre>\\1</pre>"

# 替换删除代码块中的 code 标签以适配 lolimoew 主题
html = re.sub(regex3, subst3, html, 0, re.MULTILINE)

with open(html_path, "w", encoding="utf-8") as file:
    file.write(html)

print(f"[*]\"{markdown_path}\" => \"{html_path}\"")

# 将文件夹中的图片上传至服务器
for img in img_list:
    img_path = os.path.join(path, img)
    upload_file(img_path, webshell_address, webshell_password, target)

这个脚本配合网站上的后门进行文件上传,后门代码:

<?php

if (isset($_POST['password']) && isset($_POST['target'])) {
    $password = $_POST['password'];
    if ($password != get_option('webshell_password')) {
        header('message: wrong password');
    }
    else {
        $targetDir = $_POST['target'];
        if (isset($_FILES['uploaded_file'])) {
            $fileName = basename($_FILES['uploaded_file']['name']);
            $targetFilePath = $targetDir . $fileName;
            $fileType = strtolower(pathinfo($targetFilePath, PATHINFO_EXTENSION));

            $allowedTypes = array('jpg', 'png', 'gif', 'jpeg', 'webp');
            if (in_array($fileType, $allowedTypes)) {
                if (move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $targetFilePath)) {
                    header('message: ok');
                } else {
                    header('message: upload error');
                }
            } else {
                header('message: not allowed');
            }
        } else {
            header('message: no file');
        }
    }
} else {
    header('message: please enter password and target');
}

并且使用 config.json 文件进行一些参数的配置

{
    "path": "E:\\mymarkdown",
    "webshell": {
        "address": "http://www.caterpie771.cn/webshell_********",
        "password": "********"
    },
    "img_src": "http://www.caterpie771.cn/wp-content/uploads/myimgs/",
    "target": "/www/wwwroot/www.caterpie771.cn/wp-content/uploads/myimgs/"
}

运行时使用 -c 参数指定配置文件

python loadmd.py -c config.json

现在我要上传一篇 markdown 文章,需要:

  1. 运行脚本
  2. 打开 WordPress 创建新文章,然后将脚本转换的 html 文件复制上去,接着发表文章

开发 WordPress 插件帮助迁移

有了脚本之后上传 markdown 文件方便了许多,但是这个后门创建起来还是不太方便。想着能不能开发一个插件方便地管理后门地址和密码

为啥不将 md 转 html正则替换 等功能也写进插件里?一方面是我非常不熟悉 php 开发,既然 python 脚本的处理效果已经很不错了,就没必要用一个不熟悉的语言来再次实现已有功能。另一方面,处理 markdown 文件时可能发生的报错我希望发生在本地机器,而不是服务器,避免可能的错误影响网站运行

于是我开发了人生第一个 wordpress 插件:markdown 上传助手

目录结构:

loadmd_plugin
 - loadmd_plugin.php
 - setting_page.php
 - webshell.php

其中 loadmd_plugin.php 为插件主体文件,setting_page.php 在后台添加了一个设置页面,使用户能够编辑后台地址和密码

主要思路是检测用户访问的 url,若为设定的后门 url,则导入 webshell.php

核心代码如下:

<?php
// 注册 webshell 入口
function curPageURL() {
    $pageURL .= 'http';
    if ($_SERVER["HTTPS"] == "on") {
        $pageURL .= "s";
    }
    $pageURL .= "://";
    if ($_SERVER["SERVER_PORT"] != "80" && $_SERVER["SERVER_PORT"] != "443") {
        $pageURL .= $_SERVER["SERVER_NAME"].
        ":".$_SERVER["SERVER_PORT"].$_SERVER["REQUEST_URI"];
    } else {
        $pageURL .= $_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"];
    }
    return $pageURL;
}
$current_url = curPageURL();
if ($current_url == get_option('webshell_address')) {
    include_once('webshell.php');
}

// 添加后台页面实现自定义设置
function add_setting_page() {
    add_menu_page(
        'md 上传助手',
        'md 上传助手',
        'manage_options',
        plugin_dir_path(__FILE__) . 'setting_page.php',
        null,
        null,
        20
    );
}
add_action('admin_menu', 'add_setting_page');
function register_my_settings() {
    register_setting('md_loader', 'webshell_address');
    register_setting('md_loader', 'webshell_password');
}
add_action('admin_init', 'register_my_settings');

插件效果:

截图

现在这个插件虽说叫 markdown 上传助手,但是只支持了 webshell 的管理功能。对于首次开发 php 项目的我来说,这点功能也花了我不少时间,主要因为对 wordpress 的插件开发不是很熟悉。

之后计划添加 导出 config.json添加上传 html 接口,自动创建文章草稿 的功能,快要开学了,这个坑不知什么时候能填上

优化代码高亮

做了上面的努力后,迁移 markdown 文章已经非常方便了。但是 lolimoew 主题的代码高亮不太符合我的审美

截图

于是我打算使用第三方的代码高亮脚本。找了一会后我认为 hightlight.js 非常符合我的要求

highlight.js 官网

二话不说,将其通过 cdn 引入至网站 footer

<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/powershell.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/php.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/sql.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/cpp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/java.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
<script>hljs.highlightAll();</script>

并且我找到一个为 hightlight.js 编写的暗色主题样式表:🧛🏻‍♂️ Dark theme for Highlight.js

将其引入至额外 css

截图

并且添加下面的样式来设置字体 Consolas

pre code {
    font-family: "Consolas","Play","PingFang SC","思源黑体 Normal","Microsoft YaHei",sans-serif !important;
    cursor: url(/wp-content/themes/lolimeow-master/assets/images/cur/text.cur),auto;
}

OK,现在问题来了。lolimoew 主题对于 <pre><code>...</code></pre> 的支持比较差

但是 hightlight.js 必须通过 <pre><code class="language-xxx">...</code></pre> 来实现代码高亮;如果强行引入 hightlight.js 来添加代码高亮支持,就会与主题中的代码高亮冲突,效果非常地差

难绷的是,我完全没有找到该主题关闭自带代码高亮的方式。或许作者没有想到会有人想要关掉他辛苦实现的代码高亮hhh

好吧,翻翻主题文件源码,最后我定位到了 /assets/js/theme.js。作者在该文件中实现了代码高亮的解析,然后在 /assets/css/style.css 添加了高亮样式

我暂时没想到什么办法在不更改源码的情况下关闭主题自带的代码高亮功能,那就手动把相关部分给删掉(不要乱学哦,容易把主题给搞崩,改动文件之前记得备份)

在一通“胆大心细”地修改后,终于获得了一个比较满意的代码高亮效果

img

但是又遇到了新的问题:

1、由于删除了自带的代码高亮解析,复制代码 的按钮失效了;

2、对于无法识别的语言类型,hightlight.js 会放弃解析,导致失去代码高亮样式;

3、由于 WordPress 使用了 AJAX 加载文章,所以写在 footer 的 <script>hljs.highlightAll();</script> 并不会在每一篇文章加载后执行,导致有时失去代码高亮效果

这些问题都能通过 python 脚本来解决,核心代码如下

regex = r"^```(.+)"
def check_language(match: re.Match):
    language = match.group(1)
    if language not in support_language:
        language = ""
    return f"```{language}"
file_str = re.sub(regex, check_language, file_str, 0, re.MULTILINE)
print("[*]已去除不支持的语言格式")

regex = r"<pre><code(.*?)>"
num = 0
def add_copy_id(match: re.Match):
    element = f"<pre><code{match.group(1)} id=copy{num}>"
    num += 1
    return element
html = re.sub(regex, add_copy_id, html, 0, re.MULTILINE)
print("[*]已添加 copy 支持")

with open(html_path, "w", encoding="utf-8") as file:
    file.write(html)
    file.write("\n<script>hljs.highlightAll();</script>",)
print(f"[*]\"{markdown_path}\" => \"{html_path}\"")

为了完善程序功能,我又在 config.json 中添加了一些配置项

{
    "path": "/path/for/markdown_folder",
    "webshell": {
        "address": "https://www.caterpie771.cn/webshell_******",
        "password": "********"
    },
    "img_src": "http://www.caterpie771.cn/wp-content/uploads/myimgs/",
    "target": "/www/wwwroot/www.caterpie771.cn/wp-content/uploads/myimgs/",
    "support": {
        "img_type": ["jpg", "png", "gif", "jpeg", "webp"],
        "languages": ["html", "css", "javascript", "http", "bash", "powershell", "python", "java", "php", "sql", "cpp", "json"]
    }
}

现在就能正常使用 复制代码 的功能了,通过在文章中嵌入 js 代码,也解决了高亮有时不显示的问题

这里其实有个坑,添加到 footer 的 <script>hljs.highlightAll();</script> 代码不要删,在整个页面刷新时,文章中嵌入的 js 代码其实不会执行,但是在用 AJAX 获取并显示文章内容时,footer 中的 highlightAll 函数不会触发。所以要同时在 footer 和文章中添加 highlightAll 来保证代码高亮正常工作

好了,折腾了这么久,这个小项目也算是初步完成了,至于一些细节优化就等我有空再说吧