v42 第 42 批:十一条反馈(界面 9 条 + CalDAV 两处新根因)

CalDAV(用户「还是不行,thunderbird 提示找不到日历」):
- 🔴 `.well-known/caldav` 原来**只回 301**。Thunderbird 建账号时常是
  **带鉴权直接 PROPFIND** 这个地址(把它当 DAV 服务入口探),而它对这一跳的
  301 **不跟随** ⇒ 判定"这里没有 CalDAV" ⇒ 找不到日历。
  ⇒ 按方法分流:带鉴权 PROPFIND → 直接 207 给发现内容(principal / home-set /
    日历本清单,一步到位);其余仍是 301。carddav 同修。
- 🔴 `Location` 由**相对** `/dav/` 改成**绝对 URL**(按真实 HTTP_HOST 拼,
  协议从 HTTPS / X-Forwarded-Proto 取)。TB 在服务发现这一跳对相对跳转不可靠。
- 线上终验:匿名 GET 301+绝对URL;带鉴权 PROPFIND 直接 207(含 principal/home-set);
  Depth:0 只回自身;Depth:1 列出 12 本日历。

界面九条:
- ① 新建旅程不再带入旧数据(真根因:`_list_body.php` 的 `foreach ($rows as $t)`
  循环后 `$t` 残留,而 `form.php` 读的正是 `$t` ⇒ 弹框被填上列表最后一趟的数据)。
  修:循环变量改 `$row` + `form.php` 契约兼容(`$tIn` 优先)。
- ②③④ 行程弹框里的 AntD 时间控件 / 出行方式联动车牌与车次 / 高德地址联想全部失效
  (真根因:AJAX 片段里的内联 `<script>` **既不执行也不保留**)。
  ⇒ 加 `runInlineScripts()` 把片段内联脚本抠出来执行;
  另修 `app.js`:`if (!hasMonth) return` 的短路让 `window.fbBootAntd` 从未被定义,
  把它提到所有 return 之前。
- ④b⑤ 标签改「照片」「时间范围」。
- ⑥ 简介补"还没有简介,点这里写一句"的占位(简介本身一直在,是那条 summary 为空)。
- ⑦ 资源编辑复用新建弹框(`#resAddCard`),并在其底栏补「共享」「附件」入口
  (第 7 条改动后这两个能力一度无入口)。
- ⑧ 纪念日「月/日」→「日期」+ AntD 纯日期控件(新增 mountAntdDate / [data-antd-date]),
  存储契约仍是 month + day。
- ⑨ 编辑纪念日弹框关不掉(关闭逻辑原来只在 days/index.php,而 days/edit 是整页)
  ⇒ 搬进 days/form.php 自包含。

