Bug 546932 - Add UI (preffed-off) for creating a CardDAV address book. r=mkmelin

This commit is contained in:
Geoff Lankow 2020-05-31 20:42:14 +03:00
Родитель 0da2fd1890
Коммит 7a76f848a0
12 изменённых файлов: 508 добавлений и 9 удалений

Просмотреть файл

@ -498,6 +498,9 @@ pref("toolbar.customization.usesheet", true);
pref("toolbar.customization.usesheet", false);
#endif
// Whether to enable experimental CardDAV support.
pref("mail.addr_book.carddav.enabled", false);
// Number of recipient rows shown by default
pref("mail.compose.addresswidget.numRowsShownDefault", 3);

Просмотреть файл

@ -244,6 +244,15 @@ function AbNewAddressBook() {
);
}
function AbNewCardDAVBook() {
window.openDialog(
"chrome://messenger/content/addressbook/abCardDAVDialog.xhtml",
"",
"chrome,modal,resizable=no,centerscreen",
null
);
}
function AbEditSelectedDirectory() {
let selectedDir = getSelectedDirectory();
if (!selectedDir) {

Просмотреть файл

@ -303,10 +303,17 @@ function SetNameColumn(cmd) {
Services.prefs.setIntPref(kPrefMailAddrBookLastNameFirst, prefValue);
}
function onOSXFileMenuInit() {
document
.getElementById("menu_osxAddressBook")
.setAttribute("checked", AbOSXAddressBookExists());
function onFileMenuInit() {
let osxMenuItem = document.getElementById("menu_osxAddressBook");
if (osxMenuItem) {
osxMenuItem.setAttribute("checked", AbOSXAddressBookExists());
}
document.getElementById(
"menu_newCardDAVBook"
).hidden = !Services.prefs.getBoolPref(
"mail.addr_book.carddav.enabled",
false
);
}
function CommandUpdate_AddressBook() {

Просмотреть файл

@ -302,11 +302,7 @@
<toolbaritem id="menubar-items" align="center">
<menubar id="mail-menubar">
<menu id="menu_File" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
#ifdef XP_MACOSX
<menupopup id="menu_FilePopup" onpopupshowing="onOSXFileMenuInit();">
#else
<menupopup id="menu_FilePopup">
#endif
<menupopup id="menu_FilePopup" onpopupshowing="onFileMenuInit();">
<menu id="menu_New" class="menu-iconic"
label="&newMenu.label;" accesskey="&newMenu.accesskey;">
<menupopup id="menu_NewPopup">
@ -325,6 +321,11 @@
accesskey="&newAddressBookCmd.accesskey;"
oncommand="AbNewAddressBook()"
class="menuitem-iconic"/>
<menuitem id="menu_newCardDAVBook"
label="&newCardDAVBookCmd.label;"
accesskey="&newCardDAVBookCmd.accesskey;"
oncommand="AbNewCardDAVBook()"
class="menuitem-iconic"/>
<menuitem id="addLDAP" label="&newLDAPDirectoryCmd.label;"
accesskey="&newLDAPDirectoryCmd.accesskey;"
oncommand="AbNewLDAPDirectory()"

Просмотреть файл

@ -18,6 +18,8 @@
<!ENTITY newListCmd.accesskey "L">
<!ENTITY newAddressBookCmd.label "Address Book…">
<!ENTITY newAddressBookCmd.accesskey "B">
<!ENTITY newCardDAVBookCmd.label "CardDAV Address Book…">
<!ENTITY newCardDAVBookCmd.accesskey "V">
<!ENTITY newLDAPDirectoryCmd.label "LDAP Directory…">
<!ENTITY newLDAPDirectoryCmd.accesskey "D">
<!ENTITY newMessageCmd.label "Message">

Просмотреть файл

@ -0,0 +1,45 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
carddav-window =
.title = New CardDAV Address Book
carddav-dialog =
.buttonlabelaccept = Continue
.buttonaccesskeyaccept = C
carddav-experimental-warning = CardDAV address book support is experimental and could permanently damage your data. Use at your own risk.
carddav-provider-label =
.value = CardDAV Provider:
.accesskey = P
carddav-provider-option-other = Other provider…
carddav-url-label =
.value = CardDAV URL:
.accesskey = V
carddav-username-label =
.value = Username:
.accesskey = U
carddav-username-input =
.placeholder = you@example.com
carddav-password-label =
.value = Password:
.accesskey = w
carddav-password-input =
.placeholder = Password
carddav-remember-password =
.label = Remember password
.accesskey = m
carddav-loading = Looking up configuration…
carddav-connection-error = Failed to connect.
carddav-none-found = Found no address books to add for the specified account.
carddav-already-added = All address books for the specified account have already been added.
carddav-available-books = Available address books:

Просмотреть файл

@ -0,0 +1,68 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
#bigScaryWarning {
background-color: #FFE900;
border-color: #F2D00F;
color: -moz-DialogText;
padding: 4px;
white-space: normal;
}
#bigScaryWarning image {
-moz-context-properties: fill;
fill: currentColor;
margin: 4px 8px;
}
#bigScaryWarning description {
width: 30em;
}
th {
font-weight: normal;
text-align: start;
}
#statusArea {
background: transparent;
border: 1px solid transparent;
border-radius: 2px;
padding: 3px 4px;
color: var(--text-color);
}
#statusArea #statusImageBefore {
-moz-context-properties: fill;
fill: currentColor;
width: 16px;
height: 16px;
background-size: 16px 16px;
}
#statusArea[status=error] {
background-color: #FFE900;
border-color: #F2D00F;
color: -moz-DialogText;
}
#statusArea[status=error] #statusImageBefore {
list-style-image: url("chrome://global/skin/icons/warning.svg");
}
#statusArea[status=loading] {
background-color: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.1);
}
#statusArea[status=loading] #statusImageBefore {
background: url("chrome://global/skin/icons/loading.png") no-repeat;
}
@media (min-resolution: 1.1dppx) {
#statusArea[status=loading] #statusImageBefore {
background-image: url("chrome://global/skin/icons/loading@2x.png");
background-size: 16px 16px;
}
}

