/* 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/. */ /* exported gInTab, gMainWindow, gTabmail, intializeTabOrWindowVariables, * dispose, setDialogId, loadReminders, saveReminder, * commonUpdateReminder, updateLink, * adaptScheduleAgent, sendMailToOrganizer, * openAttachmentFromItemSummary, */ /* import-globals-from ../item-editing/calendar-item-iframe.js */ /* import-globals-from ../calendar-ui-utils.js */ var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs"); XPCOMUtils.defineLazyModuleGetters(this, { CalAlarm: "resource:///modules/CalAlarm.jsm", }); // Variables related to whether we are in a tab or a window dialog. var gInTab = false; var gMainWindow = null; var gTabmail = null; /** * Initialize variables for tab vs window. */ function intializeTabOrWindowVariables() { let args = window.arguments[0]; gInTab = args.inTab; if (gInTab) { gTabmail = parent.document.getElementById("tabmail"); gMainWindow = parent; } else { gMainWindow = parent.opener; } } /** * Dispose of controlling operations of this event dialog. Uses * window.arguments[0].job.dispose() */ function dispose() { let args = window.arguments[0]; if (args.job && args.job.dispose) { args.job.dispose(); } } /** * Sets the id of a Dialog to another value to allow different CSS styles * to be used. * * @param aDialog The Dialog to be changed. * @param aNewId The new ID as String. */ function setDialogId(aDialog, aNewId) { aDialog.setAttribute("id", aNewId); applyPersistedProperties(aDialog); } /** * Apply the persisted properties from xulstore.json on a dialog based on the current dialog id. * This needs to be invoked after changing a dialog id while loading to apply the values for the * new dialog id. * * @param aDialog The Dialog to apply the property values for */ function applyPersistedProperties(aDialog) { let xulStore = Services.xulStore; // first we need to detect which properties are persisted let persistedProps = aDialog.getAttribute("persist") || ""; if (persistedProps == "") { return; } let propNames = persistedProps.split(" "); let { outerWidth: width, outerHeight: height } = aDialog; let doResize = false; // now let's apply persisted values if applicable for (let propName of propNames) { if (xulStore.hasValue(aDialog.baseURI, aDialog.id, propName)) { let propValue = xulStore.getValue(aDialog.baseURI, aDialog.id, propName); if (propName == "width") { width = propValue; doResize = true; } else if (propName == "height") { height = propValue; doResize = true; } else { aDialog.setAttribute(propName, propValue); } } } if (doResize) { aDialog.ownerGlobal.resizeTo(width, height); } } /** * Create a calIAlarm from the given menuitem. The menuitem must have the * following attributes: unit, length, origin, relation. * * @param {Element} aMenuitem - The menuitem to create the alarm from. * @param {calICalendar} aCalendar - The calendar for getting the default alarm type. * @returns The calIAlarm with information from the menuitem. */ function createReminderFromMenuitem(aMenuitem, aCalendar) { let reminder = aMenuitem.reminder || new CalAlarm(); // clone immutable reminders if necessary to set default values let isImmutable = !reminder.isMutable; if (isImmutable) { reminder = reminder.clone(); } let offset = cal.createDuration(); offset[aMenuitem.getAttribute("unit")] = aMenuitem.getAttribute("length"); offset.normalize(); offset.isNegative = aMenuitem.getAttribute("origin") == "before"; reminder.related = aMenuitem.getAttribute("relation") == "START" ? Ci.calIAlarm.ALARM_RELATED_START : Ci.calIAlarm.ALARM_RELATED_END; reminder.offset = offset; reminder.action = getDefaultAlarmType(aCalendar); // make reminder immutable in case it was before if (isImmutable) { reminder.makeImmutable(); } return reminder; } /** * This function opens the needed dialogs to edit the reminder. Note however * that calling this function from an extension is not recommended. To allow an * extension to open the reminder dialog, set the menulist "item-alarm" to the * custom menuitem and call updateReminder(). * * @param {Element} reminderList - The reminder menu element. * @param {calIEvent | calIToDo} calendarItem - The calendar item. * @param {number} lastAlarmSelection - Index of previously selected item in the menu. * @param {calICalendar} calendar - The calendar to use. * @param {calITimezone} [timezone] - Timezone to use. */ function editReminder( reminderList, calendarItem, lastAlarmSelection, calendar, timezone = cal.dtz.defaultTimezone ) { let customItem = reminderList.querySelector(".reminder-custom-menuitem"); let args = { reminders: customItem.reminders, item: calendarItem, timezone, calendar, // While these are "just" callbacks, the dialog is opened modally, so aside // from what's needed to set up the reminders, nothing else needs to be done. onOk(reminders) { customItem.reminders = reminders; }, onCancel() { reminderList.selectedIndex = lastAlarmSelection; }, }; window.setCursor("wait"); // open the dialog modally openDialog( "chrome://calendar/content/calendar-event-dialog-reminder.xhtml", "_blank", "chrome,titlebar,modal,resizable,centerscreen", args ); } /** * Update the reminder details from the selected alarm. This shows a string * describing the reminder set, or nothing in case a preselected reminder was * chosen. * * @param {Element} reminderDetails - The reminder details element. * @param {Element} reminderList - The reminder menu element. * @param {calICalendar} calendar - The calendar. */ function updateReminderDetails(reminderDetails, reminderList, calendar) { // find relevant elements in the document let reminderMultipleLabel = reminderDetails.querySelector(".reminder-multiple-alarms-label"); let iconBox = reminderDetails.querySelector(".alarm-icons-box"); let reminderSingleLabel = reminderDetails.querySelector(".reminder-single-alarms-label"); let reminders = reminderList.querySelector(".reminder-custom-menuitem").reminders || []; let actionValues = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"]; let actionMap = {}; for (let action of actionValues) { actionMap[action] = true; } // Filter out any unsupported action types. reminders = reminders.filter(x => x.action in actionMap); if (reminderList.value == "custom") { // Depending on how many alarms we have, show either the "Multiple Alarms" // label or the single reminder label. reminderMultipleLabel.hidden = reminders.length < 2; reminderSingleLabel.hidden = reminders.length > 1; cal.alarms.addReminderImages(iconBox, reminders); // If there is only one reminder, display the reminder string if (reminders.length == 1) { reminderSingleLabel.value = reminders[0].toString(window.calendarItem); } } else { reminderMultipleLabel.setAttribute("hidden", "true"); reminderSingleLabel.setAttribute("hidden", "true"); if (reminderList.value == "none") { // No reminder selected means show no icons. while (iconBox.lastChild) { iconBox.lastChild.remove(); } } else { // This is one of the predefined dropdown items. We should show a // single icon in the icons box to tell the user what kind of alarm // this will be. let mockAlarm = new CalAlarm(); mockAlarm.action = getDefaultAlarmType(calendar); cal.alarms.addReminderImages(iconBox, [mockAlarm]); } } } /** * Check whether a reminder matches one of the default menu items or not. * * @param {calIAlarm} reminder - The reminder to match to a menu item. * @param {Element} reminderList - The reminder menu element. * @param {calICalendar} calendar - The current calendar, to get the default alarm type. * @returns {boolean} True if the reminder matches a menu item, false if not. */ function matchCustomReminderToMenuitem(reminder, reminderList, calendar) { let defaultAlarmType = getDefaultAlarmType(calendar); let reminderPopup = reminderList.menupopup; if ( reminder.related != Ci.calIAlarm.ALARM_RELATED_ABSOLUTE && reminder.offset && reminder.action == defaultAlarmType ) { // Exactly one reminder that's not absolute, we may be able to match up // popup items. let relation = reminder.related == Ci.calIAlarm.ALARM_RELATED_START ? "START" : "END"; // If the time duration for offset is 0, means the reminder is '0 minutes before' let origin = reminder.offset.inSeconds == 0 || reminder.offset.isNegative ? "before" : "after"; let unitMap = { days: 86400, hours: 3600, minutes: 60, }; for (let menuitem of reminderPopup.children) { if ( menuitem.localName == "menuitem" && menuitem.hasAttribute("length") && menuitem.getAttribute("origin") == origin && menuitem.getAttribute("relation") == relation ) { let unitMult = unitMap[menuitem.getAttribute("unit")] || 1; let length = menuitem.getAttribute("length") * unitMult; if (Math.abs(reminder.offset.inSeconds) == length) { menuitem.reminder = reminder.clone(); reminderList.selectedItem = menuitem; // We've selected an item, so we are done here. return true; } } } } return false; } /** * Load an item's reminders into the dialog. * * @param {calIAlarm[]} reminders - An array of alarms to load. * @param {Element} reminderList - The reminders menulist element. * @param {calICalendar} calendar - The calendar the item belongs to. * @returns {number} Index of the selected item in reminders menu. */ function loadReminders(reminders, reminderList, calendar) { // Select 'no reminder' by default. reminderList.selectedIndex = 0; if (!reminders || !reminders.length) { // No reminders selected, we are done return reminderList.selectedIndex; } if ( reminders.length > 1 || !matchCustomReminderToMenuitem(reminders[0], reminderList, calendar) ) { // If more than one alarm is selected, or we didn't find a matching item // above, then select the "custom" item and attach the item's reminders to // it. reminderList.value = "custom"; reminderList.querySelector(".reminder-custom-menuitem").reminders = reminders; } // Return the selected index so it can be remembered. return reminderList.selectedIndex; } /** * Save the selected reminder into the passed item. * * @param {calIEvent | calITodo} item The calendar item to save the reminder into. * @param {calICalendar} calendar - The current calendar. * @param {Element} reminderList - The reminder menu element. */ function saveReminder(item, calendar, reminderList) { // We want to compare the old alarms with the new ones. If these are not // the same, then clear the snooze/dismiss times let oldAlarmMap = {}; for (let alarm of item.getAlarms()) { oldAlarmMap[alarm.icalString] = true; } // Clear the alarms so we can add our new ones. item.clearAlarms(); if (reminderList.value != "none") { let menuitem = reminderList.selectedItem; let reminders; if (menuitem.reminders) { // Custom reminder entries carry their own reminder object with // them. Make sure to clone in case these are the original item's // reminders. // XXX do we need to clone here? reminders = menuitem.reminders.map(x => x.clone()); } else { // Pre-defined entries specify the necessary information // as attributes attached to the menuitem elements. reminders = [createReminderFromMenuitem(menuitem, calendar)]; } let alarmCaps = item.calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"]; let alarmActions = {}; for (let action of alarmCaps) { alarmActions[action] = true; } // Make sure only alarms are saved that work in the given calendar. reminders.filter(x => x.action in alarmActions).forEach(item.addAlarm, item); } // Compare alarms to see if something changed. for (let alarm of item.getAlarms()) { let ics = alarm.icalString; if (ics in oldAlarmMap) { // The new alarm is also in the old set, remember this delete oldAlarmMap[ics]; } else { // The new alarm is not in the old set, this means the alarms // differ and we can break out. oldAlarmMap[ics] = true; break; } } // If the alarms differ, clear the snooze/dismiss properties if (Object.keys(oldAlarmMap).length > 0) { let cmp = "X-MOZ-SNOOZE-TIME"; // Recurring item alarms potentially have more snooze props, remove them // all. let propsToDelete = []; for (let [name] of item.properties) { if (name.startsWith(cmp)) { propsToDelete.push(name); } } item.alarmLastAck = null; propsToDelete.forEach(item.deleteProperty, item); } } /** * Get the default alarm type for the currently selected calendar. If the * calendar supports DISPLAY alarms, this is the default. Otherwise it is the * first alarm action the calendar supports. * * @param {calICalendar} calendar - The calendar to use. * @returns {string} The default alarm type. */ function getDefaultAlarmType(calendar) { let alarmCaps = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"]; return alarmCaps.includes("DISPLAY") ? "DISPLAY" : alarmCaps[0]; } /** * Common update functions for both event dialogs. Called when a reminder has * been selected from the menulist. * * @param {Element} reminderList - The reminders menu element. * @param {calIEvent | calITodo} calendarItem - The calendar item. * @param {number} lastAlarmSelection - Index of the previous selection in the reminders menu. * @param {Element} reminderDetails - The reminder details element. * @param {calITimezone} timezone - The relevant timezone. * @param {boolean} suppressDialogs - If true, controls are updated without prompting * for changes with the dialog * @returns {number} Index of the item selected in the reminders menu. */ function commonUpdateReminder( reminderList, calendarItem, lastAlarmSelection, calendar, reminderDetails, timezone, suppressDialogs ) { // if a custom reminder has been selected, we show the appropriate // dialog in order to allow the user to specify the details. // the result will be placed in the 'reminder-custom-menuitem' tag. if (reminderList.value == "custom") { // Clear the reminder icons first, this will make sure that while the // dialog is open the default reminder image is not shown which may // confuse users. let iconBox = reminderDetails.querySelector(".alarm-icons-box"); while (iconBox.lastChild) { iconBox.lastChild.remove(); } // show the dialog. This call blocks until the dialog is closed. Don't // pop up the dialog if aSuppressDialogs was specified or if this // happens during initialization of the dialog if (!suppressDialogs && reminderList.hasAttribute("last-value")) { editReminder(reminderList, calendarItem, lastAlarmSelection, calendar, timezone); } if (reminderList.value == "custom") { // Only do this if the 'custom' item is still selected. If the edit // reminder dialog was canceled then the previously selected // menuitem is selected, which may not be the custom menuitem. // If one or no reminders were selected, we have a chance of mapping // them to the existing elements in the dropdown. let customItem = reminderList.selectedItem; if (customItem.reminders.length == 0) { // No reminder was selected reminderList.value = "none"; } else if (customItem.reminders.length == 1) { // We might be able to match the custom reminder with one of the // default menu items. matchCustomReminderToMenuitem(customItem.reminders[0], reminderList, calendar); } } } reminderList.setAttribute("last-value", reminderList.value); // possibly the selected reminder conflicts with the item. // for example an end-relation combined with a task without duedate // is an invalid state we need to take care of. we take the same // approach as with recurring tasks. in case the reminder is related // to the entry date we check the entry date automatically and disable // the checkbox. the same goes for end related reminder and the due date. if (calendarItem.isTodo()) { // In general, (re-)enable the due/entry checkboxes. This will be // changed in case the alarms are related to START/END below. enableElementWithLock("todo-has-duedate", "reminder-lock"); enableElementWithLock("todo-has-entrydate", "reminder-lock"); let menuitem = reminderList.selectedItem; if (menuitem.value != "none") { // In case a reminder is selected, retrieve the array of alarms from // it, or create one from the currently selected menuitem. let reminders = menuitem.reminders || [createReminderFromMenuitem(menuitem, calendar)]; // If a reminder is related to the entry date... if (reminders.some(x => x.related == Ci.calIAlarm.ALARM_RELATED_START)) { // ...automatically check 'has entrydate'. if (!document.getElementById("todo-has-entrydate").checked) { document.getElementById("todo-has-entrydate").checked = true; // Make sure gStartTime is properly initialized updateEntryDate(); } // Disable the checkbox to indicate that we need the entry-date. disableElementWithLock("todo-has-entrydate", "reminder-lock"); } // If a reminder is related to the due date... if (reminders.some(x => x.related == Ci.calIAlarm.ALARM_RELATED_END)) { // ...automatically check 'has duedate'. if (!document.getElementById("todo-has-duedate").checked) { document.getElementById("todo-has-duedate").checked = true; // Make sure gStartTime is properly initialized updateDueDate(); } // Disable the checkbox to indicate that we need the entry-date. disableElementWithLock("todo-has-duedate", "reminder-lock"); } } } updateReminderDetails(reminderDetails, reminderList, calendar); // Return the current reminder drop down selection index so it can be remembered. return reminderList.selectedIndex; } /** * Updates the related link on the dialog. Currently only used by the * read-only summary dialog. * * @param {string} itemUrlString - The calendar item URL as a string. * @param {Element} linkRow - The row containing the link. * @param {Element} urlLink - The link element itself. */ function updateLink(itemUrlString, linkRow, urlLink) { let linkCommand = document.getElementById("cmd_toggle_link"); if (linkCommand) { // Disable if there is no url. linkCommand.disabled = !itemUrlString; } if ((linkCommand && linkCommand.getAttribute("checked") != "true") || !itemUrlString.length) { // Hide if there is no url, or the menuitem was chosen so that the url // should be hidden linkRow.hidden = true; } else { let handler, uri; try { uri = Services.io.newURI(itemUrlString); handler = Services.io.getProtocolHandler(uri.scheme); } catch (e) { // No protocol handler for the given protocol, or invalid uri linkRow.hidden = true; return; } // Only show if its either an internal protocol handler, or its external // and there is an external app for the scheme handler = cal.wrapInstance(handler, Ci.nsIExternalProtocolHandler); let show = !handler || handler.externalAppExistsForScheme(uri.scheme); linkRow.hidden = !show; setTimeout(() => { // HACK the url link doesn't crop when setting the value in onLoad urlLink.setAttribute("value", itemUrlString); urlLink.setAttribute("href", itemUrlString); }, 0); } } /** * Adapts the scheduling responsibility for caldav servers according to RfC 6638 * based on forceEmailScheduling preference for the respective calendar * * @param {calIEvent|calIToDo} aItem - Item to apply the change on */ function adaptScheduleAgent(aItem) { if ( aItem.calendar && aItem.calendar.type == "caldav" && aItem.calendar.getProperty("capabilities.autoschedule.supported") ) { let identity = aItem.calendar.getProperty("imip.identity"); let orgEmail = identity && identity.QueryInterface(Ci.nsIMsgIdentity).email; let organizerAction = aItem.organizer && orgEmail && aItem.organizer.id == "mailto:" + orgEmail; if (aItem.calendar.getProperty("forceEmailScheduling")) { cal.LOG("Enforcing clientside email based scheduling."); // for attendees, we change schedule-agent only in case of an // organizer triggered action if (organizerAction) { aItem.getAttendees().forEach(aAttendee => { // overwriting must always happen consistently for all // attendees regarding SERVER or CLIENT but must not override // e.g. NONE, so we only overwrite if the param is set to // SERVER or doesn't exist if ( aAttendee.getProperty("SCHEDULE-AGENT") == "SERVER" || !aAttendee.getProperty("SCHEDULE-AGENT") ) { aAttendee.setProperty("SCHEDULE-AGENT", "CLIENT"); aAttendee.deleteProperty("SCHEDULE-STATUS"); aAttendee.deleteProperty("SCHEDULE-FORCE-SEND"); } }); } else if ( aItem.organizer && (aItem.organizer.getProperty("SCHEDULE-AGENT") == "SERVER" || !aItem.organizer.getProperty("SCHEDULE-AGENT")) ) { // for organizer, we change the schedule-agent only in case of // an attendee triggered action aItem.organizer.setProperty("SCHEDULE-AGENT", "CLIENT"); aItem.organizer.deleteProperty("SCHEDULE-STATUS"); aItem.organizer.deleteProperty("SCHEDULE-FORCE-SEND"); } } else if (organizerAction) { aItem.getAttendees().forEach(aAttendee => { if (aAttendee.getProperty("SCHEDULE-AGENT") == "CLIENT") { aAttendee.deleteProperty("SCHEDULE-AGENT"); } }); } else if (aItem.organizer && aItem.organizer.getProperty("SCHEDULE-AGENT") == "CLIENT") { aItem.organizer.deleteProperty("SCHEDULE-AGENT"); } } } /** * Extracts the item's organizer and opens a compose window to send the * organizer an email. * * @param {calIEvent | calITodo} item - The calendar item. */ function sendMailToOrganizer(item) { let organizer = item.organizer; let email = cal.email.getAttendeeEmail(organizer, true); let emailSubject = cal.l10n.getString("calendar-event-dialog", "emailSubjectReply", [item.title]); let identity = item.calendar.getProperty("imip.identity"); cal.email.sendTo(email, emailSubject, null, identity); } /** * Opens an attachment. * * @param {AUTF8String} aAttachmentId The hashId of the attachment to open. * @param {calIEvent | calITodo} item The calendar item. */ function openAttachmentFromItemSummary(aAttachmentId, item) { if (!aAttachmentId) { return; } let attachments = item .getAttachments() .filter(aAttachment => aAttachment.hashId == aAttachmentId); if (attachments.length && attachments[0].uri && attachments[0].uri.spec != "about:blank") { Cc["@mozilla.org/uriloader/external-protocol-service;1"] .getService(Ci.nsIExternalProtocolService) .loadURI(attachments[0].uri); } }