EMlog 的 REST 接口可以完成发文、改文、查分类、传图:入口统一是 https://站点/?rest-api=<方法名>。官方文档只给了参数列表,实际行为有几处和直觉不一致。下面三条是写发布脚本时踩到的,结论均来自源码核对(include/controller/api_controller.php、include/lib/input.php、include/model/log_model.php)。
1. 请求体只接受表单编码
现象:POST 一个 JSON 到 ?rest-api=article_post,返回 {"code":1,"msg":"parameter error"},HTTP 400。
原因:所有入参经 Input::postStrVar() 取值:
public static function postStrVar($var_name, $var_default = '')
{
$str = filter_input(INPUT_POST, $var_name);
return $str ? addslashes(trim($str)) : $var_default;
}
filter_input(INPUT_POST, ...) 只解析 application/x-www-form-urlencoded 和 multipart/form-data。JSON 请求体不会进入 $_POST,于是 title、content 全部取到默认空值,触发了发布方法里的这个分支:
if (empty($title) || empty($content)) {
Output::error('parameter error');
}
错误信息不区分"参数没传"和"参数没被解析",排查时容易往鉴权方向想。
写法:Content-Type: application/x-www-form-urlencoded。Node 侧 new URLSearchParams() 即可,上传接口例外,用 FormData。
2. article_update 是整行覆盖
现象:只想改标题,结果正文变空;只想换标签,封面丢失。
原因:更新方法组装的 $logData 无条件包含全部字段,updateLog() 把它们逐个拼进 SET 子句:
$logData = [
'title' => $title,
'content' => $content,
'excerpt' => $excerpt,
'sortid' => $sort_id,
'cover' => $cover,
'author' => $author_uid,
'date' => strtotime($post_date ?: date('Y-m-d H:i:s')),
'hide' => $draft === 'y' ? 'y' : 'n',
];
$this->Log_Model->updateLog($logData, $id, $author_uid);
没有"字段未传则跳过"的判断。缺省值直接落库:
| 未传参数 | 落库结果 |
|---|---|
content |
正文清空 |
cover |
封面清空 |
sort_id |
-1(未分类) |
post_date |
发布时间改为当前时间 |
draft |
hide='n',草稿被发布 |
写法:更新前先读回原记录,与本次要改的字段合并后整体提交。读取接口有两个:
article_detail:返回content(Parsedown 渲染后的 HTML)和content_raw(库中原文)。回填必须用content_raw,否则一轮编辑之后正文里全是标签。draft_detail:只返回hide='y'的记录,content即原文,无需二次判断。
两者的差异还能用来判断文章状态:先查 draft_detail,命中即草稿;未命中再查 article_detail,即为已发布。
let existing, isDraft;
try {
const d = await apiGet('draft_detail', { id }, { auth: true, soft: true });
existing = normalizeDraft(d.draft);
isDraft = true;
} catch {
const d = await apiGet('article_detail', { id });
existing = normalizeArticle(d.article); // 取 content_raw
}
const params = { id, ...existing, ...explicitOverrides };
3. 正文存原文,渲染发生在读取时
保存链路不做任何格式转换,读取时才过 Parsedown:
$this->Parsedown = new Parsedown();
$this->Parsedown->setBreaksEnabled(true); // 自动换行
...
'log_content' => $this->Parsedown->text($row['content']),
'content_raw' => $row['content'],
由此得出三条约束:
- 正文直接提交 Markdown。预先转成 HTML 会被再处理一次。
setBreaksEnabled(true)使单个换行输出<br>。段落分隔必须用空行,不能依赖行尾空格。- 后台编辑页由
editormd渲染源码面板,看到##、|、``` 属正常显示,不代表转换失败。
log_model 未调用 setSafeMode,因此原生 HTML 块(<img>、<table>、<details>)原样透传给浏览器。但自带的 include/lib/parsedown.php 没有 hideLines 实现,GitHub 风格的 ||| 折叠语法不会渲染,需要折叠用 <details><summary>。
默认值与开关
| 项 | 行为 |
|---|---|
allow_remark |
默认 n,即评论关闭;需显式传 y,且受全站评论总开关约束 |
sort_id |
默认 -1(未分类);分类 ID 由 sort_list 获取,该接口无需鉴权 |
draft |
y 映射为 hide='y',前台不可见,article_detail 返回 Article not found |
post_date |
经 strtotime() 解析;晚于当前时间不报错,但首页查询带 date<=now,届时才可见 |
| 鉴权 | api_key=<key>,或 req_time=<unix> + req_sign=md5(req_time . api_key),时间窗 ±300 秒 |
| API 开关 | 后台未开启时除 article_list 外一律返回 api is closed |
签名方式的价值在于密钥不出现在 URL 与访问日志里。前提是本机与服务器时钟差小于 300 秒,否则返回 request expired。
脚本
上述规则封装成单文件 CLI,无 npm 依赖(Node 18+ 自带 fetch/FormData/Blob):
node emlog.mjs post --file 文章.md --draft --tags "EMlog,Node.js" --comments
node emlog.mjs show --id 12 # 回读原文与元信息,校验字段是否丢失
node emlog.mjs update --id 12 --publish
node emlog.mjs upload --file 配图.png # 返回绝对 URL,直接写入正文
update 不带 --file 时沿用线上正文,只提交显式指定的字段;带 --file 时整体替换。密钥从 ~/secrets/emlog.env 读取,不作为命令行参数传入,且所有输出(含异常堆栈)统一把密钥替换为 ***。
一个限制
REST 接口没有删除文章的方法。验证链路产生的测试稿只能登录后台手动删除,因此测试一律用 --draft(前台不可见),正式内容也先落草稿、确认后再 update --publish。