用户原话:「carddav 和 caldav 好像都不好用,没有任何数据被同步出来。
同步的客户端,在 D:/Yongquan/code/SyncCalendar 中有,你可以用这个里面的代码试试同步一下。」
三处真根因**全部靠实测**拿到(不是"看代码猜"):写了 _fb_dav_diag.py,
逐字复刻 SyncCalendar 的 DavClient 请求序列(PROPFIND/REPORT/GET + 它那几个 XML 体),
打线上一次就复现出用户症状(15 条失败),修完线上 28/0。
1. 发现链(discovery)**整段缺失** ← 这就是"没有任何数据"的根因
标准客户端(DAVx⁵ / iOS / SyncCalendar)建账号按 RFC 6764 走:
GET /.well-known/caldav → 302 到 /dav/ ← 原来 404(落到 index.php 去登录页)
PROPFIND <上下文路径> → current-user-principal ← 原来回的是"日历本清单",没有 principal
PROPFIND <principal> → calendar-home-set ← 原来没这个属性
PROPFIND <home> Depth:1 / <book> / REPORT ← 这两步一直是通的
① 就断 ⇒ 客户端**在填完账号点保存那一刻**就失败,一条数据都拉不到。
为什么历次验收全绿:上批只验了 PROPFIND /dav/ 与 /carddav/ —— 恰好是**一直通**的那两步,
断点在其**前面**,从来没被测过。
改:两个入口补 PRINCIPAL_PATH/HOME_PATH + /.well-known/* 301(**在鉴权之前**)+
根/principal/列表三处都给 current-user-principal。.htaccess 用**独立标记块**追加
well-known 重写规则(本仓库第 4 次"独立入口必须独立块")。
2. DTSTAMP 取 time() → ETag 潮汐(增量同步**永远不收敛**)
App\Ics::stampOf() 原为"没给时间就 time()",而**派生条目**(观影/行程/生日/农历/
节假日/节气)没有 updated_at/created_at ⇒ 每生成一次 ICS 就写一个新"现在" ⇒ 正文变
⇒ ETag 变。线上实测:同一本连续两次 REPORT **每一条 ETag 都不同**,整本 GET 的 ETag
也在变 ⇒ 客户端把"增量同步"退化成"每轮全量重拉",且拉回来的对不上、永远不收敛。
改:缺时间写固定值 19700101T000000Z(确定优先于"诚实的当前时间")。
3. 派生本把"日历视图条目"当"CalDAV 资源"
Calendar::exportRows() 对派生本按**日历视图**取数(重复日程展开 N 次、跨天逐日、
生日/农历逐日派生)。线上 /dav/1.ics/ 实测 **2981 个 response / 只有 196 个不同 href**
(anniv-0_familybook.ics 自己重复 **2399** 次)。CalDAV 的资源是**一个 .ics 文件** ⇒
客户端要么把同一条反复入库,要么判定服务器异常而中止本轮同步。
改:CalDav::resources() 按 **UID 去重**(资源名本就是 UID)。
4. 连带两个真 bug(同一段代码顺出来的)
· exportRows() 派生分支**没按本过滤**:month() 里"任务""纪念日"两段不带 kind 判断
(它们 cal=0)⇒ 取"观影本"会把全家所有生日一起带出来(2981 条里绝大多数是 anniv)。
· CalDav::parsePath() 的 $event=(int)$evSeg:派生资源名(movie-100_familybook)
(int) 后全是 0 ⇒ 单条资源请求被当"整本",GET /dav/4.ics/lunar-...ics 实测回 **400 条**。
5. 顺带补全:系统日历本(农历/节假日/传统节日/节气)原来**导出是空的**
它们只走派生、没有 calendar_events 行,而 exportRows 只认 custom/movie/trip ⇒
手机上订阅这几个本一条都没有。改:按 [今年-1, 今年+5] 派生(**封顶**,农历每天一条,
130 年会有 4.7 万条)。实测耗时 lunarRange 31ms / traditional 51ms / Anniversary 30ms。
改:.htaccess、router.php、dav.php、carddav.php、app/CalDav.php、app/CardDav.php、
app/Calendar.php、app/Ics.php、templates/settings/me.php(补"或只填服务器地址"的提示)。
验证(全部 0 失败):DAV 探针 28/0(**线上修前 15 失败 → 修后 28/0**,同一套探针两处都跑)、
DAV 反向验证 8/0(7 处修复各自改回旧行为,断言全变红)、e2e 1478/0、升级 153/0、
城市 32/0、几何 428/0 + 移动 680/0 + 交互 92/0、b24 41/0、b27 34/0、b28 39/0、b29 38/0、
b30_import 26/0、b30_ui2 16/0、b30_notify 23/0;部署静态断言新增 12 条守卫并逐条证伪通过。
线上 7 个文件 SHA256 与本地一致;/.well-known/caldav→301 到 /dav/ 实测生效。
⚠️ 本轮在部署脚本上真踩到并修掉两个坑(已写进 pitfalls.md):
· router.php **不发到线上**,把它写进部署断言 → grep "No such file" → 部署中止在
"改了一半"的状态(迁到 push.sh 第 1 段)。
· `set -euo pipefail` 下 `X=$(... | grep -c ...)` 在 **0 匹配**时退出码 1 ⇒
**整个脚本静默退出**(不打 FAIL,日志停在上一行)。已给全部 7 处补 `|| true`。
89 lines
5.7 KiB
ApacheConf
89 lines
5.7 KiB
ApacheConf
# 家书 FamilyBook · Apache 重写规则
|
||
#
|
||
# ⚠️ 这个文件**不随包上线**(familybook_push.sh 的打包白名单里没有它)。
|
||
# 原因:线上站点根那份 .htaccess 是**宝塔面板生成的**(PHP 版本段、防盗链、
|
||
# 缓存段、伪静态…),整份覆盖会把面板的东西清空,而且面板下次重写还会改回来,
|
||
# 变成"时好时坏"。
|
||
# /dav/ 与 /carddav/ 的重写规则由 `familybook_deploy.sh` 第 5c 段**追加**到线上那份里
|
||
# (带 `# >>> FamilyBook CalDAV` 标记,幂等)。
|
||
#
|
||
# 那这份文件是干什么用的?
|
||
# · 新建站点(没有宝塔那层)时,可直接把它作为站点根的 .htaccess;
|
||
# · 它是那段规则的**唯一书面来源**,push 脚本会断言部署脚本里的规则串
|
||
# 与它完全一致(防两处漂移)。
|
||
#
|
||
# 只做一件事:把 /dav/ 与 /carddav/ 开头的 URL 分别交给 dav.php / carddav.php。
|
||
# 其余一律不动 —— index.php 那套 ?r=xxx 路由不依赖重写,
|
||
# 少写规则就少一处"上线才发现 404"的地方。
|
||
|
||
<IfModule mod_rewrite.c>
|
||
RewriteEngine On
|
||
|
||
# 🔴 v31:`/.well-known/*` 是 RFC 6764 规定的 DAV 发现入口,客户端**第一句**就问这里。
|
||
# ⚠️ 它必须排在 `^dav` / `^carddav` 之前 —— 这两条是前缀通配(`^dav(/.*)?$`),
|
||
# 而 `.well-known/caldav` 并不以 dav 开头,所以严格说互不干扰;
|
||
# 但放在最前面是"发现链优先于资源访问"的意图表达,后人加规则时不易搞错。
|
||
# 交给 dav.php / carddav.php 回 301(它们在**鉴权之前**处理这条路径:
|
||
# .well-known 是免鉴权入口,在这里 401 会让客户端以为"不支持 DAV")。
|
||
RewriteRule ^\.well-known/caldav$ dav.php [L,QSA]
|
||
RewriteRule ^\.well-known/carddav$ carddav.php [L,QSA]
|
||
|
||
# CalDAV:/dav/、/dav/3.ics/、/dav/3.ics/12.ics 全部交给 dav.php
|
||
RewriteRule ^dav(/.*)?$ dav.php [L,QSA]
|
||
|
||
# CardDAV(v24 第 22 批):/carddav/、/carddav/1.vcf/、/carddav/1.vcf/12.vcf 交给 carddav.php
|
||
# ⚠️ 两条规则**顺序无关但都必须有**:它们的前缀(dav vs carddav)互不包含,
|
||
# 不会互相抢;写成 `^dav` 在前是因为历史顺序,真要调也不能写成 `^dav(/|$)` 之外的
|
||
# 宽松形式 —— 那会把 /dav2/ 之类也吞进来。
|
||
RewriteRule ^carddav(/.*)?$ carddav.php [L,QSA]
|
||
|
||
# 🔴 v31:`/.well-known/caldav` 与 `/.well-known/carddav`(RFC 6764)。
|
||
# 标准客户端(DAVx⁵ / iOS / Evolution)添加账号时**第一句**就试这里 ——
|
||
# 原来这里 404,客户端直接判定"域名下没有 DAV 服务",连口令框都不弹。
|
||
# 症状正是用户报的「同步不出任何数据」(账号根本建不起来)。
|
||
# ⚠️ 这两条必须**排在 `^dav` / `^carddav` 之前**吗?不需要 ——
|
||
# `.well-known/...` 不以 dav/carddav 开头,两条规则互不干扰。
|
||
# 但**必须**在站点根的"真实文件"判断之前:Apache 对 .htaccess 里的
|
||
# RewriteRule 天然就是先于文件查找生效的,所以这里无需额外处理。
|
||
# 交给 dav.php / carddav.php 自己回 301(它们在鉴权**之前**处理,
|
||
# 因为 .well-known 是免鉴权入口 —— 在这里 401 会让客户端以为"不支持")。
|
||
RewriteRule ^\.well-known/caldav$ dav.php [L,QSA]
|
||
RewriteRule ^\.well-known/carddav$ carddav.php [L,QSA]
|
||
|
||
# 导航展示页(v24 第 24 批 用户第 4 条):/nav/?<短语> —— 全站唯一免登录页面。
|
||
# ⚠️ suggest 那条**必须排在前面**:`^nav(/.*)?$` 会把 /nav/suggest.php 也吞进去,
|
||
# 顺序反了就变成"每次联想都去渲染展示页"(返回一页 HTML、前端拿到非 JSON)。
|
||
# 这也是本批新踩的一条:之前只有 dav/carddav 两条互不包含的前缀,
|
||
# 到这里第一次出现"一个前缀包含另一个"的情况,顺序**变成有语义的**了。
|
||
RewriteRule ^nav/suggest\.php$ nav_suggest.php [L,QSA]
|
||
RewriteRule ^nav/?$ nav_show.php [L,QSA]
|
||
</IfModule>
|
||
|
||
# ⚠️ 导航图标与展示页头像**不需要重写规则**(v24 第 25 批):
|
||
# nav_icon.php / nav_avatar.php 是站点根下的**真实文件**,Apache 直接就能执行。
|
||
# 展示页里的地址由 Nav::abs() 拼成 `<base_url>/nav_icon.php?f=…`,落在站点根,
|
||
# 不经过 /nav/ 这个重写前缀 —— 所以上面那两条规则一个字都不用动。
|
||
# (这正是选"站点根下的两个独立入口"而不是"塞进 /nav/ 子路径"的原因:
|
||
# 塞进去就得再加两条重写规则,而重写规则的顺序语义已经踩过一次坑了。)
|
||
|
||
# ⚠️⚠️ DAV 的 Basic 鉴权**必须**靠这一行才能在 Apache 上工作:
|
||
# Apache 默认**不把 `Authorization` 头交给 PHP-FPM** → `$_SERVER['PHP_AUTH_USER']`
|
||
# 和 `HTTP_AUTHORIZATION` 都是 null → /dav/ 与 /carddav/ 无论口令填得多正确都 401。
|
||
# 线上实测(Apache 2.4.53,直接监听 80/443):
|
||
# 没有这行 : {"PHP_AUTH_USER":null,"HTTP_AUTHORIZATION":null}
|
||
# 加上这行 : {"PHP_AUTH_USER":"abc","HTTP_AUTHORIZATION":"(有)"}
|
||
# `CGIPassAuth` 是 Apache **2.4.13+ 的核心指令**,不需要额外模块。
|
||
# (本地 `php -S` 会自己填好 PHP_AUTH_USER,所以 e2e 全绿而线上全 401 ——
|
||
# 典型的"本地好好的、线上不行"。部署脚本也会把这一段追加到线上那份里。)
|
||
<IfModule mod_authz_core.c>
|
||
CGIPassAuth On
|
||
</IfModule>
|
||
|
||
# 备份/临时文件永远不给出去(这条线上很要紧:*.php.bak 曾经可被直接下载)
|
||
<FilesMatch "\.(bak|old|orig|save|swp|swo|tmp)$">
|
||
Require all denied
|
||
</FilesMatch>
|
||
<FilesMatch "^\.|~$">
|
||
Require all denied
|
||
</FilesMatch>
|