/* Copyright (c) 2006-2007, Apple Inc. All rights reserved. */
/**
 * Class.createWithSharedInstance(): use this method instead of Prototype's Class.create()
 * to support a global shared instance of the class.
 */

Class.createWithSharedInstance = function(inOptInstanceShortcutName) {
	var cls = null;
	cls = Class.create();
	cls.sharedInstance = function() {
		if (!cls['_sharedInstance']) cls['_sharedInstance'] = new cls();
		return cls['_sharedInstance'];
	}
	if (inOptInstanceShortcutName) window[inOptInstanceShortcutName] = cls.sharedInstance;
	return cls;
}

/**
 * NotificationPublisher: used to distribute notifications (logged in, invalid session, server error, etc).
 */

var NotificationPublisher = Class.createWithSharedInstance('publisher');
NotificationPublisher.prototype = {
	initialize: function() {
		this.mSubcribers = [];
	},
	publish: function(inMessage, inObject, inUserInfo) {
		// for each subscriber...
		return this.mSubcribers.collect(function(subscriber) {
			// get the subscriber's effective message and object (if missing, always match)
			var m = subscriber.message || inMessage;
			var o = subscriber.obj || inObject;
			// if we have a match...
			if (m == inMessage && o == inObject) {
				// run the callback
				subscriber.callback(inMessage, inObject, inUserInfo);
				return true;
			}
			return false;
		}).length > 0; // return true if something responded, false otherwise
	},
	subscribe: function(inCallback, inMessage, inObject) {
		this.mSubcribers.push({callback:inCallback, message:inMessage, obj:inObject});
	},
	unsubscribe: function(inCallback, inMessage, inObject) {
		this.mSubcribers = this.mSubcribers.reject(function(subscriber) {
			return (subscriber.callback == inCallback && subscriber.message == inMessage && subscriber.obj == inObject);
		});
	}
}

/**
 * AppleCollaborationServer: Proxy object to Apple's collaboration server.
 * Optionally, initialize with an RPC URL. Uses /RPC2 by default.
 * Remember that Web browsers restrict requests to the same host as this JavaScript file.
 */

