Bug 1715756 - Detect read-only status for ICS calendars. r=darktrojan
authorLasana Murray <lasana@thunderbird.net>
Thu, 17 Jun 2021 15:22:26 +0000
changeset 32865 9d4a63fbdd7a40cfb4ebdfb4f1d029208dccdbb0
parent 32864 1681b28aa477b68c48e7c959e2039ab945d73d34
child 32866 7f27f3a95038764e3ee14297c77ac2a4cf89d5d1
push id18890
push usergeoff@darktrojan.net
push dateFri, 18 Jun 2021 02:22:15 +0000
treeherdercomm-central@4b264f974d0c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdarktrojan
bugs1715756
Bug 1715756 - Detect read-only status for ICS calendars. r=darktrojan Differential Revision: https://phabricator.services.mozilla.com/D117688
calendar/providers/ics/CalICSProvider.jsm
--- a/calendar/providers/ics/CalICSProvider.jsm
+++ b/calendar/providers/ics/CalICSProvider.jsm
@@ -249,17 +249,17 @@ class ICSDetector {
       return null;
     }
 
     let resprops = response.firstProps;
     let resourceType = resprops["D:resourcetype"] || new Set();
 
     if (resourceType.has("C:calendar") || resprops["D:getcontenttype"] == "text/calendar") {
       cal.LOG(`[calICSProvider] ${target.spec} is a calendar`);
-      return [this.handleCalendar(target, resprops["D:displayname"], resprops["A:calendar-color"])];
+      return [this.handleCalendar(target, resprops)];
     } else if (resourceType.has("D:collection")) {
       return this.handleDirectory(target);
     }
 
     return null;
   }
 
   /**
@@ -342,16 +342,24 @@ class ICSDetector {
     if (location.schemeIs("file")) {
       let fullPath = location.QueryInterface(Ci.nsIFileURL).file.path;
       let pathToDir = PathUtils.parent(fullPath);
       let dirExists = await IOUtils.exists(pathToDir);
 
       if (dirExists || pathToDir == "") {
         let calendar = this.handleCalendar(location);
         if (calendar) {
+          // Check whether we have write permission on the calendar file.
+          // Calling stat on a non-existent file is an error so we check for
+          // it's existence first.
+          let { permissions } = (await IOUtils.exists(fullPath))
+            ? await IOUtils.stat(fullPath)
+            : await IOUtils.stat(pathToDir);
+
+          calendar.readOnly = (permissions ^ 0o200) == 0;
           return [calendar];
         }
       } else {
         cal.LOG(`[calICSProvider] ${location.spec} includes a directory that does not exist`);
       }
     } else {
       cal.LOG(`[calICSProvider] ${location.spec} is not a "file" URI`);
     }
@@ -361,49 +369,54 @@ class ICSDetector {
   /**
    * Utility function to make a new attempt to detect calendars after the
    * previous PROPFIND results contained "D:resourcetype" with "D:collection".
    *
    * @param {nsIURI} location                   The location to attempt.
    * @return {Promise<calICalendar[] | null>}   An array of calendars or null.
    */
   async handleDirectory(location) {
-    let props = ["D:getcontenttype", "D:displayname", "A:calendar-color"];
+    let props = [
+      "D:getcontenttype",
+      "D:current-user-privilege-set",
+      "D:displayname",
+      "A:calendar-color",
+    ];
     let request = new CalDavPropfindRequest(this.session, null, location, props, 1);
 
     // `request.commit()` can throw; errors should be caught by calling functions.
     let response = await request.commit();
     let target = response.uri;
 
     let calendars = [];
     for (let [href, resprops] of Object.entries(response.data)) {
       if (resprops["D:getcontenttype"] != "text/calendar") {
         continue;
       }
 
       let uri = Services.io.newURI(href, null, target);
-      calendars.push(
-        this.handleCalendar(uri, resprops["D:displayname"], resprops["A:calendar-color"])
-      );
+      calendars.push(this.handleCalendar(uri, resprops));
     }
 
     cal.LOG(`[calICSProvider] ${target.spec} is a directory, found ${calendars.length} calendars`);
 
     return calendars.length ? calendars : null;
   }
 
   /**
    * Set up and return a new ICS calendar object.
    *
    * @param {nsIURI} uri              The location of the calendar.
-   * @param {string} [displayName]    The display name of the calendar.
-   * @param {string} [color]          The color for the calendar.
+   * @param {Set} [props]             For CalDav calendars, these are the props
+   *                                  parsed from the response.
    * @return {calICalendar}           A new calendar.
    */
-  handleCalendar(uri, displayName, color) {
+  handleCalendar(uri, props = new Set()) {
+    let displayName = props["D:displayname"];
+    let color = props["A:calendar-color"];
     if (!displayName) {
       let lastPath =
         uri.filePath
           .split("/")
           .filter(Boolean)
           .pop() || "";
       let fileName = lastPath
         .split(".")
@@ -412,11 +425,23 @@ class ICSDetector {
       displayName = fileName || lastPath || uri.spec;
     }
 
     let calMgr = cal.getCalendarManager();
     let calendar = calMgr.createCalendar("ics", uri);
     calendar.setProperty("color", color || cal.view.hashColor(uri.spec));
     calendar.name = displayName;
     calendar.id = cal.getUUID();
+
+    // Attempt to discover if the user is allowed to write to this calendar.
+    let privs = props["D:current-user-privilege-set"];
+    if (privs && privs instanceof Set) {
+      calendar.readOnly = ![
+        "D:write",
+        "D:write-content",
+        "D:write-properties",
+        "D:all",
+      ].some(priv => privs.has(priv));
+    }
+
     return calendar;
   }
 }