Просмотреть файл

@ -0,0 +1,231 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { MailServices } = ChromeUtils.import(
"resource:///modules/MailServices.jsm"
);
var { CardDAVDirectory } = ChromeUtils.import(
"resource:///modules/CardDAVDirectory.jsm"
);
var gUrl;
var gUsername;
var gPassword;
var uiElements = {};
window.addEventListener("DOMContentLoaded", async () => {
for (let id of [
"dialog",
"url",
"username",
"password",
"rememberPassword",
"statusArea",
"statusMessage",
"resultsArea",
"availableBooks",
]) {
uiElements[id] = document.getElementById("carddav-" + id);
}
await document.l10n.ready;
let presets = {
Fastmail: "https://carddav.fastmail.com",
Google: "https://www.googleapis.com",
};
// TODO: Only load presets of accounts we have.
let provider = document.getElementById("carddav-provider");
for (let [label, hostname] of Object.entries(presets)) {
let option = document.createElementNS(
"http://www.w3.org/1999/xhtml",
"option"
);
option.label = label;
option.value = hostname;
provider.appendChild(option);
}
let other = document.createElementNS(
"http://www.w3.org/1999/xhtml",
"option"
);
other.value = "";
other.setAttribute("data-l10n-id", "carddav-provider-option-other");
provider.appendChild(other);
uiElements.url.value = provider.value;
});
function handleChangeProvider(event) {
uiElements.url.value = event.target.value;
changeCardDAVURL(uiElements.url.value);
}
function handleCardDAVURLInput(event) {
changeCardDAVURL(event.target.value);
}
function changeCardDAVURL(value) {
setStatus();
uiElements.resultsArea.hidden = true;
gUrl = uiElements.url.value.trim();
if (gUrl && !gUrl.match(/^https?:\/\//)) {
gUrl = "https://" + gUrl;
}
gUrl = gUrl ? new URL(gUrl) : "";
}
async function check() {
gUrl = uiElements.url.value;
gUsername = uiElements.username.value;
gPassword = uiElements.password.value;
setStatus("loading", "carddav-loading");
try {
if (!gUrl.match(/^https?:\/\//)) {
gUrl = "https://" + gUrl;
}
gUrl = new URL(gUrl);
let response = await CardDAVDirectory.makeRequest(
`${gUrl.origin}/.well-known/carddav`,
{
method: "PROPFIND",
headers: {
"Content-Type": "text/xml",
Depth: 0,
},
body: `<propfind xmlns="DAV:">
<prop>
<current-user-principal/>
</prop>
</propfind>`,
}
);
let href =
gUrl.origin +
response.dom.querySelector("current-user-principal href").textContent;
response = await CardDAVDirectory.makeRequest(href, {
method: "PROPFIND",
headers: {
"Content-Type": "text/xml",
Depth: 0,
},
body: `<propfind xmlns="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
<prop>
<card:addressbook-home-set/>
</prop>
</propfind>`,
});
href =
gUrl.origin +
response.dom.querySelector("addressbook-home-set href").textContent;
response = await CardDAVDirectory.makeRequest(href, {
method: "PROPFIND",
headers: {
"Content-Type": "text/xml",
Depth: 1,
},
body: `<propfind xmlns="DAV:" xmlns:cs="http://calendarserver.org/ns/">
<prop>
<resourcetype/>
<displayname/>
<cs:getctag/>
</prop>
</propfind>`,
});
while (uiElements.availableBooks.lastChild) {
uiElements.availableBooks.lastChild.remove();
}
let existing = [...MailServices.ab.directories].map(d =>
d.getStringValue("carddav.url", "")
);
let alreadyAdded = 0;
for (let r of response.dom.querySelectorAll("response")) {
if (r.querySelector("resourcetype addressbook")) {
let bookURL = new URL(r.querySelector("href").textContent, gUrl).href;
if (existing.includes(bookURL)) {
alreadyAdded++;
continue;
}
let checkbox = uiElements.availableBooks.appendChild(
document.createXULElement("checkbox")
);
checkbox.setAttribute(
"label",
r.querySelector("displayname").textContent
);
checkbox.checked = true;
checkbox.value = bookURL;
}
}
if (uiElements.availableBooks.childElementCount == 0) {
if (alreadyAdded > 0) {
setStatus("error", "carddav-already-added");
} else {
setStatus("error", "carddav-none-found");
}
} else {
uiElements.resultsArea.hidden = false;
setStatus();
}
} catch (ex) {
setStatus("error", "carddav-connection-error");
}
}
function setStatus(status, message) {
uiElements.dialog.getButton("accept").disabled = status == "error";
if (status) {
uiElements.statusArea.setAttribute("status", status);
document.l10n.setAttributes(uiElements.statusMessage, message);
window.sizeToContent();
} else {
uiElements.statusArea.removeAttribute("status");
document.l10n.setAttributes(uiElements.statusMessage, null);
}
}
window.addEventListener("dialogaccept", event => {
if (uiElements.resultsArea.hidden) {
event.preventDefault();
check();
return;
}
if (uiElements.availableBooks.childElementCount == 0) {
return;
}
let newLoginInfo = Cc[
"@mozilla.org/login-manager/loginInfo;1"
].createInstance(Ci.nsILoginInfo);
newLoginInfo.init(gUrl.origin, null, "CardDAV", gUsername, gPassword, "", "");
// TODO: Login might exist.
Services.logins.addLogin(newLoginInfo);
let book;
for (let checkbox of uiElements.availableBooks.children) {
if (checkbox.checked) {
let dirPrefId = MailServices.ab.newAddressBook(
checkbox.getAttribute("label"),
null,
102,
null
);
book = MailServices.ab.getDirectoryFromId(dirPrefId);
book.setStringValue("carddav.url", checkbox.value);
let dir = CardDAVDirectory.forFile(book.fileName);
dir.fetchAllFromServer();
}
}
});

Просмотреть файл

@ -0,0 +1,112 @@
<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, you can obtain one at http://mozilla.org/MPL/2.0/. -->
<?xml-stylesheet href="chrome://messenger/skin/dialogs.css" type="text/css"?>
<?xml-stylesheet href="chrome://messenger/skin/menulist.css" type="text/css"?>
<?xml-stylesheet href="chrome://messenger/content/addressbook/abCardDAVDialog.css" type="text/css"?>
<window data-l10n-id="carddav-window"
data-l10n-attrs="title"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml">
<dialog id="carddav-dialog"
data-l10n-id="carddav-dialog"
data-l10n-attrs="buttonlabelaccept,buttonaccesskeyaccept">
<linkset>
<html:link rel="localization" href="messenger/addressbook/abCardDAVDialog.ftl"/>
</linkset>
<script src="chrome://global/content/globalOverlay.js"/>
<script src="chrome://global/content/editMenuOverlay.js"/>
<script src="chrome://messenger/content/addressbook/abCardDAVDialog.js"/>
<hbox id="bigScaryWarning"
align="center">
<image src="chrome://global/skin/icons/warning.svg"
width="32"
height="32"/>
<description data-l10n-id="carddav-experimental-warning"/>
</hbox>
<html:table>
<html:tr>
<html:th>
<label control="carddav-provider"
data-l10n-id="carddav-provider-label"
data-l10n-attrs="value,accesskey"/>
</html:th>
<html:td>
<html:select id="carddav-provider" onchange="handleChangeProvider(event);">
</html:select>
</html:td>
</html:tr>
<html:tr>
<html:th>
<label control="carddav-url"
data-l10n-id="carddav-url-label"
data-l10n-attrs="value,accesskey"/>
</html:th>
<html:td>
<html:input id="carddav-url" type="url"
placeholder="https://carddav.example.com/dav/addressbooks/user/"
oninput="handleCardDAVURLInput(event);"/>
</html:td>
</html:tr>
<html:tr>
<html:th>
<label control="carddav-username"
data-l10n-id="carddav-username-label"
data-l10n-attrs="value,accesskey"
flex="1"/>
</html:th>
<html:td>
<html:input id="carddav-username"
type="text"
data-l10n-id="carddav-username-input"/>
</html:td>
</html:tr>
<html:tr>
<html:th>
<label control="carddav-password"
data-l10n-id="carddav-password-label"
flex="1"/>
</html:th>
<html:td>
<html:input id="carddav-password" type="password"/>
</html:td>
</html:tr>
<html:tr>
<html:th></html:th>
<html:td>
<checkbox id="carddav-rememberPassword"
data-l10n-id="carddav-remember-password"
data-l10n-attrs="label,accesskey"
checked="true"/>
</html:td>
</html:tr>
</html:table>
<vbox id="carddav-statusArea"
align="center"
pack="center">
<hbox align="center"
pack="center">
<image id="statusImageBefore"/>
<description id="carddav-statusMessage"
flex="1">&#160;</description>
<!-- Include 160 = nbsp, to make the element occupy the
full height, for at least one line. With a normal space,
it does not have sufficient height. -->
</hbox>
</vbox>
<vbox id="carddav-resultsArea"
hidden="true">
<label data-l10n-id="carddav-available-books"/>
<vbox id="carddav-availableBooks"></vbox>
</vbox>
</dialog>
</window>

Просмотреть файл

@ -48,12 +48,16 @@ ChromeUtils.defineModuleGetter(
// Keep track of all database connections, and close them at shutdown, since
// nothing else ever tells us to close them.
// Also track all directories by filename, for AddrBookDirectory.forFile.
var connections = new Map();
var directories = new Map();
Services.obs.addObserver(() => {
for (let connection of connections.values()) {
connection.asyncClose();
}
connections.clear();
directories.clear();
}, "quit-application");
// Close a connection on demand. This serves as an escape hatch from C++ code.
@ -107,6 +111,7 @@ function openConnectionTo(file) {
* Closes the SQLite connection to `file` and removes it from the cache.
*/
function closeConnectionTo(file) {
directories.delete(file.leafName);
let connection = connections.get(file.path);
if (connection) {
return new Promise(resolve => {
@ -161,6 +166,7 @@ class AddrBookDirectory {
}
this._fileName = fileName;
directories.set(fileName, this);
}
get _prefBranch() {
@ -1079,6 +1085,10 @@ class AddrBookDirectory {
valueLocal
);
}
static forFile(fileName) {
return directories.get(fileName);
}
}
AddrBookDirectory.prototype.QueryInterface = ChromeUtils.generateQI([
Ci.nsIAbDirectory,

Просмотреть файл

@ -406,6 +406,14 @@ class CardDAVDirectory extends AddrBookDirectory {
}
}
static forFile(fileName) {
let directory = super.forFile(fileName);
if (directory instanceof CardDAVDirectory) {
return directory;
}
return undefined;
}
/**
* Make an HTTP request. If the request needs a username and password, the
* given authPrompt is called.

Просмотреть файл

@ -9,6 +9,9 @@ messenger.jar:
content/messenger/addressbook/pref-editdirectories.xhtml (addrbook/prefs/content/pref-editdirectories.xhtml)
content/messenger/addressbook/abAddressBookNameDialog.js (addrbook/content/abAddressBookNameDialog.js)
content/messenger/addressbook/abAddressBookNameDialog.xhtml (addrbook/content/abAddressBookNameDialog.xhtml)
content/messenger/addressbook/abCardDAVDialog.css (addrbook/content/abCardDAVDialog.css)
content/messenger/addressbook/abCardDAVDialog.js (addrbook/content/abCardDAVDialog.js)
content/messenger/addressbook/abCardDAVDialog.xhtml (addrbook/content/abCardDAVDialog.xhtml)
content/messenger/addressbook/abResultsPane.js (addrbook/content/abResultsPane.js)
content/messenger/addressbook/abDragDrop.js (addrbook/content/abDragDrop.js)
content/messenger/addressbook/abMailListDialog.js (addrbook/content/abMailListDialog.js)