Files
FamilyBook/router.php
T
laoyang 5da4de0d20 v33 第 33 批:DAV(Thunderbird)三处真根因修复
用户原话:
  看看 dav 功能,还是不好用,thunderbird 提示这两个地址下没有任何联系人和日程。

关键教训:v31 那个探针(复刻 SyncCalendar 的 DavClient)线上 28/0 全过,
但用户还是空的 —— 因为 Thunderbird 的请求形状不一样:它真正取数据用的是
REPORT(calendar-multiget / addressbook-multiget),而 v31 探针只走
PROPFIND + GET。复刻了 A 客户端全绿,证明不了 B 客户端能看见东西。

三处真根因(都靠新探针 _fb_dav_tb.py 逐步 dump 原始 XML 抓出):
① CardDAV 的 REPORT 回的是草案名 <card:address-data>,而 RFC 6352 规定是
   <card:addressbook-data>。Thunderbird 要后者 → 拿到 0 个数据块 → 通讯录永远空。
   修:新增 $cardDataXml 闭包,照客户端点名的属性回正文(没点名默认正式名)。
   注:第一版写成两个都发 → 正文重复一份、e2e 的 BEGIN:VCARD 计数翻倍,改成按需回。
② home-set 的 Depth:1 响应里没有集合自身。RFC 4918 要求 PROPFIND 响应必须包含
   被请求的资源本身,原来只在非 home 请求时才输出 → PROPFIND /dav/calendars/
   只回子本、没有自己 → 严格客户端判 home 不存在 → 一本都看不见。
   修:dav.php / carddav.php 的 home 分支始终先输出集合自身。
③ 站点根 PROPFIND / 被 Apache 交给 index.php → 302 跳登录页 → Thunderbird 判定
   这个域名下没有日历(RFC 6764 允许从域名根发现)。
   修:router.php(本地)+ .htaccess / 部署脚本 5c 段独立标记块(线上):
   RewriteCond 限住 REQUEST_METHOD + RewriteRule ^$ dav.php;
   必须限方法,否则 GET / 被 ^$ 吞掉、首页 404(已加反向守卫)。
   并让根响应与两个 principal 都给出两套 home-set(calendar + addressbook)。

验证:线上真机 _fb_dav_tb.py 25/0(站点根通 / home-set 含自身 / CardDAV REPORT
真带回 3 个数据块,修复前是 0);逐本 REPORT 所有集合都能取回数据;
旧诊断(SyncCalendar 序列)28/0 无回归;GET / 仍是 302 未被吞。
本地 e2e 1503/0、v33 DAV 反向验证 7/0(三处各自改回旧行为 → 断言全变红);
部署脚本静态断言段本地空跑通过。
2026-10-09 08:45:22 +08:00

84 lines
4.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 前端控制器(路由表)—— 只给 **PHP 内置服务器** 用(php -S ... router.php)。
*
* 为什么要它:
* · 线上是 Apache,`/dav/3.ics/` 这种没有对应文件的 URL 靠 .htaccess 重写到 dav.php;
* · 本地 `php -S` **不读 .htaccess**,所以同一个地址会被当成"文件不存在"直接 404,
* 于是「CalDAV 在本地测不了、只能上线验证」——这正是最容易上线才发现问题的写法。
* 这个文件把那几条重写规则用 PHP 重述一遍,本地与线上一模一样的路径行为。
* (v24 第 22 批加了 CardDAV,同样两条:/dav/ 与 /carddav/。)
*
* ⚠️ 里面**不写任何业务**:只决定"交给哪个脚本",所有业务仍在 index.php / dav.php / media.php。
* 线上部署不该带它(Apache 走 .htaccess),但带着也无害 —— 它不会被执行。
*/
$path = (string)parse_url((string)($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH);
// 🔴 v33:站点根的 DAV 请求(PROPFIND / REPORT / OPTIONS)→ dav.php。
// 为什么需要:RFC 6764 允许客户端从**域名根**开始发现 DAV 服务
// (`PROPFIND /` 问 current-user-principal),Thunderbird 就会走这条。
// Apache 把 `/` 交给 index.php(→ 302 登录页),客户端判定"这里没有日历"。
// ⇒ 只对 **DAV 方法**转发,`GET /` 仍是站点首页(绝不能把首页吞掉)。
// ⚠️ 线上等价物在 `.htaccess` 里(RewriteCond 判 REQUEST_METHOD)。
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
if (($path === '/' || $path === '') && in_array($method, ['PROPFIND', 'REPORT', 'OPTIONS'], true)) {
require __DIR__ . '/dav.php';
return true;
}
// 🔴 v31:`/.well-known/caldav` 与 `/.well-known/carddav`(RFC 6764)。
// 标准客户端(DAVx⁵ / iOS / Evolution)添加账号时**第一句**就试这里 ——
// 原来没有这条路由,于是落到 index.php、被 302 去登录页,
// 客户端判定"域名下没有 DAV 服务",连口令框都不弹。
// 症状正是用户报的「同步不出任何数据」(账号根本建不起来)。
// ⚠️ 必须排在下面 dav/carddav 通配两条**之前**?不需要(前缀互不包含),
// 但**必须**排在后面"真实文件"判断之前 —— 这个目录线上并不存在。
if ($path === '/.well-known/caldav') {
require __DIR__ . '/dav.php';
return true;
}
if ($path === '/.well-known/carddav') {
require __DIR__ . '/carddav.php';
return true;
}
// CalDAV:/dav/ 及其子路径(/dav/、/dav/3.ics/、/dav/3.ics/12.ics)
if (preg_match('#^/dav(/.*)?$#', $path)) {
require __DIR__ . '/dav.php';
return true;
}
// CardDAV:/carddav/ 及其子路径(v24 第 22 批)
// ⚠️ 这条必须在下面那条"真实文件"判断**之前**:carddav.php 本身是个真文件,
// 但我们要的是把 /carddav/1.vcf/ 这种**不存在的路径**交给它处理;
// 顺序反了的话 /carddav/ 会去要一个不存在的目录 → 404。
if (preg_match('#^/carddav(/.*)?$#', $path)) {
require __DIR__ . '/carddav.php';
return true;
}
// 导航**展示页**(v24 第 24 批 用户第 4 条):`/nav/?<短语>` —— 全站唯一免登录页面。
// ⚠️ 三条顺序**不能换**:
// ① `/nav/suggest.php` 是联想接口(免登录,但凭短语门禁放行);
// ② `/nav/` 恰好是展示页;
// ③ 两者都必须排在下面"真实文件"判断**之前** —— 否则 `/nav/` 会去找一个
// 不存在的目录 → 404(与上面 carddav 那条同一个坑)。
if ($path === '/nav/suggest.php') {
require __DIR__ . '/nav_suggest.php';
return true;
}
if (preg_match('#^/nav/?$#', $path)) {
require __DIR__ . '/nav_show.php';
return true;
}
// 真实存在的文件(静态资源、media.php 等)→ 交给内置服务器自己处理
$file = __DIR__ . $path;
if ($path !== '/' && is_file($file)) {
return false;
}
// 其余交给主路由
require __DIR__ . '/index.php';
return true;