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:
@@ -522,8 +522,28 @@ if ($method === 'PROPFIND') {
|
||||
|
||||
// ---- 6. GET / HEAD:整本导出成 ics,或单条资源 ----
|
||||
if ($method === 'GET' || $method === 'HEAD') {
|
||||
/* 🔴🔴 v45b(2026-10-12 用户「TB 还是找不到日历」)——**真 bug 修在这里**。
|
||||
原来这一段是:
|
||||
if (!$cal) { dav_fail(404, '没有这本日历。'); }
|
||||
而 `$cal` 在 `bookId === 0` 时恒为 null —— 也就是**请求的是 home 容器
|
||||
`/dav/` 本身**(不是某一本)时,一律回 **404**。
|
||||
|
||||
为什么这会让 Thunderbird「找不到日历」:
|
||||
TB 添加账号时先 `GET /.well-known/caldav` → 我们回 301 到 `/dav/`,
|
||||
**TB 会跟随这个跳转**(它跟随 GET 跳转,只是不跟随带鉴权的 PROPFIND 跳转),
|
||||
于是变成 `GET /dav/` ⇒ 拿到 **404「没有这本日历」**
|
||||
⇒ TB 判定"这个地址后面没有日历" ⇒ 报「找不到日历」。
|
||||
⚠️ 探针一直没抓到:`_fb_dav_tb.py` 只发 PROPFIND/REPORT,**从来不发这个 GET**;
|
||||
而 curl 默认也不跟随(要 -L)——本次用 `-L` 走了一遍才暴露。
|
||||
|
||||
⇒ 对齐参照(DAViCal):容器上的 GET 回 **405 Method Not Allowed**,
|
||||
**不是 404**。语义差别很关键:
|
||||
· 404 = "这个资源不存在" ⇒ 客户端认为"这里没有日历"(致命);
|
||||
· 405 = "这个方法不行,但资源在" ⇒ 客户端改用 PROPFIND,正常工作。
|
||||
⚠️ Allow 里必须**不含 GET**(否则自相矛盾),且带上 PROPFIND/REPORT。 */
|
||||
if (!$cal) {
|
||||
dav_fail(404, '没有这本日历。');
|
||||
header('Allow: OPTIONS, PROPFIND, REPORT, PUT, DELETE, MKCALENDAR');
|
||||
dav_fail(405, 'GET 只能用于某一本日历或其中的条目;列日历本请用 PROPFIND。');
|
||||
}
|
||||
|
||||
// 6a. 单条资源(/dav/3.ics/12.ics)—— 客户端增量同步走的就是这条。
|
||||
@@ -576,8 +596,14 @@ if ($method === 'GET' || $method === 'HEAD') {
|
||||
|
||||
// ---- 7. REPORT:calendar-multiget(按 href 取) / calendar-query(整本或按时间范围)----
|
||||
if ($method === 'REPORT') {
|
||||
/* 🔴 v45b:REPORT 打到 **home 容器**(`/dav/`,bookId=0)时 ——
|
||||
照参照(DAViCal)回 **403**,不回 404。
|
||||
参照实测:`REPORT /caldav/<user>/` → `403 Forbidden`(不是 404)。
|
||||
404 会让客户端以为"整条路径都不存在",403 才是"这里不能这么查"。
|
||||
⚠️ REPORT 本来就该打到**某一本**上(calendar-query 需要一个 calendar 集合),
|
||||
打到容器上属于用错,403 是准确的状态码。 */
|
||||
if (!$cal) {
|
||||
dav_fail(404, '没有这本日历。');
|
||||
dav_fail(403, 'REPORT 要打到某一本日历上(如 /dav/1.ics/),不能打在容器上。');
|
||||
}
|
||||
$bodyRaw = (string)file_get_contents('php://input');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user