var AppleCollaborationServer = Class.createWithSharedInstance('server');
AppleCollaborationServer.prototype = {
	initialize: function(inOptionalURL) {
		this.mPlainTextLogin = getMetaTagValue('apple_use_plaintext_login');
		this.url = fixSiteRelativeURLForIE(inOptionalURL || '/RPC2');
		this.getSessionIDFromCookie();
		this.setTimezoneCookie();
		// set up the services
		Object.extend(this, {
			wiki: new CollaborationService(this, 'wiki', ['getSidebarEntries', 'filterText']),
			weblog: new CollaborationService(this, 'weblog'),
			calendar: new CollaborationService(this, 'calendar'),
			mailinglist: new CollaborationService(this, 'mailinglist'),
			versions: new CollaborationService(this, 'versions'),
			search: new CollaborationService(this, 'search'),
			settings: new CollaborationService(this, 'settings', ['getSettings', 'setSettings', 'manageTags']),
			tags: new CollaborationService(this, 'tags'),
			preferences: new CollaborationService(this, 'preferences', ['getUsersRelevantTags', 'getUsersRecentPages']),
			whitepages: new CollaborationService(this, 'whitepages')
		});
	},
	login: function(inCallback, inUsername, inPassword, inOptAuthorizePath) {
		var checkSuccess = function(q, r) {
			if (r.success) {
				// store the session ID
				this.sessionID = r.session_id;
				document.cookie = 'sessionID='+this.sessionID+'; path=/';
				// authenticated callbacks and notifications
				if (inCallback) inCallback();
				publisher().publish('AUTHENTICATED', this);
				// did we pre-authorize?
				if (r.accessLevel && r.accessLevel != 'none') {
					publisher().publish('AUTHORIZED', this, {action:r.accessLevel, path:inOptAuthorizePath});
				}
			}
			else {
				this.sessionID = 'unauthenticated';
				document.cookie = 'sessionID=unauthenticated; path=/';
				var caught = publisher().publish('AUTHENTICATION_FAILED', this, inUsername);
				if (!caught) alert('Uncaught authentication failure for user: ' + inUsername);
			}
		}
		var performLoginRequest = function() {
			var method = this.mPlainTextLogin ? 'login' : 'digestLogin';
			var args = this.mPlainTextLogin ? [inUsername, inPassword] : [digestResponse(inUsername, inPassword, this.mCachedChallenge)];
			this.mCachedChallenge = null;
			if (inOptAuthorizePath) args.push(inOptAuthorizePath);
			var req = new XMLRPCServiceRequest(this, method, checkSuccess.bind(this), args);
		}.bind(this);
		if (this.mCachedChallenge) {
			performLoginRequest();
		}
		else {
			this.cacheDigestChallenge(performLoginRequest);
		}
	},
	checkSessionAuthorization: function(inCallback, inOptAction, inOptPath) {
		var action = inOptAction || 'write';
		var authorizedCallback = function() {
			// send an authorized message
			var userInfo = {};
			if (inOptAction) userInfo['action'] = inOptAction;
			if (inOptPath) userInfo['path'] = inOptPath;
			publisher().publish('AUTHORIZED', this, userInfo);
			// run callback function
			if (inCallback && inCallback.constructor == Array) inCallback[0]();
			else if (inCallback) inCallback();
		}
		var callback = authorizedCallback;
		if (inCallback && inCallback.constructor == Array) {
			callback = [authorizedCallback, inCallback[1]];
		}
		if (inOptPath) return new XMLRPCRequest(this, 'checkSessionAuthorization', callback, action, inOptPath);
		return new XMLRPCRequest(this, 'checkSessionAuthorization', callback, action);
	},
	logout: function(inCallback) {
		var logoutCallback = function() {
			this.sessionID = 'unauthenticated';
			document.cookie = 'sessionID=unauthenticated; path=/';
			if (inCallback) inCallback();
			publisher().publish('LOGGED_OUT', this, {reload:true}); // reload
		}
		return new XMLRPCRequest(this, 'logout', logoutCallback.bind(this));
	},
	getUploadProgress: function(inCallback, inUploadID) {
		return new XMLRPCRequest(this, 'getUploadProgress', inCallback, inUploadID);
	},
	adminKickServer: function(inCallback) {
		return new XMLRPCRequest(this, 'adminKickServer', inCallback);
	},
	groupsForSession: function(inCallback) {
		return new XMLRPCRequest(this, 'groupsForSession', inCallback);
	},
	/* Usually called indirectly through login(). 
	   Call this manually to make challenge request asynchronously for better UI responsiveness. */
	cacheDigestChallenge: function(inCallback) {
		if (this.mPlainTextLogin) {
			if (inCallback) inCallback();
			return true;
		}
		var callback = function(q, r) {
			if (r.success) { // ##5389561
				alert('Got a session id already: '+ r.session_id);
			}
			else {
				this.mCachedChallenge = r.challenge;
				if (inCallback) inCallback();
			}
		}
		var req = new XMLRPCRequest(this, 'digestLogin', callback.bind(this));
	},
	/* Local functions; these don't necessarily touch the server and return immediately. */
	isSavingSomething: function() {
		return XMLRPCRequest.allRequests && XMLRPCRequest.allRequests.detect(function(r) {return r.required && (!r.responseObj)});
	},
	getNextUploadID: function() { // Used for file uploads to "uniquify" them
		var uploadID = Math.floor(Math.random() * 1000000);
		document.cookie = 'uploadID='+uploadID+'; path=/';
		return uploadID;
	},
	getSessionIDFromCookie: function() { // Usually just used on initialize
		var results = document.cookie.match('sessionID=([^;]+)');
		if (results) {
			if ((this.sessionID == 'unauthenticated') && (results[1] != 'unauthenticated') && window.gNotifier) gNotifier.print('loggedIn');
			this.sessionID = results[1];
		}
		else {
			this.sessionID = 'unauthenticated';
			document.cookie = 'sessionID=unauthenticated; path=/';
		}
	},
	setTimezoneCookie: function() { // Usually just used on initialize
		var dt = new Date();
		var offset = dt.getTimezoneOffset() / (-60);
		dt.setFullYear(dt.getFullYear()+2);
		document.cookie = 'utcOffset='+offset+'; expires='+dt.toGMTString()+'; path=/';
	}
}

