Bug 340949: error handling, task alarm handling, misc
git-svn-id: svn://10.0.0.236/trunk@208804 18797224-902f-48f8-a5cc-f745e15eee43
This commit is contained in:
parent
0f2080bb34
commit
6055018da3
@ -183,9 +183,11 @@ calWcapCachedCalendar.prototype = {
|
||||
function( err )
|
||||
{
|
||||
debugger;
|
||||
var str = this.logError( err );
|
||||
this.notifyObservers( "onError",
|
||||
[err instanceof Error ? -1 : err, str] );
|
||||
var msg = this.logError(err);
|
||||
this.notifyObservers(
|
||||
"onError",
|
||||
err instanceof Components.interfaces.nsIException
|
||||
? [err.result, err.message] : [-1, msg] );
|
||||
},
|
||||
|
||||
// calIWcapCalendar:
|
||||
@ -472,7 +474,7 @@ calWcapCachedCalendar.prototype = {
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error(
|
||||
throw new Components.Exception(
|
||||
"unexpected operation type! " +
|
||||
"(expected SYNC)" );
|
||||
}
|
||||
|
||||
@ -128,17 +128,19 @@ calWcapCalendar.prototype = {
|
||||
logError:
|
||||
function( err, context )
|
||||
{
|
||||
var str = ("error: " + errorToString(err));
|
||||
Components.utils.reportError( this.log( str, context ) );
|
||||
return str;
|
||||
var msg = errorToString(err);
|
||||
Components.utils.reportError( this.log("error: " + msg, context) );
|
||||
return msg;
|
||||
},
|
||||
notifyError:
|
||||
function( err )
|
||||
{
|
||||
debugger;
|
||||
var str = this.logError( err );
|
||||
this.notifyObservers( "onError",
|
||||
[err instanceof Error ? -1 : err, str] );
|
||||
var msg = this.logError(err);
|
||||
this.notifyObservers(
|
||||
"onError",
|
||||
err instanceof Components.interfaces.nsIException
|
||||
? [err.result, err.message] : [-1, msg] );
|
||||
},
|
||||
|
||||
// calICalendar:
|
||||
@ -319,7 +321,6 @@ calWcapCalendar.prototype = {
|
||||
},
|
||||
// set defaultTimezone( tzid ) {
|
||||
// if (this.readOnly)
|
||||
// throw Components.interfaces.calIErrors.CAL_IS_READONLY;
|
||||
// // xxx todo:
|
||||
// throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
||||
// },
|
||||
|
||||
@ -355,11 +355,12 @@ calWcapCalendar.prototype.storeItem = function( item, oldItem, receiverFunc )
|
||||
// undefined, assume an allDay todo???
|
||||
dtstart = item.entryDate;
|
||||
dtend = item.dueDate;
|
||||
if (dtend) {
|
||||
url += ("&due=" + getIcalUTC(dtend));
|
||||
// url += ("&X-NSCP-DUE-TZID=" + encodeURIComponent(
|
||||
// this.getAlignedTimezone(dtend.timezone)));
|
||||
}
|
||||
url += ("&due=" + getIcalUTC(dtend));
|
||||
// if (dtend) {
|
||||
// url += ("&X-NSCP-DUE-TZID=" + encodeURIComponent(
|
||||
// this.getAlignedTimezone(dtend.timezone)));
|
||||
// }
|
||||
|
||||
bIsAllDay = (dtstart && dtstart.isDate);
|
||||
if (item.isCompleted)
|
||||
url += "&percent=100";
|
||||
@ -383,10 +384,10 @@ calWcapCalendar.prototype.storeItem = function( item, oldItem, receiverFunc )
|
||||
if (bIsAllDay)
|
||||
url += "&isAllDay=1";
|
||||
|
||||
url += ("&dtstart=" + getIcalUTC(dtstart));
|
||||
if (dtstart) {
|
||||
// important to provide tz info with entry date for proper
|
||||
// occurrence calculation (daylight savings)
|
||||
url += ("&dtstart=" + getIcalUTC(dtstart));
|
||||
// xxx todo: setting X-NSCP-tz does not work.
|
||||
// i.e. for now no separate tz for start/end.
|
||||
// url += ("&X-NSCP-DTSTART-TZID=" +
|
||||
@ -396,6 +397,10 @@ calWcapCalendar.prototype.storeItem = function( item, oldItem, receiverFunc )
|
||||
this.getAlignedTimezone(dtstart.timezone)));
|
||||
}
|
||||
|
||||
// xxx todo: alarm mimic of todos currently different:
|
||||
// WCAP offsets relate to DUE
|
||||
if (bIsEvent) {
|
||||
|
||||
// alarm support:
|
||||
var alarmStart = item.alarmOffset;
|
||||
if (alarmStart) {
|
||||
@ -422,19 +427,16 @@ calWcapCalendar.prototype.storeItem = function( item, oldItem, receiverFunc )
|
||||
// this.log( "setting default alarm start: " + alarmStart.icalString );
|
||||
// }
|
||||
// }
|
||||
url += "&alarmStart=";
|
||||
if (alarmStart) {
|
||||
// minimal alarm server support: Alarms are currently off by default,
|
||||
// so let server at least send reminder eMails...
|
||||
url += alarmStart.icalString;
|
||||
url += "&alarmEmails=";
|
||||
var emails = "";
|
||||
if (item.hasProperty("alarmEmailAddress"))
|
||||
url += encodeURIComponent( item.getProperty("alarmEmailAddress") );
|
||||
emails = encodeURIComponent(item.getProperty("alarmEmailAddress"));
|
||||
else {
|
||||
var ar = this.session.getUserPreferences(
|
||||
"X-NSCP-WCAP-PREF-ceDefaultAlarmEmail", {});
|
||||
if (ar.length > 0 && ar[0].length > 0) {
|
||||
var emails = "";
|
||||
for each ( var i in ar ) {
|
||||
var ars = i.split(/[;,]/);
|
||||
for each ( var j in ars ) {
|
||||
@ -446,20 +448,21 @@ calWcapCalendar.prototype.storeItem = function( item, oldItem, receiverFunc )
|
||||
}
|
||||
}
|
||||
}
|
||||
url += emails;
|
||||
url += "&alarmPopup=";
|
||||
}
|
||||
else {
|
||||
// xxx todo: popup exor emails can be currently specified...
|
||||
url += ("&alarmPopup=" + alarmStart.icalString);
|
||||
}
|
||||
}
|
||||
url += ("&alarmStart=" + alarmStart.icalString);
|
||||
url += ("&alarmEmails=" + emails);
|
||||
url += "&alarmPopup=";
|
||||
if (emails.length == 0)
|
||||
url += alarmStart.icalString;
|
||||
// xxx todo: missing: alarm triggers for flashing, etc.
|
||||
}
|
||||
else {
|
||||
// clear popup, emails:
|
||||
url += "&alarmPopup=&alarmEmails=";
|
||||
url += "&alarmStart=&alarmPopup=&alarmEmails=";
|
||||
}
|
||||
|
||||
} // if (bIsEvent)
|
||||
|
||||
// xxx todo: however, sometimes socs just returns an empty calendar when
|
||||
// nothing or only optional props like attendee ROLE has changed,
|
||||
@ -506,7 +509,7 @@ calWcapCalendar.prototype.adoptItem_resp = function( wcapResponse, listener )
|
||||
Components.interfaces.calICalendar.ITEM_FILTER_ALL_ITEMS,
|
||||
0, null, null );
|
||||
if (items.length < 1)
|
||||
throw new Error("empty VCALENDAR returned!");
|
||||
throw new Components.Exception("empty VCALENDAR returned!");
|
||||
if (items.length > 1)
|
||||
this.notifyError( "unexpected number of items: " + items.length );
|
||||
item = items[0];
|
||||
@ -535,9 +538,13 @@ calWcapCalendar.prototype.adoptItem_resp = function( wcapResponse, listener )
|
||||
calWcapCalendar.prototype.adoptItem = function( item, listener )
|
||||
{
|
||||
this.log( "adoptItem() call: " + item.title );
|
||||
if (this.readOnly)
|
||||
throw Components.interfaces.calIErrors.CAL_IS_READONLY;
|
||||
try {
|
||||
if (this.readOnly) {
|
||||
throw new Components.Exception(
|
||||
"Calendar is read-only.",
|
||||
Components.interfaces.calIErrors.CAL_IS_READONLY );
|
||||
}
|
||||
|
||||
// xxx todo: workaround really necessary for adding an occurrence?
|
||||
var oldItem = null;
|
||||
if (!isParent(item)) {
|
||||
@ -583,7 +590,7 @@ calWcapCalendar.prototype.modifyItem_resp = function(
|
||||
Components.interfaces.calICalendar.ITEM_FILTER_ALL_ITEMS,
|
||||
0, null, null );
|
||||
if (items.length < 1)
|
||||
throw new Error("empty VCALENDAR returned!");
|
||||
throw new Components.Exception("empty VCALENDAR returned!");
|
||||
if (items.length > 1)
|
||||
this.notifyError( "unexpected number of items: " + items.length );
|
||||
item = items[0];
|
||||
@ -610,13 +617,15 @@ calWcapCalendar.prototype.modifyItem_resp = function(
|
||||
|
||||
calWcapCalendar.prototype.modifyItem = function( newItem, oldItem, listener )
|
||||
{
|
||||
this.log( "modifyItem() call: " + newItem.id );
|
||||
if (this.readOnly)
|
||||
throw Components.interfaces.calIErrors.CAL_IS_READONLY;
|
||||
|
||||
this.log( "modifyItem() call: " + newItem.id );
|
||||
try {
|
||||
if (this.readOnly) {
|
||||
throw new Components.Exception(
|
||||
"Calendar is read-only.",
|
||||
Components.interfaces.calIErrors.CAL_IS_READONLY );
|
||||
}
|
||||
if (!newItem.id)
|
||||
throw new Error("new item has no id!");
|
||||
throw new Components.Exception("new item has no id!");
|
||||
|
||||
var this_ = this;
|
||||
this.storeItem(
|
||||
@ -673,11 +682,14 @@ calWcapCalendar.prototype.deleteItem_resp = function(
|
||||
calWcapCalendar.prototype.deleteItem = function( item, listener )
|
||||
{
|
||||
this.log( "deleteItem() call: " + item.id );
|
||||
if (this.readOnly)
|
||||
throw Components.interfaces.calIErrors.CAL_IS_READONLY;
|
||||
try {
|
||||
if (this.readOnly) {
|
||||
throw new Components.Exception(
|
||||
"Calendar is read-only.",
|
||||
Components.interfaces.calIErrors.CAL_IS_READONLY );
|
||||
}
|
||||
if (item.id == null)
|
||||
throw new Error("no item id!");
|
||||
throw new Components.Exception("no item id!");
|
||||
|
||||
var url = this.session.getCommandUrl(
|
||||
isEvent(item) ? "deleteevents_by_id" : "deletetodos_by_id" );
|
||||
@ -777,12 +789,13 @@ calWcapCalendar.prototype.parseItems = function(
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (item &&
|
||||
item.alarmOffset && !item.entryDate && !item.dueDate) {
|
||||
// xxx todo: loss on roundtrip
|
||||
this_.log( "app currently does not support " +
|
||||
"absolute alarm trigger datetimes. " +
|
||||
"Removing alarm from item: " + item.title );
|
||||
// if (item &&
|
||||
// item.alarmOffset && !item.entryDate && !item.dueDate) {
|
||||
// // xxx todo: loss on roundtrip
|
||||
// this_.log( "app currently does not support " +
|
||||
// "absolute alarm trigger datetimes. " +
|
||||
// "Removing alarm from item: " + item.title );
|
||||
if (item) { // xxx todo: todo alarms currently off
|
||||
item.alarmOffset = null;
|
||||
item.alarmLastAck = null;
|
||||
}
|
||||
@ -865,7 +878,7 @@ calWcapCalendar.prototype.parseItems = function(
|
||||
for each ( var item in excItems ) {
|
||||
var parent = uid2parent[item.id];
|
||||
if (parent == null) {
|
||||
this.logError( "getItems_resp(): no parent item for rid=" +
|
||||
this.logError( "parseItems(): no parent item for rid=" +
|
||||
item.recurrenceId );
|
||||
}
|
||||
else {
|
||||
@ -931,7 +944,7 @@ calWcapCalendar.prototype.getItem = function( id, listener )
|
||||
Components.interfaces.calICalendar.ITEM_FILTER_ALL_ITEMS,
|
||||
1, null, null );
|
||||
if (items.length < 1)
|
||||
throw new Error("no such item!");
|
||||
throw new Components.Exception("no such item!");
|
||||
if (items.length > 1) {
|
||||
this_.notifyError(
|
||||
"unexpected number of items: " + items.length );
|
||||
@ -941,7 +954,7 @@ calWcapCalendar.prototype.getItem = function( id, listener )
|
||||
listener.onGetResult(
|
||||
this_.superCalendar, Components.results.NS_OK,
|
||||
Components.interfaces.calIItemBase,
|
||||
this_.log( "getItems_resp(): success." ),
|
||||
this_.log( "getItem(): success." ),
|
||||
items.length, items );
|
||||
listener.onOperationComplete(
|
||||
this_.superCalendar, Components.results.NS_OK,
|
||||
@ -988,8 +1001,8 @@ calWcapCalendar.prototype.getItems_resp = function(
|
||||
var exc = wcapResponse.exception;
|
||||
// check whether access is denied,
|
||||
// then show free-busy information instead:
|
||||
if (exc && (exc == Components.interfaces.
|
||||
calIWcapErrors.WCAP_ACCESS_DENIED_TO_CALENDAR)) {
|
||||
if (testResultCode( exc, Components.interfaces.
|
||||
calIWcapErrors.WCAP_ACCESS_DENIED_TO_CALENDAR)) {
|
||||
if (listener != null) {
|
||||
var this_ = this;
|
||||
var freeBusyListener = { // calIWcapFreeBusyListener:
|
||||
@ -1048,7 +1061,7 @@ calWcapCalendar.prototype.getItems_resp = function(
|
||||
if (listener != null) {
|
||||
listener.onGetResult( this.superCalendar, Components.results.NS_OK,
|
||||
Components.interfaces.calIItemBase,
|
||||
this.log( "getItems_resp(): success." ),
|
||||
this.log( "getItems(): success." ),
|
||||
items.length, items );
|
||||
listener.onOperationComplete(
|
||||
this.superCalendar, Components.results.NS_OK,
|
||||
@ -1149,7 +1162,8 @@ calWcapCalendar.prototype.getItems = function(
|
||||
Components.interfaces.calIOperationListener.GET,
|
||||
null, exc );
|
||||
}
|
||||
if (exc == Components.interfaces.calIWcapErrors.WCAP_LOGIN_FAILED) {
|
||||
if (testResultCode(exc, Components.interfaces.
|
||||
calIWcapErrors.WCAP_LOGIN_FAILED)) {
|
||||
// silently ignore login failed, no calIObserver UI:
|
||||
this.log( "getItems_resp() ignored: " + errorToString(exc) );
|
||||
}
|
||||
@ -1211,14 +1225,16 @@ FinishListener.prototype = {
|
||||
}
|
||||
else if (this.m_opType != opType) {
|
||||
this.m_syncState.abort(
|
||||
new Error("unexpected operation type: " + opType) );
|
||||
new Components.Exception("unexpected operation type: " +
|
||||
opType) );
|
||||
}
|
||||
this.m_syncState.release();
|
||||
},
|
||||
onGetResult:
|
||||
function( calendar, status, itemType, detail, count, items )
|
||||
{
|
||||
this.m_syncState.abort( new Error("unexpected onGetResult()!") );
|
||||
this.m_syncState.abort(
|
||||
new Components.Exception("unexpected onGetResult()!") );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -60,6 +60,9 @@ var CACHE = "off";
|
||||
// denotes where to host local storage calendar(s)
|
||||
var CACHE_DIR = null;
|
||||
|
||||
// timeout for sync network requests (in secs):
|
||||
var SYNC_REQUESTS_TIMEOUT = 10;
|
||||
|
||||
// logging:
|
||||
#expand var LOG_LEVEL = __LOG_LEVEL__;
|
||||
var LOG_TIMEZONE = null;
|
||||
@ -92,6 +95,9 @@ function initWcapProvider()
|
||||
"busyItem.title.text");
|
||||
g_busyPhantomItemUuidPrefix = ("PHANTOM_uuid" + getTime().icalString);
|
||||
|
||||
SYNC_REQUESTS_TIMEOUT = getPref(
|
||||
"calendar.wcap.sync_request_timeout", 10);
|
||||
|
||||
LOG_TIMEZONE = getPref("calendar.timezone.local", null);
|
||||
|
||||
var logLevel = getPref("calendar.wcap.log_level", null);
|
||||
@ -166,7 +172,7 @@ function initWcapProvider()
|
||||
}
|
||||
}
|
||||
|
||||
var calWcapCalendarModule = {
|
||||
var calWcapCalendarModule = { // nsIModule:
|
||||
|
||||
WcapCalendarInfo: {
|
||||
classDescription: "Sun Java System Calendar Server WCAP Provider",
|
||||
@ -197,6 +203,16 @@ var calWcapCalendarModule = {
|
||||
fileSpec, location, type );
|
||||
},
|
||||
|
||||
unregisterSelf:
|
||||
function( compMgr, fileSpec, location ) {
|
||||
compMgr = compMgr.QueryInterface(
|
||||
Components.interfaces.nsIComponentRegistrar );
|
||||
compMgr.unregisterFactoryLocation(
|
||||
this.WcapCalendarInfo.classID, fileSpec );
|
||||
compMgr.unregisterFactoryLocation(
|
||||
this.WcapSessionInfo.classID, fileSpec );
|
||||
},
|
||||
|
||||
m_scriptsLoaded: false,
|
||||
getClassObject:
|
||||
function( compMgr, cid, iid )
|
||||
@ -230,9 +246,9 @@ var calWcapCalendarModule = {
|
||||
if (!iid.equals( Components.interfaces.nsIFactory ))
|
||||
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
||||
|
||||
return {
|
||||
createInstance:
|
||||
function( outer, iid ) {
|
||||
return { // nsIFactory:
|
||||
lockFactory: function( lock ) {},
|
||||
createInstance: function( outer, iid ) {
|
||||
if (outer != null)
|
||||
throw Components.results.NS_ERROR_NO_AGGREGATION;
|
||||
var session = new calWcapSession();
|
||||
|
||||
@ -37,10 +37,90 @@
|
||||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
//
|
||||
// Common netwerk errors:
|
||||
//
|
||||
const NS_ERROR_MODULE_BASE_OFFSET = 0x45;
|
||||
const NS_ERROR_MODULE_NETWORK = 6;
|
||||
|
||||
function generateFailure(module, code) {
|
||||
return ((1<<31) | ((module + NS_ERROR_MODULE_BASE_OFFSET) << 16) | code);
|
||||
}
|
||||
|
||||
function generateNetFailure(code) {
|
||||
return generateFailure(NS_ERROR_MODULE_NETWORK, code);
|
||||
}
|
||||
|
||||
function getErrorModule( rc ) {
|
||||
return (((rc >>> 16) & 0x7fff) - NS_ERROR_MODULE_BASE_OFFSET);
|
||||
}
|
||||
|
||||
// Cannot perform operation, because user is offline.
|
||||
// This has been taken from netwerk/base/public/nsNetError.h
|
||||
// The following error codes have been adopted from
|
||||
// netwerk/base/public/nsNetError.h
|
||||
// and ought to be defined in IDL. xxx todo
|
||||
const NS_ERROR_OFFLINE = ((1<<31) | ((6+0x45)<<16) | 16);
|
||||
|
||||
// The requested action could not be completed while the networking
|
||||
// is in the offline state.
|
||||
const NS_ERROR_OFFLINE = generateNetFailure(16);
|
||||
|
||||
// The async request failed for some unknown reason.
|
||||
const NS_BINDING_FAILED = generateNetFailure(1);
|
||||
|
||||
const g_nsNetErrorCodes = [
|
||||
NS_BINDING_FAILED,
|
||||
"The async request failed for some unknown reason.",
|
||||
/*NS_BINDING_ABORTED*/ generateNetFailure(2),
|
||||
"The async request failed because it was aborted by some user action.",
|
||||
/*NS_ERROR_MALFORMED_URI*/ generateNetFailure(10),
|
||||
"The URI is malformed.",
|
||||
/*NS_ERROR_UNKNOWN_PROTOCOL*/ generateNetFailure(18),
|
||||
"The URI scheme corresponds to an unknown protocol handler.",
|
||||
/*NS_ERROR_CONNECTION_REFUSED*/ generateNetFailure(13),
|
||||
"The connection attempt failed, for example, because no server was listening at specified host:port.",
|
||||
/*NS_ERROR_PROXY_CONNECTION_REFUSED*/ generateNetFailure(72),
|
||||
"The connection attempt to a proxy failed.",
|
||||
/*NS_ERROR_NET_TIMEOUT*/ generateNetFailure(14),
|
||||
"The connection was lost due to a timeout error.",
|
||||
NS_ERROR_OFFLINE,
|
||||
"The requested action could not be completed while the networking library is in the offline state.",
|
||||
/*NS_ERROR_PORT_ACCESS_NOT_ALLOWED*/ generateNetFailure(19),
|
||||
"The requested action was prohibited because it would have caused the networking library to establish a connection to an unsafe or otherwise banned port.",
|
||||
/*NS_ERROR_NET_RESET*/ generateNetFailure(20),
|
||||
"The connection was established, but no data was ever received.",
|
||||
/*NS_ERROR_NET_INTERRUPT*/ generateNetFailure(71),
|
||||
"The connection was established, but the data transfer was interrupted.",
|
||||
/*NS_ERROR_NOT_RESUMABLE*/ generateNetFailure(25),
|
||||
"This request is not resumable, but it was tried to resume it, or to request resume-specific data.",
|
||||
/*NS_ERROR_ENTITY_CHANGED*/ generateNetFailure(32),
|
||||
"It was attempted to resume the request, but the entity has changed in the meantime.",
|
||||
/*NS_ERROR_REDIRECT_LOOP*/ generateNetFailure(31),
|
||||
"The request failed as a result of a detected redirection loop.",
|
||||
/*NS_ERROR_UNKNOWN_HOST*/ generateNetFailure(30),
|
||||
"The lookup of a hostname failed. This generally refers to the hostname from the URL being loaded.",
|
||||
/*NS_ERROR_UNKNOWN_PROXY_HOST*/ generateNetFailure(42),
|
||||
"The lookup of a proxy hostname failed.",
|
||||
/*NS_ERROR_UNKNOWN_SOCKET_TYPE*/ generateNetFailure(51),
|
||||
"The specified socket type does not exist.",
|
||||
/*NS_ERROR_SOCKET_CREATE_FAILED*/ generateNetFailure(52),
|
||||
"The specified socket type could not be created."
|
||||
];
|
||||
|
||||
function netErrorToString( rc )
|
||||
{
|
||||
if (getErrorModule(rc) == NS_ERROR_MODULE_NETWORK) {
|
||||
var i = 0;
|
||||
while (i < g_nsNetErrorCodes.length) {
|
||||
// rc is kept unsigned, our generated code signed,
|
||||
// so == won't work here:
|
||||
if ((g_nsNetErrorCodes[i] ^ rc) == 0)
|
||||
return g_nsNetErrorCodes[i + 1];
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
throw Components.results.NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// WCAP error handling helpers
|
||||
@ -181,34 +261,6 @@ function wcapErrorToString( rc )
|
||||
throw Components.results.NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
function errorToString( err )
|
||||
{
|
||||
if (typeof(err) == "string")
|
||||
return err;
|
||||
if (err instanceof Error)
|
||||
return err.message;
|
||||
switch (err) {
|
||||
case NS_ERROR_OFFLINE:
|
||||
return "NS_ERROR_OFFLINE";
|
||||
// xxx todo: there may be a more comprehensive API for these:
|
||||
case Components.results.NS_ERROR_INVALID_ARG:
|
||||
return "NS_ERROR_INVALID_ARG";
|
||||
case Components.results.NS_ERROR_NO_INTERFACE:
|
||||
return "NS_ERROR_NO_INTERFACE";
|
||||
case Components.results.NS_ERROR_NOT_IMPLEMENTED:
|
||||
return "NS_ERROR_NOT_IMPLEMENTED";
|
||||
case Components.results.NS_ERROR_FAILURE:
|
||||
return "NS_ERROR_FAILURE";
|
||||
default: // probe for WCAP error:
|
||||
try {
|
||||
return wcapErrorToString(err);
|
||||
}
|
||||
catch (exc) {
|
||||
return ("[" + err + "] Unknown error.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWcapErrorCode( errno )
|
||||
{
|
||||
var index = -1;
|
||||
@ -233,7 +285,9 @@ function getWcapXmlErrno( xml )
|
||||
return parseInt(elem.textContent);
|
||||
}
|
||||
// some commands just respond with an empty calendar, no errno. WTF.
|
||||
throw Components.interfaces.calIWcapErrors.WCAP_NO_ERRNO;
|
||||
throw new Components.Exception(
|
||||
"No WCAP errno (missing X-NSCP-WCAP-ERRNO).",
|
||||
Components.interfaces.calIWcapErrors.WCAP_NO_ERRNO );
|
||||
}
|
||||
|
||||
function getWcapIcalErrno( icalRootComp )
|
||||
@ -242,15 +296,26 @@ function getWcapIcalErrno( icalRootComp )
|
||||
if (prop)
|
||||
return parseInt(prop.value);
|
||||
// some commands just respond with an empty calendar, no errno. WTF.
|
||||
throw Components.interfaces.calIWcapErrors.WCAP_NO_ERRNO;
|
||||
throw new Components.Exception(
|
||||
"No WCAP errno (missing X-NSCP-WCAP-ERRNO).",
|
||||
Components.interfaces.calIWcapErrors.WCAP_NO_ERRNO );
|
||||
}
|
||||
|
||||
function checkWcapErrno( errno, expectedErrno )
|
||||
{
|
||||
if (expectedErrno == undefined)
|
||||
expectedErrno = 0; // i.e. Command successful.
|
||||
if (errno != expectedErrno)
|
||||
throw getWcapErrorCode(errno);
|
||||
if (errno != expectedErrno) {
|
||||
var rc;
|
||||
try {
|
||||
rc = getWcapErrorCode(errno);
|
||||
}
|
||||
catch (exc) {
|
||||
throw new Components.Exception(
|
||||
"No WCAP error no.", Components.results.NS_ERROR_INVALID_ARG );
|
||||
}
|
||||
throw new Components.Exception( wcapErrorToString(rc), rc );
|
||||
}
|
||||
}
|
||||
|
||||
function checkWcapXmlErrno( xml, expectedErrno )
|
||||
@ -263,3 +328,34 @@ function checkWcapIcalErrno( icalRootComp, expectedErrno )
|
||||
checkWcapErrno( getWcapIcalErrno(icalRootComp), expectedErrno );
|
||||
}
|
||||
|
||||
|
||||
function errorToString( err )
|
||||
{
|
||||
if (typeof(err) == "string")
|
||||
return err;
|
||||
if (err instanceof Error)
|
||||
return err.message;
|
||||
switch (err) {
|
||||
case Components.results.NS_ERROR_INVALID_ARG:
|
||||
return "NS_ERROR_INVALID_ARG";
|
||||
case Components.results.NS_ERROR_NO_INTERFACE:
|
||||
return "NS_ERROR_NO_INTERFACE";
|
||||
case Components.results.NS_ERROR_NOT_IMPLEMENTED:
|
||||
return "NS_ERROR_NOT_IMPLEMENTED";
|
||||
case Components.results.NS_ERROR_FAILURE:
|
||||
return "NS_ERROR_FAILURE";
|
||||
default: // probe for WCAP error:
|
||||
try {
|
||||
return wcapErrorToString(err);
|
||||
}
|
||||
catch (exc) { // probe for netwerk error:
|
||||
try {
|
||||
return netErrorToString(err);
|
||||
}
|
||||
catch (exc) {
|
||||
return ("[" + err + "] Unknown error.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -78,8 +78,11 @@ WcapResponse.prototype = {
|
||||
|
||||
function stringToIcal( data )
|
||||
{
|
||||
if (!data || data == "") // assuming time-out
|
||||
throw Components.interfaces.calIWcapErrors.WCAP_LOGIN_FAILED;
|
||||
if (!data || data == "") { // assuming time-out
|
||||
throw new Components.Exception(
|
||||
"Login failed. Invalid session ID.",
|
||||
Components.interfaces.calIWcapErrors.WCAP_LOGIN_FAILED );
|
||||
}
|
||||
var icalRootComp = getIcsService().parseICS( data );
|
||||
checkWcapIcalErrno( icalRootComp );
|
||||
return icalRootComp;
|
||||
@ -87,8 +90,11 @@ function stringToIcal( data )
|
||||
|
||||
function stringToXml( data )
|
||||
{
|
||||
if (!data || data == "") // assuming time-out
|
||||
throw Components.interfaces.calIWcapErrors.WCAP_LOGIN_FAILED;
|
||||
if (!data || data == "") { // assuming time-out
|
||||
throw new Components.Exception(
|
||||
"Login failed. Invalid session ID.",
|
||||
Components.interfaces.calIWcapErrors.WCAP_LOGIN_FAILED );
|
||||
}
|
||||
var xml = getDomParser().parseFromString( data, "text/xml" );
|
||||
checkWcapXmlErrno( xml );
|
||||
return xml;
|
||||
@ -185,7 +191,21 @@ function issueSyncRequest( url, receiverFunc, bLogging )
|
||||
var channel = getIoService().newChannel(
|
||||
url, "" /* charset */, null /* baseURI */ );
|
||||
channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
|
||||
|
||||
var timer = Components.classes["@mozilla.org/timer;1"]
|
||||
.createInstance(Components.interfaces.nsITimer);
|
||||
timer.initWithCallback(
|
||||
{ // nsITimerCallback:
|
||||
notify: function( timer_ ) {
|
||||
if (channel.isPending())
|
||||
channel.cancel(NS_BINDING_FAILED);
|
||||
}
|
||||
},
|
||||
SYNC_REQUESTS_TIMEOUT * 1000,
|
||||
Components.interfaces.nsITimer.TYPE_ONE_SHOT );
|
||||
var stream = channel.open();
|
||||
timer.cancel();
|
||||
|
||||
var status = channel.status;
|
||||
if (status == Components.results.NS_OK) {
|
||||
var charset = channel.contentCharset;
|
||||
@ -205,7 +225,11 @@ function issueSyncRequest( url, receiverFunc, bLogging )
|
||||
logMessage( "issueSyncRequest( \"" + url + "\" )",
|
||||
"failed: " + status );
|
||||
}
|
||||
throw status;
|
||||
|
||||
throw new Components.Exception(
|
||||
bLogging ? ("issueSyncRequest( \"" + url + "\" ) failed.")
|
||||
: "issueSyncRequest() failed.",
|
||||
status );
|
||||
}
|
||||
|
||||
function issueSyncXMLRequest( url, receiverFunc, bLogging )
|
||||
|
||||
@ -134,17 +134,19 @@ calWcapSession.prototype = {
|
||||
logError:
|
||||
function( err, context )
|
||||
{
|
||||
var str = ("error: " + errorToString(err));
|
||||
Components.utils.reportError( this.log( str, context ) );
|
||||
return str;
|
||||
var msg = errorToString(err);
|
||||
Components.utils.reportError( this.log("error: " + msg, context) );
|
||||
return msg;
|
||||
},
|
||||
notifyError:
|
||||
function( err )
|
||||
{
|
||||
debugger;
|
||||
var str = this.logError( err );
|
||||
this.notifyObservers( "onError",
|
||||
[err instanceof Error ? -1 : err, str] );
|
||||
var msg = this.logError(err);
|
||||
this.notifyObservers(
|
||||
"onError",
|
||||
err instanceof Components.interfaces.nsIException
|
||||
? [err.result, err.message] : [-1, msg] );
|
||||
},
|
||||
|
||||
m_observers: null,
|
||||
@ -211,7 +213,7 @@ calWcapSession.prototype = {
|
||||
var str = issueSyncRequest( url );
|
||||
var icalRootComp = getIcsService().parseICS( str );
|
||||
if (icalRootComp == null)
|
||||
throw new Error("invalid data, expected ical!");
|
||||
throw new Components.Exception("invalid data, expected ical!");
|
||||
checkWcapIcalErrno( icalRootComp );
|
||||
var tzids = [];
|
||||
var this_ = this;
|
||||
@ -258,7 +260,7 @@ calWcapSession.prototype = {
|
||||
var str = issueSyncRequest( url );
|
||||
var icalRootComp = getIcsService().parseICS( str );
|
||||
if (icalRootComp == null)
|
||||
throw new Error("invalid data, expected ical!");
|
||||
throw new Components.Exception("invalid data, expected ical!");
|
||||
checkWcapIcalErrno( icalRootComp );
|
||||
var serverTime = getDatetimeFromIcalProp(
|
||||
icalRootComp.getFirstProperty( "X-NSCP-WCAPTIME" ) );
|
||||
@ -298,30 +300,35 @@ calWcapSession.prototype = {
|
||||
{
|
||||
if (this.m_bNoLoginsAnymore) {
|
||||
this.log( "login has failed, no logins anymore for this user." );
|
||||
throw Components.interfaces.calIWcapErrors.WCAP_LOGIN_FAILED;
|
||||
throw new Components.Exception(
|
||||
"Login failed. Invalid session ID.",
|
||||
Components.interfaces.calIWcapErrors.WCAP_LOGIN_FAILED );
|
||||
}
|
||||
if (getIoService().offline) {
|
||||
this.log( "in offline mode." );
|
||||
throw NS_ERROR_OFFLINE;
|
||||
throw new Components.Exception(
|
||||
"The requested action could not be completed while the " +
|
||||
"networking library is in the offline state.",
|
||||
NS_ERROR_OFFLINE );
|
||||
}
|
||||
|
||||
if (this.m_sessionId == null || this.m_sessionId == timedOutSessionId) {
|
||||
|
||||
var this_ = this;
|
||||
syncExec(
|
||||
lockedExec(
|
||||
function() {
|
||||
if (this_.m_sessionId == null ||
|
||||
this_.m_sessionId == timedOutSessionId)
|
||||
{
|
||||
if (!this_.m_bNoLoginsAnymore &&
|
||||
(this_.m_sessionId == null ||
|
||||
this_.m_sessionId == timedOutSessionId)) {
|
||||
if (timedOutSessionId != null) {
|
||||
this_.m_sessionId = null;
|
||||
this_.log(
|
||||
"session timeout; prompting to reconnect." );
|
||||
this_.log( "session timeout; " +
|
||||
"prompting to reconnect." );
|
||||
var prompt =getWindowWatcher().getNewPrompter(null);
|
||||
var bundle = getWcapBundle();
|
||||
if (!prompt.confirm(
|
||||
bundle.GetStringFromName(
|
||||
"reconnectConfirmation.label" ),
|
||||
"reconnectConfirmation.label"),
|
||||
bundle.formatStringFromName(
|
||||
"reconnectConfirmation.text",
|
||||
[this_.uri.hostPort], 1 ) )) {
|
||||
@ -343,8 +350,11 @@ calWcapSession.prototype = {
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
if (this.m_sessionId == null) {
|
||||
throw Components.interfaces.calIWcapErrors.WCAP_LOGIN_FAILED;
|
||||
throw new Components.Exception(
|
||||
"Login failed. Invalid session ID.",
|
||||
Components.interfaces.calIWcapErrors.WCAP_LOGIN_FAILED );
|
||||
}
|
||||
return this.m_sessionId;
|
||||
},
|
||||
@ -381,7 +391,7 @@ calWcapSession.prototype = {
|
||||
var loginUri = this.sessionUri.clone();
|
||||
if (loginUri.scheme.toLowerCase() != "https") {
|
||||
if (loginUri.port == -1) {
|
||||
// no https, but no port specified
|
||||
// no https and no port specified
|
||||
// => enforce login via https:
|
||||
loginUri.scheme = "https";
|
||||
}
|
||||
@ -421,7 +431,7 @@ calWcapSession.prototype = {
|
||||
loginText = this.getServerInfo( loginUri );
|
||||
}
|
||||
if (loginText == null) {
|
||||
throw new Error(
|
||||
throw new Components.Exception(
|
||||
getWcapBundle().formatStringFromName(
|
||||
"accessingServerFailedError.text",
|
||||
[loginUri.hostPort], 1 ) );
|
||||
@ -429,7 +439,7 @@ calWcapSession.prototype = {
|
||||
if (this.sessionUri.scheme.toLowerCase() == "https") {
|
||||
// user specified https, so http is no option:
|
||||
loginText = null;
|
||||
throw new Error(
|
||||
throw new Components.Exception(
|
||||
getWcapBundle().formatStringFromName(
|
||||
"mandatoryHttpsError.text",
|
||||
[loginUri.hostPort], 1 ) );
|
||||
@ -477,6 +487,7 @@ calWcapSession.prototype = {
|
||||
|
||||
var savePW = { value: false };
|
||||
while (this.m_sessionId == null) {
|
||||
this.log( "prompting for user/pw..." );
|
||||
var prompt = getWindowWatcher().getNewPrompter(null);
|
||||
if (prompt.promptUsernameAndPassword(
|
||||
getWcapBundle().GetStringFromName(
|
||||
@ -526,7 +537,7 @@ calWcapSession.prototype = {
|
||||
checkWcapIcalErrno( icalRootComp );
|
||||
var prop = icalRootComp.getFirstProperty( "X-NSCP-WCAP-SESSION-ID" );
|
||||
if (prop == null)
|
||||
throw new Error("missing X-NSCP-WCAP-SESSION-ID!");
|
||||
throw new Components.Exception("missing X-NSCP-WCAP-SESSION-ID!");
|
||||
this.m_sessionId = prop.value;
|
||||
|
||||
// var xml = issueSyncXMLRequest(
|
||||
@ -552,7 +563,7 @@ calWcapSession.prototype = {
|
||||
return null; // no ical data returned
|
||||
icalRootComp = getIcsService().parseICS( str );
|
||||
if (icalRootComp == null)
|
||||
throw new Error("invalid ical data!");
|
||||
throw new Components.Exception("invalid ical data!");
|
||||
}
|
||||
catch (exc) { // soft error; request denied etc.
|
||||
this.log( "server version request failed: " + errorToString(exc) );
|
||||
@ -566,7 +577,7 @@ calWcapSession.prototype = {
|
||||
loginTextVars.push( prop ? prop.value : "<unknown>" );
|
||||
prop = icalRootComp.getFirstProperty( "X-NSCP-WCAPVERSION" );
|
||||
if (prop == null)
|
||||
throw new Error("missing X-NSCP-WCAPVERSION!");
|
||||
throw new Components.Exception("missing X-NSCP-WCAPVERSION!");
|
||||
loginTextVars.push( prop.value );
|
||||
var wcapVersion = parseInt(prop.value);
|
||||
if (wcapVersion < 3) {
|
||||
@ -578,7 +589,7 @@ calWcapSession.prototype = {
|
||||
bundle.formatStringFromName(
|
||||
"insufficientWcapVersionConfirmation.text",
|
||||
loginTextVars, loginTextVars.length ) )) {
|
||||
throw new Error(labelText);
|
||||
throw new Components.Exception(labelText);
|
||||
}
|
||||
}
|
||||
return getWcapBundle().formatStringFromName(
|
||||
@ -589,7 +600,7 @@ calWcapSession.prototype = {
|
||||
function( wcapCommand )
|
||||
{
|
||||
if (this.sessionUri == null)
|
||||
throw new Error("no URI!");
|
||||
throw new Components.Exception("no URI!");
|
||||
// ensure established session, so userId is set;
|
||||
// (calId defaults to userId) if not set:
|
||||
this.getSessionId();
|
||||
@ -613,8 +624,10 @@ calWcapSession.prototype = {
|
||||
data, wcapResponse );
|
||||
}
|
||||
catch (exc) {
|
||||
if (exc == Components.interfaces.
|
||||
calIWcapErrors.WCAP_LOGIN_FAILED) /* timeout */ {
|
||||
if (testResultCode(
|
||||
exc, Components.interfaces.
|
||||
calIWcapErrors.WCAP_LOGIN_FAILED)) /* timeout */
|
||||
{
|
||||
// getting a new session will throw any exception in
|
||||
// this block, thus it is notified into receiverFunc
|
||||
this_.getSessionId(
|
||||
@ -931,7 +944,7 @@ calWcapSession.prototype = {
|
||||
}
|
||||
catch (exc) {
|
||||
const calIWcapErrors = Components.interfaces.calIWcapErrors;
|
||||
switch (exc) {
|
||||
switch (getResultCode(exc)) {
|
||||
case calIWcapErrors.WCAP_NO_ERRNO: // workaround
|
||||
case calIWcapErrors.WCAP_ACCESS_DENIED_TO_CALENDAR:
|
||||
case calIWcapErrors.WCAP_CALENDAR_DOES_NOT_EXIST:
|
||||
|
||||
@ -131,6 +131,57 @@ function getCalendarManager()
|
||||
return g_calendarManager;
|
||||
};
|
||||
|
||||
var g_bInitedLockedExec = false;
|
||||
var g_eventQueueService = null;
|
||||
var g_threadManager = null;
|
||||
|
||||
/** Locks event dispatching for the current event queue while the passed
|
||||
function is executed.
|
||||
|
||||
xxx todo: this hinders timeeout events for sync requests
|
||||
to be dispatched!
|
||||
*/
|
||||
function lockedExec( func )
|
||||
{
|
||||
if (!g_bInitedLockedExec) {
|
||||
try {
|
||||
var cl = Components.classes["@mozilla.org/event-queue-service;1"];
|
||||
if (cl) {
|
||||
g_eventQueueService =
|
||||
cl.getService(Components.interfaces.nsIEventQueueService);
|
||||
}
|
||||
}
|
||||
catch (exc) { // eventQueue has vanished on trunk
|
||||
}
|
||||
if (!g_eventQueueService) {
|
||||
g_threadManager =
|
||||
Components.classes["@mozilla.org/thread-manager;1"]
|
||||
.getService(Components.interfaces.nsIThreadManager);
|
||||
}
|
||||
g_bInitedLockedExec = true;
|
||||
}
|
||||
|
||||
var queue;
|
||||
if (g_eventQueueService)
|
||||
queue = g_eventQueueService.pushThreadEventQueue();
|
||||
else // we are on trunk using nsIThreadInternal
|
||||
g_threadManager.currentThread.pushEventQueue(null);
|
||||
try {
|
||||
return func();
|
||||
}
|
||||
catch (exc) {
|
||||
if (g_eventQueueService)
|
||||
g_eventQueueService.popThreadEventQueue(queue);
|
||||
else // we are on trunk using nsIThreadInternal
|
||||
g_threadManager.currentThread.popEventQueue();
|
||||
throw exc;
|
||||
}
|
||||
if (g_eventQueueService)
|
||||
g_eventQueueService.popThreadEventQueue(queue);
|
||||
else // we are on trunk using nsIThreadInternal
|
||||
g_threadManager.currentThread.popEventQueue();
|
||||
}
|
||||
|
||||
var g_wcapBundle = null;
|
||||
function getWcapBundle()
|
||||
{
|
||||
@ -144,6 +195,17 @@ function getWcapBundle()
|
||||
return g_wcapBundle;
|
||||
}
|
||||
|
||||
function getResultCode( exc )
|
||||
{
|
||||
return (exc instanceof Components.interfaces.nsIException
|
||||
? exc.result : exc);
|
||||
}
|
||||
|
||||
function testResultCode( exc, rc )
|
||||
{
|
||||
return (getResultCode(exc) == rc);
|
||||
}
|
||||
|
||||
function isEvent( item )
|
||||
{
|
||||
return (item instanceof Components.interfaces.calIEvent);
|
||||
@ -151,11 +213,13 @@ function isEvent( item )
|
||||
|
||||
function isParent( item )
|
||||
{
|
||||
if (item.id != item.parentItem.id)
|
||||
throw new Error("proxy has different id than its parent!");
|
||||
if (item.id != item.parentItem.id) {
|
||||
throw new Components.Exception(
|
||||
"proxy has different id than its parent!");
|
||||
}
|
||||
if (item.parentItem.recurrenceId) {
|
||||
throw new Error("parent has recurrenceId: " +
|
||||
item.parentItem.recurrenceId);
|
||||
throw new Components.Exception("parent has recurrenceId: " +
|
||||
item.parentItem.recurrenceId);
|
||||
}
|
||||
return (!item.recurrenceId);
|
||||
}
|
||||
@ -260,91 +324,8 @@ function setPref(prefName, value)
|
||||
prefBranch.setCharPref(prefName, value);
|
||||
break;
|
||||
default:
|
||||
throw new Error("unsupported pref value: " + typeof(value));
|
||||
throw new Components.Exception("unsupported pref value: " +
|
||||
typeof(value));
|
||||
}
|
||||
}
|
||||
|
||||
function syncExec( func )
|
||||
{
|
||||
// xxx todo: how to do better?
|
||||
// possible HACK here, because of lack of sync possibilities:
|
||||
// when we run into executing dialogs, the js runtime
|
||||
// concurrently executes (another getItems() request).
|
||||
// That concurrent request needs to wait for the first login
|
||||
// attempt to finish.
|
||||
// Creating a thread event queue somehow hinders the js engine
|
||||
// from scheduling another js execution.
|
||||
var eventQueueService = null;
|
||||
try {
|
||||
eventQueueService =
|
||||
Components.classes["@mozilla.org/event-queue-service;1"]
|
||||
.getService(Components.interfaces.nsIEventQueueService);
|
||||
}
|
||||
catch (exc) {
|
||||
}
|
||||
if (eventQueueService != null) {
|
||||
var eventQueue = eventQueueService.pushThreadEventQueue();
|
||||
try {
|
||||
func();
|
||||
}
|
||||
catch (exc) {
|
||||
eventQueueService.popThreadEventQueue( eventQueue );
|
||||
throw exc;
|
||||
}
|
||||
eventQueueService.popThreadEventQueue( eventQueue );
|
||||
}
|
||||
else // xxx todo: eventQueue has vanished on TRUNK
|
||||
func();
|
||||
}
|
||||
|
||||
// // xxx todo: the below code still does not sync properly...
|
||||
// function syncExec( func )
|
||||
// {
|
||||
// // sync all execution for login to UI thread, using nsIRunnable:
|
||||
// // change from MOZILLA_1_8_BRANCH->TRUNK: probe xxx todo: test
|
||||
// var target = null; // eventQueue or eventTarget
|
||||
// try {
|
||||
// var eventQueueService =
|
||||
// Components.classes["@mozilla.org/event-queue-service;1"]
|
||||
// .getService(Components.interfaces.nsIEventQueueService);
|
||||
// if (eventQueueService != null) {
|
||||
// target = eventQueueService.getSpecialEventQueue(
|
||||
// Components.interfaces.
|
||||
// nsIEventQueueService.UI_THREAD_EVENT_QUEUE );
|
||||
// }
|
||||
// }
|
||||
// catch (exc) {
|
||||
// // eventQueue has vanished on TRUNK
|
||||
// }
|
||||
// if (target == null) {
|
||||
// // we are on the TRUNK:
|
||||
// var threadManager = Components.classes["@mozilla.org/thread-manager;1"]
|
||||
// .getService(Components.interfaces.nsIThreadManager);
|
||||
// target = threadManager.mainThread;
|
||||
// }
|
||||
|
||||
// var proxyObjectManager =
|
||||
// Components.classes["@mozilla.org/xpcomproxy;1"]
|
||||
// .getService(Components.interfaces.nsIProxyObjectManager);
|
||||
// var proxy = proxyObjectManager.getProxyForObject(
|
||||
// target, Components.interfaces.nsIRunnable,
|
||||
// { // need to implemented QueryInterface, because object param
|
||||
// // is not associated with iid:
|
||||
// QueryInterface:
|
||||
// function( iid ) {
|
||||
// if (Components.interfaces.nsIRunnable.equals(iid) ||
|
||||
// Components.interfaces.nsISupports.equals(iid))
|
||||
// return this;
|
||||
// throw Components.results.NS_ERROR_NO_INTERFACE;
|
||||
// },
|
||||
// // nsIRunnable:
|
||||
// run:
|
||||
// function() {
|
||||
// func();
|
||||
// }
|
||||
// },
|
||||
// Components.interfaces.nsIProxyObjectManager.INVOKE_SYNC );
|
||||
// // xxx todo: are rc/exceptions forwarded to current thread?
|
||||
// proxy.run();
|
||||
// }
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user