v45b 修复:Thunderbird「找不到日历/联系人」的真根因

用户 2026-10-12 晚:「dav 还是不行,thunderbird 提示找不到日历,联系人也没有。」

真根因(一个由两半无缝衔接构成的 bug):
  TB 添加账号 → GET /.well-known/caldav → 我们 301 到 /dav/
              → TB **跟随这个 GET 跳转** → GET /dav/
              → 我们回 **404「没有这本日历」**
              → TB 判定"这里没有日历" ⇒ 报「找不到日历」。
  CardDAV 完全同型(/.well-known/carddav → GET /carddav/ → 404)
  ⇒ 所以日历和联系人是**同一个 bug**。

修法(对齐参照 DAViCal 的状态码语义):
  · GET 打到 home 容器(bookId=0)→ **405**(原来 404)
    404 = "资源不存在" ⇒ 客户端放弃;405 = "资源在、方法不行" ⇒ 客户端改用 PROPFIND。
  · REPORT 打到容器 → **403**(参照实测也是 403)。
  · dav.php 与 carddav.php **两处同修**(同型孪生 bug)。

顺带对齐参照(「完全按参照返回」口径):
  · 每个 XML 响应都带两行 `DAV:` 头(原来只有 OPTIONS 带)——
    CalDav::xmlHeader / CardDav::multis(所有 XML 响应的必经处)。
  · Content-Type 改 `text/xml`(参照的字面形态)。

探针补齐(这类 bug 长期漏网的三个原因):
  · 原 TB 探针只发 PROPFIND/REPORT,**从不发 GET /dav/**;
  · curl 默认不跟随跳转(要 -L);
  · 得先 GET .well-known 再跟到 /dav/ 才显形。
  ⇒ _fb_dav_tb.py 新增 ①c(follow=True 真跟跳转,断言跟随后不能 404、应为 405);
     _fb_dav_tb.py 支持 FB_SITE(供反向验证指向注入副本)。

验证:
  · TB 探针 35 → **39/0**(新增 4 条 ①c 全过)
  · v45b DAV 反向验证 **7/0**(新增 2 个注入:dav/carddav 容器 GET 改回 404
    —— 都已证明会让断言变红)
  · 全流水线 PUSH_RC=0
  · 线上真机复验:跟随 well-known 的 GET 跳转 → CalDAV 405 / CardDAV 405
    (修复前都是 404);PROPFIND 带两行 DAV 头 + text/xml;
    发现链 207、8 个日历本、1 个通讯录。
This commit is contained in:
2026-10-13 00:29:55 +08:00
parent 4ccbcc3360
commit feff31baad
4 changed files with 67 additions and 6 deletions
+15 -2
View File
@@ -358,8 +358,20 @@ if ($method === 'PROPFIND') {
// ---- 6. GET / HEAD:整本导出成一个 vcf,或单张卡 ----
if ($method === 'GET' || $method === 'HEAD') {
/* 🔴🔴 v45b(2026-10-12 用户「联系人也找不到」)—— 与 CalDAV 那边**同一个 bug**,
一并修(dav.php 已修,这里是它 CardDAV 的孪生兄弟)。
原来:请求 home 容器(`/carddav/`,bookId=0)时回 **404**。
为什么这会让 Thunderbird「找不到通讯录」:
① TB 先 `GET /.well-known/carddav` → 301 到 `/carddav/`;
② TB **跟随这个 GET 跳转** → `GET /carddav/` → 拿到 404
⇒ 判定"这个地址后面没有通讯录" ⇒ 报「找不到通讯录」。
⚠️ 实测证据(线上真实公网,修复前):
`curl -L .../.well-known/carddav` → 最终 `GET /carddav/` → **404**。
⇒ 对齐参照(DAViCal 对容器 GET 回 405)与 dav.php 的修法:
回 **405 Method Not Allowed**("资源在,但这个方法不行"),不是 404。 */
if ($bookId === 0) {
cd_fail(404, '没有这本通讯录。');
header('Allow: OPTIONS, PROPFIND, REPORT');
cd_fail(405, 'GET 只能用于某一本通讯录或其中的卡片;列通讯录请用 PROPFIND。');
}
// 6a. 单张卡(/carddav/1.vcf/12.vcf)。
@@ -410,8 +422,9 @@ if ($method === 'GET' || $method === 'HEAD') {
// ---- 7. REPORT:addressbook-query(按条件) / addressbook-multiget(按 href 取)----
if ($method === 'REPORT') {
/* 🔴 v45b:与 dav.php 同一处对齐 —— REPORT 打到 home 容器上时回 403(不是 404)。 */
if ($bookId === 0) {
cd_fail(404, '没有这本通讯录。');
cd_fail(403, 'REPORT 要打到某一本通讯录上(如 /carddav/1.vcf/),不能打在容器上。');
}
$bodyRaw = (string)file_get_contents('php://input');