/**
 * CollaborationServiceBase: Proxy object for a service on the server.
 * Automatically proxies standard service methods. Initialize with parent server object and service name.
 * Additionally, initialize with additional method names handled by the service.
 */

var CollaborationService = Class.create();
CollaborationService.prototype = {
	mBaseMethods: [
		'getEntries', 'getEntryWithUID', 'addEntry', 'updateEntry', 'deleteEntry',
		'permanentlyDeleteEntry', 'undeleteEntry', 'addTagToEntry', 'removeTagFromEntry',
		'removeVersionFromEntry', 'OLDgetEntries'
	],
	initialize: function(inParent, inServiceName, inOptMethods) {
		this.mParent = inParent;
		this.mServiceName = inServiceName;
		$A(this.mBaseMethods).concat(inOptMethods||[]).each(function(methodName) {
			this[methodName] = function() {
				var args = $A(arguments);
				var cb = args.splice(0, 1)[0];
				return new XMLRPCServiceRequest(this.mParent, this.mServiceName+'.'+methodName, cb, args);
			}.bind(this);
		}.bind(this));
	}
}

/**
 * CollabUID: Data model class for collaboration UIDs. Use this to split UIDs into parts.
 * If not initialized with a UID string, it will initialize with the value of the page's "apple_collab_uid" meta tag.
 */

var CollabUID = Class.createWithSharedInstance('uid');
CollabUID.prototype = {
	mMetaTagName: 'apple_collab_uid',
	initialize: function(inOptValue) {
		this.setValue(inOptValue || getMetaTagValue(this.mMetaTagName) || '');
	},
	setValue: function(inValue) {
		var splitValue = inValue.split('/');
		while (splitValue.length < 4) splitValue.push('');
		this.mValue = inValue; // groups/testgroup/wiki/pagename
		this.mOwnerType = splitValue[0]; // groups
		this.mOwnerName = splitValue[1]; // testgroup
		this.mService = splitValue[2]; // wiki
		this.mItemName = splitValue[3]; // pagename
		this.mBasePath = this.mOwnerType + '/' + this.mOwnerName; // groups/testgroup
		this.mBaseLocation = '/' + this.mBasePath + '/'; // for forming URLs
		this.mBaseLocation = this.mBaseLocation.replace('//','/'); // workaround for directory
		this.mParentPath = this.mBasePath + '/' + this.mService; // groups/testgroup/wiki
		this.mParentLocation = '/' + this.mParentPath + '/'; // for forming URLs
	},
	toString: function() {
		return this.mValue;
	}
}

/* **************  UTILITY FUNCTIONS  *************** */

function getMetaTagValue(inTagName) {
	var matchingTag = $A(d.getElementsByTagName('META')).detect(function(tag) {
		return (tag.name == inTagName);
	});
	return (matchingTag ? matchingTag.content : null);
}

function padNumberStr(theNumber, digits) {
	var padder = ((arguments.length > 2) ? arguments[2] : '0');
	var theString = "";
	theString += theNumber;
	
	for (var i = 0; i < (digits-theString.length); i++) {
		theString = padder + theString;
	}
	
	return theString;
}

