Use PROPFIND instead of OPTIONS to cache WebDAV credentials

Some WebDAV servers allow unauthenticated OPTIONS requests, so the
Authorization header capturing added in 089701eca8 wouldn't work.

PROPFIND with Depth: 0 reliably requires authentication while only
returning properties of the directory itself.

https://forums.zotero.org/discussion/comment/506993/#Comment_506993
This commit is contained in:
Dan Stillman 2026-02-03 23:38:57 -05:00
parent 5e82c888b6
commit eecdd66098
2 changed files with 51 additions and 12 deletions

View file

@ -253,12 +253,20 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
Zotero.debug("Caching WebDAV credentials");
let xmlstr = "<propfind xmlns='DAV:'><prop>"
+ "<getcontentlength/>"
+ "</prop></propfind>";
try {
var req = await Zotero.HTTP.request(
"OPTIONS",
await Zotero.HTTP.request(
"PROPFIND",
this.rootURI,
{
successCodes: [200, 204, 404],
body: xmlstr,
headers: {
Depth: 0,
"Content-Type": "text/xml; charset=utf-8"
},
successCodes: [207, 404],
errorDelayIntervals: this.ERROR_DELAY_INTERVALS,
errorDelayMax: this.ERROR_DELAY_MAX,
onAuthorizationHeader: (authorization) => {
@ -267,7 +275,6 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
},
}
);
if (this._channelAuthorization) {
Zotero.debug("Authorization header cached");
}
@ -278,7 +285,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
catch (e) {
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
let msg = "HTTP " + e.status + " error from WebDAV server "
+ "for OPTIONS request";
+ "for PROPFIND request";
Zotero.logError(msg);
throw new Error(this.defaultErrorRestart);
}

View file

@ -60,7 +60,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
httpd.registerPathHandler(path, {
handle: function (request, response) {
// Always handle OPTIONS with auth (for cacheCredentials calls)
// Always handle OPTIONS with auth (for checkServer calls)
if (request.method == 'OPTIONS') {
if (!checkAuth(request)) {
send401(response);
@ -70,7 +70,29 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
response.setStatusLine(null, 200, "OK");
return;
}
// Handle PROPFIND with auth (for cacheCredentials() calls) unless a
// custom handler is registered
if (request.method == 'PROPFIND' && !pathHandlers[path]?.PROPFIND) {
if (!checkAuth(request)) {
send401(response);
return;
}
response.setHeader('Content-Type', 'text/xml; charset="utf-8"', false);
response.setStatusLine(null, 207, "Multi-Status");
response.write('<?xml version="1.0" encoding="utf-8"?>'
+ '<D:multistatus xmlns:D="DAV:">'
+ '<D:response>'
+ `<D:href>${path}</D:href>`
+ '<D:propstat>'
+ '<D:prop><D:getcontentlength/></D:prop>'
+ '<D:status>HTTP/1.1 200 OK</D:status>'
+ '</D:propstat>'
+ '</D:response>'
+ '</D:multistatus>');
return;
}
let methodHandlers = pathHandlers[path];
let methodHandler = methodHandlers && methodHandlers[request.method];
@ -728,12 +750,12 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
await OS.File.remove(zipPath);
// OPTIONS request to cache credentials
// PROPFIND request to cache credentials
httpd.registerPathHandler(
`${davBasePath}zotero/`,
{
handle: function (request, response) {
if (request.method == 'OPTIONS') {
if (request.method == 'PROPFIND') {
// Force Basic Auth
if (!request.hasHeader('Authorization')) {
response.setStatusLine(null, 401, null);
@ -746,8 +768,18 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
return;
}
response.setHeader('Set-Cookie', 'foo=bar', false);
response.setHeader('DAV', '1', false);
response.setStatusLine(null, 200, "OK");
response.setHeader('Content-Type', 'text/xml; charset="utf-8"', false);
response.setStatusLine(null, 207, "Multi-Status");
response.write('<?xml version="1.0" encoding="utf-8"?>'
+ '<D:multistatus xmlns:D="DAV:">'
+ '<D:response>'
+ `<D:href>${davBasePath}zotero/</D:href>`
+ '<D:propstat>'
+ '<D:prop><D:getcontentlength/></D:prop>'
+ '<D:status>HTTP/1.1 200 OK</D:status>'
+ '</D:propstat>'
+ '</D:response>'
+ '</D:multistatus>');
}
}
}
@ -1300,7 +1332,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
});
var results = await controller.purgeOrphanedStorageFiles();
assertRequestCount(7);
assertRequestCount(8);
assert.sameMembers(
results.deleted,