验证:e2e 1510/0;v42 真机 33/0;DAV·TB 35/0;DAV 反向 8/0;
b23 55/0;b28 39/0;b32 26/0;b32 反向 5/0;v31 DAV 28/0;b31 反向 8/0;
部署断言空跑 337/0;完整流水线 PUSH_RC=0。
This commit is contained in:
2026-10-11 18:35:03 +08:00
parent 7544fb9f6a
commit 81dfacaf1b
10 changed files with 612 additions and 44 deletions
+72 -1
View File
@@ -90,8 +90,79 @@ $path = (string)parse_url((string)($_SERVER['REQUEST_URI'] ?? '/dav/'), PHP_UR
* (本批实测踩过:跳到 principal 只对"严格按 RFC 5397 走的"客户端够用。)
*/
if ($path === '/.well-known/caldav') {
/* 🔴🔴 v42(用户第 10 条)核心修复之一:
这条路径原来**只回 301**(跳 `/dav/`)。匿名 GET 时这是对的;
但 Thunderbird 建账号时**常常是带鉴权直接 PROPFIND 这个地址**
(它把 `.well-known/caldav` 当成"DAV 服务的入口"来探,而不是只当跳转)。
对带鉴权的 PROPFIND 回 301 的后果:**TB 不跟这个跳转**
(它对"服务发现跳转"的跟随策略与浏览器不同),于是判定
"这里没有 CalDAV 服务" ⇒ 报「找不到日历」。
⇒ 修法:**按方法分流**
· 有鉴权凭据 + PROPFIND → 直接给出与 `/dav/` 一致的发现响应(207,
含 current-user-principal / calendar-home-set / 日历本清单),
客户端**一步到位**,不依赖它对 301 的跟随;
· 其余(匿名、GET、OPTIONS…)→ 仍是 301 到绝对 URL `/dav/`(RFC 6764 语义)。
⚠️ 免鉴权这条必须保住:匿名 OPTIONS/GET 不能 401(否则客户端以为不支持 DAV)。 */
$wkWantPropfind = (strtoupper($method) === 'PROPFIND');
$wkHasAuth = !empty($user) && $pass !== null && $pass !== '';
if ($wkWantPropfind && $wkHasAuth) {
$wkWho = Auth::davAuth($user, $pass);
if (!$wkWho) {
header('WWW-Authenticate: Basic realm="FamilyBook DAV"');
dav_fail(401, '需要 DAV 口令(在「个人设置 → DAV 同步」页生成,不是登录密码)。');
}
$wkFamily = (int)$wkWho['family_id'];
$wkMe = (int)$wkWho['person_id'];
$wkParts = [
'<d:response><d:href>/dav/</d:href><d:propstat><d:prop>'
. '<d:resourcetype><d:collection/></d:resourcetype>'
. '<d:displayname>' . CalDav::x('家书日历') . '</d:displayname>'
. '<d:current-user-principal><d:href>' . CalDav::x(CalDav::PRINCIPAL_PATH)
. '</d:href></d:current-user-principal>'
. '<d:principal-URL><d:href>' . CalDav::x(CalDav::PRINCIPAL_PATH)
. '</d:href></d:principal-URL>'
. '<c:calendar-home-set><d:href>' . CalDav::x(CalDav::HOME_PATH)
. '</d:href></c:calendar-home-set>'
. '<card:addressbook-home-set><d:href>' . CalDav::x(CalDav::CARD_HOME_PATH)
. '</d:href></card:addressbook-home-set>'
. '</d:prop></d:propstat></d:response>',
];
foreach (CalDav::books($wkFamily, $wkMe) as $wkC) {
$wkParts[] = '<d:response><d:href>' . CalDav::x(CalDav::hrefOf($wkC)) . '</d:href>'
. '<d:propstat><d:prop>'
. '<d:resourcetype><d:collection/><c:calendar/></d:resourcetype>'
. '<d:displayname>' . CalDav::x('家书 · ' . (string)$wkC['name']) . '</d:displayname>'
. '<c:supported-calendar-component-set><c:comp name="VEVENT"/></c:supported-calendar-component-set>'
. '</d:prop></d:propstat></d:response>';
}
CalDav::multis();
echo '<?xml version="1.0" encoding="utf-8"?>' . "\n"
. '<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" '
. 'xmlns:card="urn:ietf:params:xml:ns:carddav">'
. implode('', $wkParts) . '</d:multistatus>';
exit;
}
http_response_code(301);
header('Location: /dav/');
/* 🔴🔴 v42(用户 2026-10-11 第 10 条:「caldav 还是不行,thunderbird 提示找不到日历」)
这里原来是**相对路径** `Location: /dav/`。
⚠️ RFC 7231 允许相对 Location(客户端按 RFC 3986 解析),
但 Thunderbird 的 CalDAV 服务发现在这一跳上**对相对 Location 不可靠** ——
`curl -L` 会跟、浏览器会跟,但 TB 自己不跟,
它把这个 301 当成"这里没有 CalDAV 服务" ⇒ 报「找不到日历」。
而我们的探针用 curl/urllib(都会自动跟)⇒ 全绿,
于是"探针全过、真客户端不行"这个组合一直复现不出来。
⇒ 改成**绝对 URL**(RFC 6764 §6.1 的示例也是绝对 URL)。
⚠️ 按**真实请求的 host**拼(不能写死域名):
本站既可能被 family.example.com 访问,也可能被 IP / 其它别名访问,
写死会把客户端带到别的域(跨域后鉴权头会丢)。 */
$wkHost = (string)($_SERVER['HTTP_HOST'] ?? '');
$wkProto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
// 走代理(宝塔 nginx→apache)时协议可能丢,再从 X-Forwarded-Proto 补一次
if ($wkProto === 'http' && !empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
$wkProto = (string)$_SERVER['HTTP_X_FORWARDED_PROTO'];
}
$wkTarget = ($wkHost !== '') ? ($wkProto . '://' . $wkHost . '/dav/') : '/dav/';
header('Location: ' . $wkTarget);
exit;
}