function dateObjToISO8601(inDateObj, inOptMakeGMT) {
	var dt = new Date(inDateObj.getTime()); // copy the incoming date
	if (inOptMakeGMT) dt.setHours(dt.getHours()-(dt.getTimezoneOffset() / (-60)));
	var iso_string = '';
	iso_string += dt.getFullYear()
				+ padNumberStr(dt.getMonth()+1, 2)
				+ padNumberStr(dt.getDate(), 2)
				+ 'T'
				+ padNumberStr(dt.getHours(), 2)
				+ padNumberStr(dt.getMinutes(), 2)
				+ padNumberStr(dt.getSeconds(), 2)
				+ 'Z';
	return iso_string;
}

function fixSiteRelativeURLForIE(inURLString) {
	if (!document.all || inURLString.indexOf('/') != 0) return inURLString;
	return window.location.protocol + '//' + window.location.host + inURLString;
}


/* **************  DIGEST FUNCTIONS  *************** */

/*
 * Basic function for encoding UTF-8 as specified in RFC3629
 */

function _ucs4Ordinal(ch) {
	var ucs4Ordinal = "";
	ucs4Ordinal += String.fromCharCode(240 | (ch >> 18));
	ucs4Ordinal += String.fromCharCode(127 | ((ch >> 12) & 63));
	ucs4Ordinal += String.fromCharCode(127 | ((ch >> 6) & 63));
	ucs4Ordinal += String.fromCharCode(127 | (ch & 63));
	return ucs4Ordinal;
}

/*
 * Based on PyUnicode_EncodeUTF8
 */

function utf8Encode (string) {
	var utf8Text = "";

	for (var i = 0; i < string.length; i++) {
		var ch = string.charCodeAt(i);

		if (ch < 128) {
			// Characters less than 128 are ASCII
			utf8Text += String.fromCharCode(ch);
		}
		else if((ch > 127) && (ch < 2048)) {
			// Characters between 127 and 2048 are Latin-1
			utf8Text += String.fromCharCode(192 | (ch >> 6));
			utf8Text += String.fromCharCode(128 | (ch & 63));
		}
		else {
			// All other characters are UCS2 or UCS4 unicode ordinals
			if (ch < 65536) {
				// Special case check for high surrogates
				if ((55296 <= ch) && (ch <= 57343) && i != string.length) {
					var ch2 = string.charCodeAt(i + 1);
					// Check for low surrogate and combine for a UCS4 value
					if ((56320 <= ch2) && (ch2 < 57343)) {
						ch = ((ch - 55296) << 10 | (ch2 - 56320)) + 65536;
						i++;
						utf8Text += _ucs4Ordinal(ch);
					}
					// Fall through and handle isolated high surrogates
				}
				utf8Text += String.fromCharCode(224 | (ch >> 12));
				utf8Text += String.fromCharCode(128 | ((ch >> 6) & 63));
				utf8Text += String.fromCharCode(128 | (ch & 63));
				continue;
			}
			utf8Text += _ucs4Ordinal(ch);
		}

	}

	return utf8Text;
}

function digestResponse(username, password, challenge) {

	var HA1 = new Array(username,
						challenge.realm,
						password);

	HA1 = utf8Encode(HA1.join(':'));
	HA1 = hex_md5(HA1, HA1.length + chrsz);
	HA2 = "POST:/RPC2";
	HA2 = hex_md5(HA2, HA2.length + chrsz);

	var response = new Array(HA1,
							 challenge.nonce,
							 HA2);

	response = response.join(':');
	response = hex_md5(response, response.length + chrsz);

	var responseObj = {
		realm: challenge.realm,
		username: username,
		response: response,
		nonce: challenge.nonce,
		opaque: challenge.opaque,
		algorithm: challenge.algorithm,
		qop: challenge.qop,
		uri: '/RPC2',
		nc: '00000001'
	};

	return responseObj;
}


/* **************  BASE CLASSES  *************** */

