ZBlog踩坑实录:XML-RPC的九九八十一难

ZBlog踩坑实录:一个XML-RPC接口的九九八十一难
如果你是一个ZBlog多站运营者,尤其是同时维护着bjlhyg、linghuo、waimaopeixun等站点,你一定对ZBlog的XML-RPC接口又爱又恨。这个看似标准的接口协议,在真实的生产环境中埋了无数的坑。本文就是一份用血泪换来的踩坑实录。
以下所有问题,均来自上述三个站点的实际运维过程。如果满分10分,我愿给ZBlog的XML-RPC接口打3分——能跑,但每一步都是折磨。
01 linghuo站:XML-RPC返回401/403,urllib直发绕过
linghuo站是最早接入自动化发布的站点。脚本用标准的xmlrpc.client.ServerProxy调用metaWeblog.newPost,突然有一天所有请求都返回了401 Unauthorized或403 Forbidden。检查了用户名密码、应用密码、IP白名单,一切正常。
排查了整整两天,最终发现:ZBlog的XML-RPC处理器对Python标准库xmlrpc.client发送的某些HTTP头部存在兼容性问题。具体来说,xmlrpc.client在发送请求时采用的User-Agent和Content-Type组合,在某个版本的PHP环境下被误判为恶意请求。
解决方案:弃用xmlrpc.client,改用urllib.request手动构建XML-RPC请求体,再解析返回的XML。代码核心如下:
import xml.etree.ElementTree as ET
import urllib.request
def xmlrpc_call(url, method, params):
payload = xmlrpc.client.dumps(params, methodname=method)
req = urllib.request.Request(
url, data=payload.encode(),
headers={
'User-Agent': 'Mozilla/5.0 (compatible)',
'Content-Type': 'text/xml'
}
)
resp = urllib.request.urlopen(req)
return xmlrpc.client.loads(resp.read())[0][0]
Tip:用urllib直发可以精细控制请求头,但需要自己处理编码和异常。建议封装成工具函数,统一处理ZBlog的特殊响应。
02 linghuo站:目录权限问题导致图片上传失败 → Base64内嵌方案
通过metaWeblog.newMediaObject上传图片到linghuo站时,明明返回了成功({"url": "..."}),但访问图片链接始终是404。检查服务器发现——文件压根没写进去。
原因:ZBlog的zb_users/upload/目录权限不足,PHP进程没有写入权限。更坑的是,newMediaObject在写入失败时并不会返回错误,而是假装成功,返回一个拼接好的假URL。这个bug让排查白白浪费了一整天。
解决方案:放弃newMediaObject上传,改用Base64内嵌图片的方式。将图片Base64编码后直接拼接到文章HTML的<img>标签中:
import base64
def embed_image(image_path):
with open(image_path, 'rb') as f:
b64 = base64.b64encode(f.read()).decode()
ext = image_path.rsplit('.', 1)[-1]
return f'data:image/{ext};base64,{b64}'
Tip:Base64内嵌适合图片数量少、体积小的场景(每站图片控制在200KB以内)。优点是零权限问题、零文件管理负担。缺点是文章体积变大,且不便于后续更换CDN——但如果你的站点图片本来就没上CDN,这反而是最稳定的方案。
后来bjlhyg和waimaopeixun站也统一采用了此方案,再也没有因为"上传成功但看不到图"的问题来骚扰运维了。
03 metaWeblog.newMediaObject → 本地自动下载 → Permission denied
这个坑最让人哭笑不得。ZBlog在处理newMediaObject请求时,会自动从远程URL下载图片到本地服务器——即使你传的bits参数是图片的二进制数据,而不是URL。
ZBlog源码中有一段逻辑:如果检测到bits参数的内容看起来像是一个URL(以一个可识别的图片扩展名结尾),它会首先尝试file_get_contents去拉取这个URL到本地临时目录,然后再移动到上传目录。如果临时目录(通常是/tmp或PHP的sys_get_temp_dir)没有写入权限,就会报Permission denied——且错误信息完全不指向真正的根因。
解决方案:
- 方案A(推荐):直接用Base64内嵌,彻底绕过
newMediaObject。 - 方案B:确保
bits参数是纯粹的二进制数据,不要让它被误识别为URL。可以在bits前加一个无关前缀确保它不是URL格式。 - 方案C:修改服务器
/tmp权限为777或给PHP进程单独配置临时目录。
我们在bjlhyg站上采用的是方案A,一次修复,永久见效。
04 删除权限问题:发布账号可发不可删
这是一个经典的权限设计缺陷。ZBlog中,使用文章发布者(Author)角色通过XML-RPC发布的文章,在尝试调用metaWeblog.deletePost删除时,会返回权限不足。但奇怪的是——同样的文章,在后台Web管理界面中,该账号是可以删除的。
深度排查后发现:ZBlog的XML-RPC删除接口在做权限校验时,使用了与Web管理界面不同的校验逻辑。XML-RPC接口额外检查了一个叫做can_delete_all_posts的内部权限标志,而这个标志只有管理员(Administrator)角色才有。
解决方案:
- 临时方案:为发布账号赋予管理员角色——但这对多站运营来说风险较大。
- 推荐方案:在ZBlog后台的用户管理 → 权限设置中,找到"XML-RPC删除"选项(某些主题/插件版本才有),手动赋权。
- 最终方案:对waimaopeixun站,我们写了一个简单的PHP插件,hook到XML-RPC删除接口,将权限校验逻辑替换为与Web后台一致。
Tip:如果你没有修改ZBlog源码的权限,可以在发布后标记文章状态为"草稿"而非"删除",用状态管理替代删除操作。
05 发布无标题文章的修复
这个坑看起来很小,但在批量发布时足以让整个Pipeline崩掉。当通过XML-RPC发布一篇文章时,如果title参数为空字符串,ZBlog会静默失败——不报错、不返回异常,只是文章没有被创建。
更坑的是,如果你传了title=""(空字符串),ZBlog的XML-RPC处理器会直接将整个请求丢弃,连日志都不会留下。而如果你不传title字段,某些版本的ZBlog又会报Missing parameter。
解决方案:在发布前强制检查title,如果为空则生成一个默认标题:
title = article_data.get('title', '').strip()
if not title:
import datetime
title = f"无标题文章 - {datetime.date.today().isoformat()} - {hash(article_data['content']) % 10000:04d}"
后来我们对bjlhyg、linghuo、waimaopeixun三个站的所有发布脚本都加上了这个修复,并额外加了一个标题非空断言,一旦发现空标题就记录告警。
06 封面图过期403 → 转存到稳定CDN
封面图是文章的门面,但也是最容易出问题的静态资源。我们在waimaopeixun站遇到了一个诡异的场景:文章发布后封面图正常显示,但过了一段时间(通常是7~30天)后,变成了403 Forbidden。
排查后发现,封面图引用的是某个第三方素材站的外链,该素材站启用了Referer防盗链 + 时效签名。ZBlog在发布时拿到了临时的有效URL并写入文章,但签名过期后就无法访问了。
解决方案:建立自己的封面图转存机制。在发布Pipeline中增加一个步骤:
import hashlib, requests
def transship_cover(cover_url, cdn_base):
resp = requests.get(cover_url, timeout=10)
ext = cover_url.rsplit('.', 1)[-1].split('?')[0]
filename = hashlib.md5(cover_url.encode()).hexdigest() + '.' + ext
# 上传到自己的CDN / 对象存储
upload_to_cdn(resp.content, filename, cdn_base)
return f"{cdn_base}/{filename}"
我们将这个转存逻辑应用到了bjlhyg和linghuo站。所有封面图在发布时都会被下载、计算MD5、上传到阿里云OSS(配CDN域名)。从此再也没出现封面图过期403的问题。
Tip:转存时建议保留原始图片的格式和尺寸。如果CDN支持图片处理(如WebP自适应、缩放),可以在URL上加参数,不要修改源文件。
总结
ZBlog的XML-RPC接口就像一个布满暗礁的海域。每个站点(bjlhyg、linghuo、waimaopeixun)都在不同的地方触过礁。下面是最终的解决方案清单,供所有ZBlog多站运营者参考:
| 问题 | 影响站点 | 最终方案 |
|---|---|---|
| 401/403认证失败 | linghuo | urllib直发手动构建请求 |
| 图片上传目录权限 | linghuo | Base64内嵌图片 |
| newMediaObject自动下载 | bjlhyg | Base64内嵌 + 禁用newMediaObject |
| 删除权限不足 | waimaopeixun | 自定义权限插件 / 状态管理替代 |
| 无标题文章静默失败 | 全部 | 发布前强制生成默认标题 |
| 封面图过期403 | waimaopeixun | 发布时转存到稳定CDN(OSS) |
最后想说的是:ZBlog虽然是个老牌的PHP博客系统,但在XML-RPC这个接口上,显得有点"年久失修"。如果你也遇到了类似的坑,欢迎补充这个踩坑清单。毕竟,多站运营的痛,只有多站运营的人才懂。