1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
6var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
7var { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
8
9// Usually the backend loader gets loaded via profile-after-change, but in case
10// a calendar component hooks in earlier, its very likely it will use calUtils.
11// Getting the service here will load if its not already loaded
12Cc["@mozilla.org/calendar/backend-loader;1"].getService();
13
14// The calendar console instance
15var gCalendarConsole = new ConsoleAPI({
16  prefix: "Calendar",
17  consoleID: "calendar",
18  maxLogLevel: Services.prefs.getBoolPref("calendar.debug.log", false) ? "all" : "warn",
19});
20
21// Cache services to avoid calling getService over and over again. The cache is
22// a separate object to avoid polluting `cal`, and is defined here since a call
23// to `_service` will require it to already exist.
24var gServiceCache = {};
25
26const EXPORTED_SYMBOLS = ["cal"];
27var cal = {
28  // These functions exist to reduce boilerplate code for creating instances
29  // as well as getting services and other (cached) objects.
30  createDateTime: _instance("@mozilla.org/calendar/datetime;1", Ci.calIDateTime, "icalString"),
31  createDuration: _instance("@mozilla.org/calendar/duration;1", Ci.calIDuration, "icalString"),
32  createRecurrenceDate: _instance(
33    "@mozilla.org/calendar/recurrence-date;1",
34    Ci.calIRecurrenceDate,
35    "icalString"
36  ),
37  createRecurrenceRule: _instance(
38    "@mozilla.org/calendar/recurrence-rule;1",
39    Ci.calIRecurrenceRule,
40    "icalString"
41  ),
42
43  getCalendarManager: _service("@mozilla.org/calendar/manager;1", "calICalendarManager"),
44  getIcsService: _service("@mozilla.org/calendar/ics-service;1", "calIICSService"),
45  getTimezoneService: _service("@mozilla.org/calendar/timezone-service;1", "calITimezoneService"),
46  getFreeBusyService: _service("@mozilla.org/calendar/freebusy-service;1", "calIFreeBusyService"),
47  getWeekInfoService: _service("@mozilla.org/calendar/weekinfo-service;1", "calIWeekInfoService"),
48  getDragService: _service("@mozilla.org/widget/dragservice;1", "nsIDragService"),
49
50  /**
51   * The calendar console instance
52   */
53  console: gCalendarConsole,
54
55  /**
56   * Logs a calendar message to the console. Needs calendar.debug.log enabled to show messages.
57   * Shortcut to cal.console.log()
58   */
59  LOG: gCalendarConsole.log,
60  LOGverbose: gCalendarConsole.debug,
61
62  /**
63   * Logs a calendar warning to the console. Shortcut to cal.console.warn()
64   */
65  WARN: gCalendarConsole.warn,
66
67  /**
68   * Logs a calendar error to the console. Shortcut to cal.console.error()
69   */
70  ERROR: gCalendarConsole.error,
71
72  /**
73   * Uses the prompt service to display an error message. Use this sparingly,
74   * as it interrupts the user.
75   *
76   * @param aMsg The message to be shown
77   * @param aWindow The window to show the message in, or null for any window.
78   */
79  showError(aMsg, aWindow = null) {
80    Services.prompt.alert(aWindow, cal.l10n.getCalString("genericErrorTitle"), aMsg);
81  },
82
83  /**
84   * Returns a string describing the current js-stack with filename and line
85   * numbers.
86   *
87   * @param aDepth (optional) The number of frames to include. Defaults to 5.
88   * @param aSkip  (optional) Number of frames to skip
89   */
90  STACK(aDepth = 10, aSkip = 0) {
91    let stack = "";
92    let frame = Components.stack.caller;
93    for (let i = 1; i <= aDepth + aSkip && frame; i++) {
94      if (i > aSkip) {
95        stack += `${i}: [${frame.filename}:${frame.lineNumber}] ${frame.name}\n`;
96      }
97      frame = frame.caller;
98    }
99    return stack;
100  },
101
102  /**
103   * Logs a message and the current js-stack, if aCondition fails
104   *
105   * @param aCondition  the condition to test for
106   * @param aMessage    the message to report in the case the assert fails
107   * @param aCritical   if true, throw an error to stop current code execution
108   *                    if false, code flow will continue
109   *                    may be a result code
110   */
111  ASSERT(aCondition, aMessage, aCritical = false) {
112    if (aCondition) {
113      return;
114    }
115
116    let string = `Assert failed: ${aMessage}\n ${cal.STACK(0, 1)}`;
117    if (aCritical) {
118      let rescode = aCritical === true ? Cr.NS_ERROR_UNEXPECTED : aCritical;
119      throw new Components.Exception(string, rescode);
120    } else {
121      Cu.reportError(string);
122    }
123  },
124
125  /**
126   * Generates the QueryInterface function. This is a replacement for XPCOMUtils.generateQI, which
127   * is being replaced. Unfortunately Calendar's code depends on some of its classes providing
128   * nsIClassInfo, which causes xpconnect/xpcom to make all methods available, e.g. for an event
129   * both calIItemBase and calIEvent.
130   *
131   * @param {Array<String|nsIIDRef>} aInterfaces      The interfaces to generate QI for.
132   * @return {Function}                               The QueryInterface function
133   */
134  generateQI(aInterfaces) {
135    if (aInterfaces.length == 1) {
136      cal.WARN(
137        "When generating QI for one interface, please use ChromeUtils.generateQI",
138        cal.STACK(10)
139      );
140      return ChromeUtils.generateQI(aInterfaces);
141    }
142    /* Note that Ci[Ci.x] == Ci.x for all x */
143    let names = [];
144    if (aInterfaces) {
145      for (let i = 0; i < aInterfaces.length; i++) {
146        let iface = aInterfaces[i];
147        let name = (iface && iface.name) || String(iface);
148        if (name in Ci) {
149          names.push(name);
150        }
151      }
152    }
153    return makeQI(names);
154  },
155
156  /**
157   * Generate a ClassInfo implementation for a component. The returned object
158   * must be assigned to the 'classInfo' property of a JS object. The first and
159   * only argument should be an object that contains a number of optional
160   * properties: "interfaces", "contractID", "classDescription", "classID" and
161   * "flags". The values of the properties will be returned as the values of the
162   * various properties of the nsIClassInfo implementation.
163   */
164  generateCI(classInfo) {
165    if ("QueryInterface" in classInfo) {
166      throw Error("In generateCI, don't use a component for generating classInfo");
167    }
168    /* Note that Ci[Ci.x] == Ci.x for all x */
169    let _interfaces = [];
170    for (let i = 0; i < classInfo.interfaces.length; i++) {
171      let iface = classInfo.interfaces[i];
172      if (Ci[iface]) {
173        _interfaces.push(Ci[iface]);
174      }
175    }
176    return {
177      get interfaces() {
178        return [Ci.nsIClassInfo, Ci.nsISupports].concat(_interfaces);
179      },
180      getScriptableHelper() {
181        return null;
182      },
183      contractID: classInfo.contractID,
184      classDescription: classInfo.classDescription,
185      classID: classInfo.classID,
186      flags: classInfo.flags,
187      QueryInterface: ChromeUtils.generateQI(["nsIClassInfo"]),
188    };
189  },
190
191  /**
192   * Schedules execution of the passed function to the current thread's queue.
193   */
194  postPone(func) {
195    if (this.threadingEnabled) {
196      Services.tm.currentThread.dispatch({ run: func }, Ci.nsIEventTarget.DISPATCH_NORMAL);
197    } else {
198      func();
199    }
200  },
201
202  /**
203   * Create an adapter for the given interface. If passed, methods will be
204   * added to the template object, otherwise a new object will be returned.
205   *
206   * @param iface     The interface to adapt, either using
207   *                    Components.interfaces or the name as a string.
208   * @param template  (optional) A template object to extend
209   * @return          If passed the adapted template object, otherwise a
210   *                    clean adapter.
211   *
212   * Currently supported interfaces are:
213   *  - calIObserver
214   *  - calICalendarManagerObserver
215   *  - calIOperationListener
216   *  - calICompositeObserver
217   */
218  createAdapter(iface, template) {
219    let methods;
220    let adapter = template || {};
221    switch (iface.name || iface) {
222      case "calIObserver":
223        methods = [
224          "onStartBatch",
225          "onEndBatch",
226          "onLoad",
227          "onAddItem",
228          "onModifyItem",
229          "onDeleteItem",
230          "onError",
231          "onPropertyChanged",
232          "onPropertyDeleting",
233        ];
234        break;
235      case "calICalendarManagerObserver":
236        methods = ["onCalendarRegistered", "onCalendarUnregistering", "onCalendarDeleting"];
237        break;
238      case "calIOperationListener":
239        methods = ["onGetResult", "onOperationComplete"];
240        break;
241      case "calICompositeObserver":
242        methods = ["onCalendarAdded", "onCalendarRemoved", "onDefaultCalendarChanged"];
243        break;
244      default:
245        methods = [];
246        break;
247    }
248
249    for (let method of methods) {
250      if (!(method in template)) {
251        adapter[method] = function() {};
252      }
253    }
254    adapter.QueryInterface = ChromeUtils.generateQI([iface]);
255
256    return adapter;
257  },
258
259  /**
260   * Make a UUID, without enclosing brackets, e.g. 0d3950fd-22e5-4508-91ba-0489bdac513f
261   *
262   * @return {String}         The generated UUID
263   */
264  getUUID() {
265    let uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
266    // generate uuids without braces to avoid problems with
267    // CalDAV servers that don't support filenames with {}
268    return uuidGen
269      .generateUUID()
270      .toString()
271      .replace(/[{}]/g, "");
272  },
273
274  /**
275   * Adds an observer listening for the topic.
276   *
277   * @param func function to execute on topic
278   * @param topic topic to listen for
279   * @param oneTime whether to listen only once
280   */
281  addObserver(func, topic, oneTime) {
282    let observer = {
283      // nsIObserver:
284      observe(subject, topic_, data) {
285        if (topic == topic_) {
286          if (oneTime) {
287            Services.obs.removeObserver(this, topic);
288          }
289          func(subject, topic, data);
290        }
291      },
292    };
293    Services.obs.addObserver(observer, topic);
294  },
295
296  /**
297   * Wraps an instance, making sure the xpcom wrapped object is used.
298   *
299   * @param aObj the object under consideration
300   * @param aInterface the interface to be wrapped
301   *
302   * Use this function to QueryInterface the object to a particular interface.
303   * You may only expect the return value to be wrapped, not the original passed object.
304   * For example:
305   * // BAD USAGE:
306   * if (cal.wrapInstance(foo, Ci.nsIBar)) {
307   *   foo.barMethod();
308   * }
309   * // GOOD USAGE:
310   * foo = cal.wrapInstance(foo, Ci.nsIBar);
311   * if (foo) {
312   *   foo.barMethod();
313   * }
314   *
315   */
316  wrapInstance(aObj, aInterface) {
317    if (!aObj) {
318      return null;
319    }
320
321    try {
322      return aObj.QueryInterface(aInterface);
323    } catch (e) {
324      return null;
325    }
326  },
327
328  /**
329   * Tries to get rid of wrappers, if this is not possible then return the
330   * passed object.
331   *
332   * @param aObj  The object under consideration
333   * @return      The possibly unwrapped object.
334   */
335  unwrapInstance(aObj) {
336    return aObj && aObj.wrappedJSObject ? aObj.wrappedJSObject : aObj;
337  },
338
339  /**
340   * Adds an xpcom shutdown observer.
341   *
342   * @param func function to execute
343   */
344  addShutdownObserver(func) {
345    cal.addObserver(func, "xpcom-shutdown", true /* one time */);
346  },
347
348  /**
349   * Due to wrapped js objects, some objects may have cyclic references.
350   * You can register properties of objects to be cleaned up on xpcom-shutdown.
351   *
352   * @param obj    object
353   * @param prop   property to be deleted on shutdown
354   *               (if null, |object| will be deleted)
355   */
356  registerForShutdownCleanup: shutdownCleanup,
357};
358
359/**
360 * Update the logging preferences for the calendar console based on the state of verbose logging and
361 * normal calendar logging.
362 */
363function updateLogPreferences() {
364  if (cal.verboseLogEnabled) {
365    gCalendarConsole.maxLogLevel = "all";
366  } else if (cal.debugLogEnabled) {
367    gCalendarConsole.maxLogLevel = "log";
368  } else {
369    gCalendarConsole.maxLogLevel = "warn";
370  }
371}
372
373// Preferences
374XPCOMUtils.defineLazyPreferenceGetter(
375  cal,
376  "debugLogEnabled",
377  "calendar.debug.log",
378  false,
379  updateLogPreferences
380);
381XPCOMUtils.defineLazyPreferenceGetter(
382  cal,
383  "verboseLogEnabled",
384  "calendar.debug.log.verbose",
385  false,
386  updateLogPreferences
387);
388XPCOMUtils.defineLazyPreferenceGetter(
389  cal,
390  "threadingEnabled",
391  "calendar.threading.disabled",
392  false
393);
394
395// Sub-modules for calUtils
396XPCOMUtils.defineLazyModuleGetter(
397  cal,
398  "acl",
399  "resource:///modules/calendar/utils/calACLUtils.jsm",
400  "calacl"
401);
402XPCOMUtils.defineLazyModuleGetter(
403  cal,
404  "alarms",
405  "resource:///modules/calendar/utils/calAlarmUtils.jsm",
406  "calalarms"
407);
408XPCOMUtils.defineLazyModuleGetter(
409  cal,
410  "async",
411  "resource:///modules/calendar/utils/calAsyncUtils.jsm",
412  "calasync"
413);
414XPCOMUtils.defineLazyModuleGetter(
415  cal,
416  "auth",
417  "resource:///modules/calendar/utils/calAuthUtils.jsm",
418  "calauth"
419);
420XPCOMUtils.defineLazyModuleGetter(
421  cal,
422  "category",
423  "resource:///modules/calendar/utils/calCategoryUtils.jsm",
424  "calcategory"
425);
426XPCOMUtils.defineLazyModuleGetter(
427  cal,
428  "data",
429  "resource:///modules/calendar/utils/calDataUtils.jsm",
430  "caldata"
431);
432XPCOMUtils.defineLazyModuleGetter(
433  cal,
434  "dtz",
435  "resource:///modules/calendar/utils/calDateTimeUtils.jsm",
436  "caldtz"
437);
438XPCOMUtils.defineLazyModuleGetter(
439  cal,
440  "email",
441  "resource:///modules/calendar/utils/calEmailUtils.jsm",
442  "calemail"
443);
444XPCOMUtils.defineLazyModuleGetter(
445  cal,
446  "invitation",
447  "resource:///modules/calendar/utils/calInvitationUtils.jsm",
448  "calinvitation"
449);
450XPCOMUtils.defineLazyModuleGetter(
451  cal,
452  "item",
453  "resource:///modules/calendar/utils/calItemUtils.jsm",
454  "calitem"
455);
456XPCOMUtils.defineLazyModuleGetter(
457  cal,
458  "iterate",
459  "resource:///modules/calendar/utils/calIteratorUtils.jsm",
460  "caliterate"
461);
462XPCOMUtils.defineLazyModuleGetter(
463  cal,
464  "itip",
465  "resource:///modules/calendar/utils/calItipUtils.jsm",
466  "calitip"
467);
468XPCOMUtils.defineLazyModuleGetter(
469  cal,
470  "l10n",
471  "resource:///modules/calendar/utils/calL10NUtils.jsm",
472  "call10n"
473);
474XPCOMUtils.defineLazyModuleGetter(
475  cal,
476  "print",
477  "resource:///modules/calendar/utils/calPrintUtils.jsm",
478  "calprint"
479);
480XPCOMUtils.defineLazyModuleGetter(
481  cal,
482  "provider",
483  "resource:///modules/calendar/utils/calProviderUtils.jsm",
484  "calprovider"
485);
486XPCOMUtils.defineLazyModuleGetter(
487  cal,
488  "unifinder",
489  "resource:///modules/calendar/utils/calUnifinderUtils.jsm",
490  "calunifinder"
491);
492XPCOMUtils.defineLazyModuleGetter(
493  cal,
494  "view",
495  "resource:///modules/calendar/utils/calViewUtils.jsm",
496  "calview"
497);
498XPCOMUtils.defineLazyModuleGetter(
499  cal,
500  "window",
501  "resource:///modules/calendar/utils/calWindowUtils.jsm",
502  "calwindow"
503);
504XPCOMUtils.defineLazyModuleGetter(
505  cal,
506  "xml",
507  "resource:///modules/calendar/utils/calXMLUtils.jsm",
508  "calxml"
509);
510
511/**
512 * Returns a function that provides access to the given service.
513 *
514 * @param cid           The contract id to create
515 * @param iid           The interface id to create with
516 * @return {function}   A function that returns the given service
517 */
518function _service(cid, iid) {
519  let name = `_${iid}`;
520  XPCOMUtils.defineLazyServiceGetter(gServiceCache, name, cid, iid);
521  return function() {
522    return gServiceCache[name];
523  };
524}
525
526/**
527 * Returns a function that creates an instance of the given component and
528 * optionally initializes it using the property name passed.
529 *
530 * @param cid           The contract id to create
531 * @param iid           The interface id to create with
532 * @param prop          The property name used for initialization
533 * @return {function}   A function that creates the given instance, which takes an
534 *                          initialization value.
535 */
536function _instance(cid, iid, prop) {
537  return function(propval) {
538    let thing = Cc[cid].createInstance(iid);
539    if (propval) {
540      thing[prop] = propval;
541    }
542    return thing;
543  };
544}
545
546// will be used to clean up global objects on shutdown
547// some objects have cyclic references due to wrappers
548function shutdownCleanup(obj, prop) {
549  if (!shutdownCleanup.mEntries) {
550    shutdownCleanup.mEntries = [];
551    cal.addShutdownObserver(() => {
552      for (let entry of shutdownCleanup.mEntries) {
553        if (entry.mProp) {
554          delete entry.mObj[entry.mProp];
555        } else {
556          delete entry.mObj;
557        }
558      }
559      delete shutdownCleanup.mEntries;
560    });
561  }
562  shutdownCleanup.mEntries.push({ mObj: obj, mProp: prop });
563}
564
565/**
566 * This is the makeQI function from XPCOMUtils.jsm, it is separate to avoid leaks
567 *
568 * @param {Array<String|nsIIDRef>} aInterfaces      The interfaces to make QI for.
569 * @return {Function}                               The QueryInterface function.
570 */
571function makeQI(aInterfaces) {
572  return function(iid) {
573    if (iid.equals(Ci.nsISupports)) {
574      return this;
575    }
576    if (iid.equals(Ci.nsIClassInfo) && "classInfo" in this) {
577      return this.classInfo;
578    }
579    for (let i = 0; i < aInterfaces.length; i++) {
580      if (Ci[aInterfaces[i]].equals(iid)) {
581        return this;
582      }
583    }
584
585    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
586  };
587}
588