/**
 * XMLRPCMessage: Data model class for a generic outgoing XML-RPC message.
 * Not to be used directly; create a XMLRPCRequest object instead.
 */

var XMLRPCMessage = Class.create();
XMLRPCMessage.prototype = {
	initialize: function(methodname) {
		this.method = methodname || 'system.listMethods';
		this.params = $A([]);
	},
	addParameter: function(data) {
		if (arguments.length == 0) return false;
		this.params.push(data);
	},
	xml: function() {
		return '<?xml version="1.0"?>\n<methodCall>\n<methodName>' + this.method + '</methodName>\n<params>\n' + this.params.collect(function(param) {
			var s = new XMLRPCSerializer(param);
			return '<param>\n<value>'+s.xml()+'</value>\n</param>\n';
		}).join('') + '</params>\n</methodCall>\n';
	}
}

/**
 * XMLRPCSerializer: This class serializes JavaScript to XML-RPC.
 * Not to be used directly; create a XMLRPCRequest object instead.
 */

var XMLRPCSerializer = Class.create();
XMLRPCSerializer.prototype = {
	initialize: function(inData) {
		this.mData = inData;
	},
	xml: function() {
		var type = (''+typeof(this.mData)).toLowerCase();
		if (this['_'+type]) return this['_'+type]();
		return ''; // this should never happen
	},
	_array: function() {
		return '<array><data>\n' + $A(this.mData).collect(function(cur) {
			var s = new XMLRPCSerializer(cur);
			return '<value>'+s.xml()+'</value>\n';
		}.bind(this)).join('') + '</data></array>\n';
	},
	_boolean: function() {
		return '<boolean>'+(this.mData?1:0)+'</boolean>';
	},
	_date: function() {
		return '<dateTime.iso8601>'+dateObjToISO8601(this.mData)+'</dateTime.iso8601>';
	},
	_function: function() {
		return '';
	},
	_number: function() {
		if (Math.round(this.mData) == this.mData) return '<i4>'+this.mData+'</i4>';
		return '<double>'+this.mData+'</double>';
	},
	_object: function() {
		var con = this.mData.constructor;
		if (con == Date) return this._date();
		if (con == Array) return this._array();
		return this._struct();
	},
	_string: function() {
		var strVal = this.mData.escapeHTML();
		/* skip newline and whitespace chars */
		try {
			var rx = new RegExp('[\\ca\\cb\\cc\\cd\\ce\\cf\\cg\\ch\\ck\\cl\\cn\\co\\cp\\cq\\cr\\cs\\ct\\cu\\cv\\cw\\cx\\cy\\cz\\c_]', 'g');
			if (IEFixes.isIE) rx = new RegExp('[\\ca\\cb\\cc\\cd\\ce\\cf\\cg\\ch\\ck\\cl\\cn\\co\\cp\\cq\\cr\\cs\\ct\\cu\\cv\\cw\\cx\\cy\\cz]', 'g'); // part of ##5350388 IE/Vista: image dialog javascript errors.
			if (!('a'.match(/[\ca]/))) strVal = strVal.replace(rx, '');
		}
		catch(e) {
		}
		return '<string>'+strVal+'</string>';
	},
	_struct: function() {
		return '<struct>\n'+$H(this.mData).collect(function(cur) {
			var s = new XMLRPCSerializer(cur.value);
			return '<member>\n<name>'+cur.key.escapeHTML()+'</name>\n<value>'+s.xml()+'</value>\n</member>\n';
		}.bind(this)).join('')+'</struct>\n';
	}
}

/**
 * XMLRPCRequest: Initiate this to make a raw request to the server.
 * Example: new XMLRPCRequest(AppleCollaborationServer.sharedInstance(), 'methodName', callbackFunction [, arg1, arg2, ...])
 * You should probably use the proxy methods in the AppleCollaborationServer object instead.
 * Proxy methods return a request object. Call makeRequired() on that request in order to mark the request as "required."
 */

var XMLRPCRequest = Class.create();
XMLRPCRequest.prototype = {
	initialize: function(inServerObj, inMethodNameStr, inCallback /*[, arg1, arg2, ...]*/) {
		this.setupRequest(inServerObj, inMethodNameStr, inCallback);
		for (var i = 3; i < arguments.length; i++) {
			if (arguments[i] != null) this.requestMessageObj.addParameter(arguments[i]);
		}
		this.sendRequest();
	},
	setupRequest: function(inServerObj, inMethodNameStr, inCallback) {
		if (inCallback.constructor == Array) {
			this.callbackFunction = inCallback[0];
			this.errorFunction = inCallback[1];
		}
		else {
			this.callbackFunction = inCallback;
		}
		this.serverObj = inServerObj;
		this.methodNameStr = inMethodNameStr;
		this.requestMessageObj = new XMLRPCMessage(inMethodNameStr);
		this.ajaxSocketObj = null;
		// if a session ID exists, make it the first XML-RPC arg
		if (inServerObj.sessionID && (inMethodNameStr != 'login') && (inMethodNameStr != 'digestLogin')) this.requestMessageObj.addParameter(inServerObj.sessionID);
	},
	sendRequest: function() {
		if (!XMLRPCRequest.allRequests) XMLRPCRequest.allRequests = $A([]);
		XMLRPCRequest.allRequests.push(this);
		this.ajaxSocketObj = new Ajax.Request(this.serverObj.url, {
			method: 'post',
			postBody: this.requestMessageObj.xml(),
			onComplete: this.handleCompleted.bind(this),
			asynchronous: true
		});
	},
	makeRequired: function() {
		this.required = true;
		return this; // so we can chain
	},
	handleCompleted: function(inTransportObj) {
		if (inTransportObj.status == 200) {
			this.responseObj = new XMLRPCResponse(inTransportObj);
			if (this.responseObj.deserializedResponse.faultCode && this.responseObj.deserializedResponse.faultString) {
				this.handleError(this, this.responseObj.deserializedResponse.faultCode, this.responseObj.deserializedResponse.faultString);
			}
			else {
				if (this.callbackFunction) this.callbackFunction(this, this.responseObj.deserializedResponse);
			}
		}
		else {
			this.handleError(this, inTransportObj.status, inTransportObj.statusText);
		}
		XMLRPCRequest.allRequests = XMLRPCRequest.allRequests.without(this);
	},
	handleError: function(inRequestObj, inFaultCode, inFaultString) {
		if (this.errorFunction) {
			this.errorFunction(inRequestObj, inFaultCode, inFaultString);
		}
		else if (inFaultCode == 2) { // invalid session
			// If we get a "bad session ID" message and we have a session ID, switch to unauthenticated and try again
			if (this.serverObj.sessionID != 'unauthenticated' && this.requestMessageObj.params.length > 0 && this.requestMessageObj.params[0] == this.serverObj.sessionID) {
				this.serverObj.sessionID = 'unauthenticated';
				document.cookie = 'sessionID=unauthenticated; path=/';
				this.requestMessageObj.params[0] = this.serverObj.sessionID;
				this.sendRequest();
				publisher().publish('LOGGED_OUT', this, {reload:false});
			}
			else {
				var caught = publisher().publish('BAD_SESSION_ID', this);
				if (!caught) alert('Uncaught error from server: bad session ID');
			}
		}
		else if (inFaultCode == 3) { // unauthorized
			var caught = publisher().publish('AUTHORIZATION_FAILED', this);
			if (!caught) alert('Uncaught error from server: unauthorized');
		}
		else {
			var caught = publisher().publish('MISC_SERVER_ERROR', this, {faultCode:inFaultCode, faultString:inFaultString});
			if (!caught) alert('Uncaught error from server: ' + inFaultString + ' (' + inFaultCode + ')');
		}
	},
	toString: function() {
		return this.requestMessageObj.xml();
	}
}

/**
 * XMLRPCServiceRequest: simple subclass of XMLRPCRequest. This one takes the forwarded arguments as an array.
 */

var XMLRPCServiceRequest = Class.create();
Object.extend(Object.extend(XMLRPCServiceRequest.prototype, XMLRPCRequest.prototype), {
	initialize: function(inServerObj, inMethodNameStr, inCallback, inOptArgumentsArray) {
		this.setupRequest(inServerObj, inMethodNameStr, inCallback);
		$A(inOptArgumentsArray||[]).each(function(arg) {
			this.requestMessageObj.addParameter(arg);
		}.bind(this));
		this.sendRequest();
	}
});

/**
 * XMLRPCResponse: object returned from an XMLRPCRequest.
 */

var XMLRPCResponse = Class.create();
XMLRPCResponse.prototype = {
	initialize: function(inTransportObj) {
		// traverse to the first (and only) child of the first "value" tag
		var returnedValueNode = inTransportObj.responseXML.getElementsByTagName('value').item(0).firstChild;
		this.responseText = inTransportObj.responseText;
		this.deserializedResponse = this.deserializeNode(returnedValueNode);
	},
	deserializeNode: function(inNode) {
		switch(inNode.nodeName.toLowerCase()) {
			case "#text":
				return inNode.nodeValue;
				break;
			case "string":
				return $A(inNode.childNodes).inject('', function(str, node) {
					return (node.nodeName.toLowerCase() == "#text" ? str + node.nodeValue : str);
				});
				break;
			case "i4":
			case "int":
				return parseInt(inNode.firstChild.nodeValue);
				break;
			case "double":
				return parseFloat(inNode.firstChild.nodeValue);
				break;
			case "dateTime.iso8601":
				return createDateObjFromISO8601(inNode.firstChild.nodeValue);
				break;
			case "struct":
				return this.deserializeStructNode(inNode);
				break;
			case "array": // skip past useless "data" child tag
				return this.deserializeArrayNode(inNode.getElementsByTagName('data').item(0));
				break;
			case "boolean":
				return (inNode.firstChild.nodeValue == "1");
				break;
		}
	},
	deserializeStructNode: function(inNode) {
		var deserializedStruct = new Object();
		var childNodesLength = inNode.childNodes.length;
		// each child node is a "member" tag... contains "name" and "value" tags
		for (var i = 0; i < childNodesLength; i++) {
			var currentChildNode = inNode.childNodes[i];
			if (currentChildNode.nodeName == 'member') {
				var currentKeyStr = currentChildNode.getElementsByTagName('name').item(0).firstChild.nodeValue;
				var currentValueNode = currentChildNode.getElementsByTagName('value').item(0).firstChild;
				var currentValue = this.deserializeNode(currentValueNode);
				deserializedStruct[currentKeyStr] = currentValue;
			}
		}
		return deserializedStruct;
	},
	deserializeArrayNode: function(inNode) {
		var deserializedArray = new Array();
		var childNodesLength = inNode.childNodes.length;
		// each child is a "value" tag with a type tag inside
		for (var i = 0; i < childNodesLength; i++) {
			var currentChildNode = inNode.childNodes[i];
			
			// ##5033021
			// IE7 Vista beta 2 is choking on cleanWhitespace 
			//Element.cleanWhitespace(currentChildNode);
			if(!document.all) Element.cleanWhitespace(currentChildNode);
			
			if (currentChildNode.nodeName == 'value') {
				var currentValueNode = currentChildNode.firstChild;
				var currentValue = this.deserializeNode(currentValueNode);
				deserializedArray[deserializedArray.length] = currentValue;
			}
		}
		return deserializedArray;
	},
	toString: function() {
		return this.responseText;
	}
}

if (window.loaded) loaded('wikiserver_api.js');