Commit dc2c44c2 authored by Alex Wenckus's avatar Alex Wenckus Committed by alex

Incorporated feedback from code review.

git-svn-id: http://svn.igniterealtime.org/svn/repos/wildfire/trunk@6687 b35dd754-fafc-0310-a699-88a17e54d16e
parent 82bccaca
...@@ -32,7 +32,10 @@ import java.io.IOException; ...@@ -32,7 +32,10 @@ import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
/** /**
* Handles requests to the HTTP Bind service. * Servlet which handles requests to the HTTP binding service. It determines if there is currently
* an {@link HttpSession} related to the connection or if one needs to be created and then passes
* it off to the {@link HttpBindManager} for processing of the client request and formulating of
* the response.
* *
* @author Alexander Wenckus * @author Alexander Wenckus
*/ */
...@@ -50,6 +53,8 @@ public class HttpBindServlet extends HttpServlet { ...@@ -50,6 +53,8 @@ public class HttpBindServlet extends HttpServlet {
} }
} }
private ThreadLocal<XMPPPacketReader> localReader = new ThreadLocal<XMPPPacketReader>();
public HttpBindServlet() { public HttpBindServlet() {
} }
...@@ -66,8 +71,7 @@ public class HttpBindServlet extends HttpServlet { ...@@ -66,8 +71,7 @@ public class HttpBindServlet extends HttpServlet {
sessionManager.stop(); sessionManager.stop();
} }
@Override @Override protected void doPost(HttpServletRequest request, HttpServletResponse response)
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException throws ServletException, IOException
{ {
if (isContinuation(request, response)) { if (isContinuation(request, response)) {
...@@ -196,7 +200,7 @@ public class HttpBindServlet extends HttpServlet { ...@@ -196,7 +200,7 @@ public class HttpBindServlet extends HttpServlet {
{ {
byte[] content; byte[] content;
try { try {
content = connection.getDeliverable().getBytes("utf-8"); content = connection.getResponse().getBytes("utf-8");
} }
catch (HttpBindTimeoutException e) { catch (HttpBindTimeoutException e) {
content = createEmptyBody().getBytes("utf-8"); content = createEmptyBody().getBytes("utf-8");
...@@ -235,9 +239,12 @@ public class HttpBindServlet extends HttpServlet { ...@@ -235,9 +239,12 @@ public class HttpBindServlet extends HttpServlet {
private Document createDocument(HttpServletRequest request) throws private Document createDocument(HttpServletRequest request) throws
DocumentException, IOException, XmlPullParserException { DocumentException, IOException, XmlPullParserException {
// Reader is associated with a new XMPPPacketReader // Reader is associated with a new XMPPPacketReader
XMPPPacketReader reader = new XMPPPacketReader(); XMPPPacketReader reader = localReader.get();
if (reader == null) {
reader = new XMPPPacketReader();
reader.setXPPFactory(factory); reader.setXPPFactory(factory);
localReader.set(reader);
}
return reader.read("utf-8", request.getInputStream()); return reader.read("utf-8", request.getInputStream());
} }
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* $Revision: $ * $Revision: $
* $Date: $ * $Date: $
* *
* Copyright (C) 2006 Jive Software. All rights reserved. * Copyright (C) 2007 Jive Software. All rights reserved.
* *
* This software is published under the terms of the GNU Public License (GPL), * This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution. * a copy of which is included in this distribution.
...@@ -11,17 +11,16 @@ ...@@ -11,17 +11,16 @@
package org.jivesoftware.wildfire.http; package org.jivesoftware.wildfire.http;
import org.jivesoftware.wildfire.Connection;
import org.mortbay.util.ajax.Continuation; import org.mortbay.util.ajax.Continuation;
/** /**
* A connection to a client. The client will wait on getDeliverable() until the server forwards a * Represents one HTTP connection with a client using the HTTP Binding service. The client will wait
* message to it or the wait time on the session timesout. * on {@link #getResponse()} until the server forwards a message to it or the wait time on the
* session timesout.
* *
* @author Alexander Wenckus * @author Alexander Wenckus
*/ */
public class HttpConnection { public class HttpConnection {
private Connection.CompressionPolicy compressionPolicy;
private long requestId; private long requestId;
private String body; private String body;
private HttpSession session; private HttpSession session;
...@@ -29,15 +28,17 @@ public class HttpConnection { ...@@ -29,15 +28,17 @@ public class HttpConnection {
private boolean isClosed; private boolean isClosed;
private boolean isSecure = false; private boolean isSecure = false;
/**
* Constructs an HTTP Connection.
*
* @param requestId the ID which uniquely identifies this request.
* @param isSecure true if this connection is using HTTPS
*/
public HttpConnection(long requestId, boolean isSecure) { public HttpConnection(long requestId, boolean isSecure) {
this.requestId = requestId; this.requestId = requestId;
this.isSecure = isSecure; this.isSecure = isSecure;
} }
public boolean validate() {
return false;
}
/** /**
* The connection should be closed without delivering a stanza to the requestor. * The connection should be closed without delivering a stanza to the requestor.
*/ */
...@@ -54,10 +55,21 @@ public class HttpConnection { ...@@ -54,10 +55,21 @@ public class HttpConnection {
} }
} }
/**
* Returns true if this connection has been closed, either a response was delivered to the
* client or the server closed the connection aburbtly.
*
* @return true if this connection has been closed.
*/
public boolean isClosed() { public boolean isClosed() {
return isClosed; return isClosed;
} }
/**
* Returns true if this connection is using HTTPS.
*
* @return true if this connection is using HTTPS.
*/
public boolean isSecure() { public boolean isSecure() {
return isSecure; return isSecure;
} }
...@@ -93,16 +105,16 @@ public class HttpConnection { ...@@ -93,16 +105,16 @@ public class HttpConnection {
/** /**
* A call that will cause a wait, or in the case of Jetty the thread to be freed, if there is no * A call that will cause a wait, or in the case of Jetty the thread to be freed, if there is no
* deliverable currently available. Once the deliverable becomes available it is returned. * deliverable currently available. Once the response becomes available, it is returned.
* *
* @return the deliverable to send to the client * @return the deliverable to send to the client
* @throws HttpBindTimeoutException to indicate that the maximum wait time requested by the * @throws HttpBindTimeoutException to indicate that the maximum wait time requested by the
* client has been surpassed and an empty response should be returned. * client has been surpassed and an empty response should be returned.
*/ */
public String getDeliverable() throws HttpBindTimeoutException { public String getResponse() throws HttpBindTimeoutException {
if (body == null && continuation != null) { if (body == null && continuation != null) {
try { try {
body = waitForDeliverable(); body = waitForResponse();
} }
catch (HttpBindTimeoutException e) { catch (HttpBindTimeoutException e) {
this.isClosed = true; this.isClosed = true;
...@@ -115,31 +127,11 @@ public class HttpConnection { ...@@ -115,31 +127,11 @@ public class HttpConnection {
return body; return body;
} }
private String waitForDeliverable() throws HttpBindTimeoutException { /**
if (continuation.suspend(session.getWait() * 1000)) { * Returns the ID which uniquely identifies this connection.
String deliverable = (String) continuation.getObject(); *
// This will occur when the hold attribute of a session has been exceded. * @return the ID which uniquely identifies this connection.
if (deliverable == null) { */
throw new HttpBindTimeoutException();
}
return deliverable;
}
throw new HttpBindTimeoutException("Request " + requestId + " exceded response time from " +
"server of " + session.getWait() + " seconds.");
}
public boolean isCompressed() {
return false;
}
public Connection.CompressionPolicy getCompressionPolicy() {
return compressionPolicy;
}
public void setCompressionPolicy(Connection.CompressionPolicy compressionPolicy) {
this.compressionPolicy = compressionPolicy;
}
public long getRequestId() { public long getRequestId() {
return requestId; return requestId;
} }
...@@ -165,4 +157,17 @@ public class HttpConnection { ...@@ -165,4 +157,17 @@ public class HttpConnection {
void setContinuation(Continuation continuation) { void setContinuation(Continuation continuation) {
this.continuation = continuation; this.continuation = continuation;
} }
private String waitForResponse() throws HttpBindTimeoutException {
if (continuation.suspend(session.getWait() * 1000)) {
String deliverable = (String) continuation.getObject();
// This will occur when the hold attribute of a session has been exceded.
if (deliverable == null) {
throw new HttpBindTimeoutException();
}
return deliverable;
}
throw new HttpBindTimeoutException("Request " + requestId + " exceded response time from " +
"server of " + session.getWait() + " seconds.");
}
} }
...@@ -32,21 +32,41 @@ import java.net.InetAddress; ...@@ -32,21 +32,41 @@ import java.net.InetAddress;
* <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>. * <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>.
*/ */
public class HttpSessionManager { public class HttpSessionManager {
private SessionManager sessionManager; private SessionManager sessionManager;
private Map<String, HttpSession> sessionMap = new ConcurrentHashMap<String, HttpSession>(); private Map<String, HttpSession> sessionMap = new ConcurrentHashMap<String, HttpSession>();
private TimerTask inactivityTask; private TimerTask inactivityTask;
private SessionListener sessionListener = new SessionListener() {
public void connectionOpened(HttpSession session, HttpConnection connection) {
}
public void connectionClosed(HttpSession session, HttpConnection connection) {
}
public void sessionClosed(HttpSession session) {
sessionMap.remove(session.getStreamID().getID());
sessionManager.removeSession(session);
}
};
/**
* Creates a new HttpSessionManager instance.
*/
public HttpSessionManager() { public HttpSessionManager() {
this.sessionManager = SessionManager.getInstance(); this.sessionManager = SessionManager.getInstance();
} }
/**
* Starts the services used by the HttpSessionManager.
*/
public void start() { public void start() {
inactivityTask = new HttpSessionReaper(); inactivityTask = new HttpSessionReaper();
TaskEngine.getInstance().schedule(inactivityTask, 30 * JiveConstants.SECOND, TaskEngine.getInstance().schedule(inactivityTask, 30 * JiveConstants.SECOND,
30 * JiveConstants.SECOND); 30 * JiveConstants.SECOND);
} }
/**
* Stops any services and cleans up any resources used by the HttpSessionManager.
*/
public void stop() { public void stop() {
inactivityTask.cancel(); inactivityTask.cancel();
for(HttpSession session : sessionMap.values()) { for(HttpSession session : sessionMap.values()) {
...@@ -173,6 +193,47 @@ public class HttpSessionManager { ...@@ -173,6 +193,47 @@ public class HttpSessionManager {
return JiveGlobals.getIntProperty("xmpp.httpbind.client.idle", 30); return JiveGlobals.getIntProperty("xmpp.httpbind.client.idle", 30);
} }
/**
* Forwards a client request, which is related to a session, to the server. A connection is
* created and queued up in the provided session. When a connection reaches the top of a queue
* any pending packets bound for the client will be forwarded to the client through the
* connection.
*
* @param rid the unique, sequential, requestID sent from the client.
* @param session the HTTP session of the client that made the request.
* @param isSecure true if the request was made over a secure channel, HTTPS, and false if it
* was not.
* @param rootNode the XML body of the request.
* @return the created HTTP connection.
* @throws HttpBindException for several reasons: if the encoding inside of an auth packet is
* not recognized by the server, or if the packet type is not recognized.
* @throws HttpConnectionClosedException if the session is no longer available.
*/
public HttpConnection forwardRequest(long rid, HttpSession session, boolean isSecure,
Element rootNode) throws HttpBindException,
HttpConnectionClosedException
{
//noinspection unchecked
List<Element> elements = rootNode.elements();
boolean isPoll = elements.size() <= 0;
HttpConnection connection = session.createConnection(rid, isPoll, isSecure);
SessionPacketRouter router = new SessionPacketRouter(session);
for (Element packet : elements) {
try {
router.route(packet);
}
catch (UnsupportedEncodingException e) {
throw new HttpBindException("Bad auth request, unknown encoding", true, 400);
}
catch (UnknownStanzaException e) {
throw new HttpBindException("Unknown packet type.", false, 400);
}
}
return connection;
}
private HttpSession createSession(long rid, InetAddress address) throws UnauthorizedException { private HttpSession createSession(long rid, InetAddress address) throws UnauthorizedException {
// Create a ClientSession for this user. // Create a ClientSession for this user.
StreamID streamID = SessionManager.getInstance().nextStreamID(); StreamID streamID = SessionManager.getInstance().nextStreamID();
...@@ -180,18 +241,7 @@ public class HttpSessionManager { ...@@ -180,18 +241,7 @@ public class HttpSessionManager {
HttpSession session = sessionManager.createClientHttpSession(rid, address, streamID); HttpSession session = sessionManager.createClientHttpSession(rid, address, streamID);
// Register that the new session is associated with the specified stream ID // Register that the new session is associated with the specified stream ID
sessionMap.put(streamID.getID(), session); sessionMap.put(streamID.getID(), session);
session.addSessionCloseListener(new SessionListener() { session.addSessionCloseListener(sessionListener);
public void connectionOpened(HttpSession session, HttpConnection connection) {
}
public void connectionClosed(HttpSession session, HttpConnection connection) {
}
public void sessionClosed(HttpSession session) {
sessionMap.remove(session.getStreamID().getID());
sessionManager.removeSession(session);
}
});
return session; return session;
} }
...@@ -227,52 +277,12 @@ public class HttpSessionManager { ...@@ -227,52 +277,12 @@ public class HttpSessionManager {
return response.asXML(); return response.asXML();
} }
/**
* Forwards a client request, which is related to a session, to the server. A connection is
* created and queued up in the provided session. When a connection reaches the top of a queue
* any pending packets bound for the client will be forwarded to the client through the
* connection.
*
* @param rid the unique, sequential, requestID sent from the client.
* @param session the HTTP session of the client that made the request.
* @param isSecure true if the request was made over a secure channel, HTTPS, and false if it
* was not.
* @param rootNode the XML body of the request.
* @return the created HTTP connection.
* @throws HttpBindException for several reasons: if the encoding inside of an auth packet is
* not recognized by the server, or if the packet type is not recognized.
* @throws HttpConnectionClosedException if the session is no longer available.
*/
public HttpConnection forwardRequest(long rid, HttpSession session, boolean isSecure,
Element rootNode) throws HttpBindException,
HttpConnectionClosedException
{
//noinspection unchecked
List<Element> elements = rootNode.elements();
boolean isPoll = elements.size() <= 0;
HttpConnection connection = session.createConnection(rid, isPoll, isSecure);
SessionPacketRouter router = new SessionPacketRouter(session);
for (Element packet : elements) {
try {
router.route(packet);
}
catch (UnsupportedEncodingException e) {
throw new HttpBindException("Bad auth request, unknown encoding", true, 400);
}
catch (UnknownStanzaException e) {
throw new HttpBindException("Unknown packet type.", false, 400);
}
}
return connection;
}
private class HttpSessionReaper extends TimerTask { private class HttpSessionReaper extends TimerTask {
public void run() { public void run() {
long currentTime = System.currentTimeMillis();
for(HttpSession session : sessionMap.values()) { for(HttpSession session : sessionMap.values()) {
long lastActive = (System.currentTimeMillis() - session.getLastActivity()) / 1000; long lastActive = (currentTime - session.getLastActivity()) / 1000;
if (lastActive > session.getInactivityTimeout()) { if (lastActive > session.getInactivityTimeout()) {
session.close(); session.close();
} }
......
...@@ -24,9 +24,9 @@ import java.util.zip.GZIPOutputStream; ...@@ -24,9 +24,9 @@ import java.util.zip.GZIPOutputStream;
import java.io.*; import java.io.*;
/** /**
* * Combines and serves resources, such as javascript or css files.
*/ */
public class JavaScriptServlet extends HttpServlet { public class ResourceServlet extends HttpServlet {
// private static String suffix = ""; // Set to "_src" to use source version // private static String suffix = ""; // Set to "_src" to use source version
private static long expiresOffset = 3600 * 24 * 10; // 10 days util client cache expires private static long expiresOffset = 3600 * 24 * 10; // 10 days util client cache expires
private boolean debug = false; private boolean debug = false;
...@@ -44,10 +44,11 @@ public class JavaScriptServlet extends HttpServlet { ...@@ -44,10 +44,11 @@ public class JavaScriptServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) { public void service(HttpServletRequest request, HttpServletResponse response) {
boolean compress = false; boolean compress = false;
boolean javascript = request.getRequestURI().endsWith("scripts/");
if (!disableCompression) { if (!disableCompression) {
if (request.getHeader("accept-encoding") != null && if (request.getHeader("accept-encoding") != null &&
request.getHeader("accept-encoding").indexOf("gzip") != -1) request.getHeader("accept-encoding").indexOf("gzip") != -1) {
{
compress = true; compress = true;
} }
else if (request.getHeader("---------------") != null) { else if (request.getHeader("---------------") != null) {
...@@ -56,7 +57,12 @@ public class JavaScriptServlet extends HttpServlet { ...@@ -56,7 +57,12 @@ public class JavaScriptServlet extends HttpServlet {
} }
} }
if(javascript) {
response.setHeader("Content-type", "text/javascript"); response.setHeader("Content-type", "text/javascript");
}
else {
response.setHeader("Content-type", "text/css");
}
response.setHeader("Vary", "Accept-Encoding"); // Handle proxies response.setHeader("Vary", "Accept-Encoding"); // Handle proxies
if (!debug) { if (!debug) {
...@@ -74,21 +80,24 @@ public class JavaScriptServlet extends HttpServlet { ...@@ -74,21 +80,24 @@ public class JavaScriptServlet extends HttpServlet {
InputStream in = null; InputStream in = null;
try { try {
byte[] jsContent; byte[] content;
String cacheKey = String.valueOf(compress); String cacheKey = String.valueOf(compress + " " + javascript);
jsContent = cache.get(cacheKey); content = cache.get(cacheKey);
if (debug || jsContent == null) { if (javascript && (debug || content == null)) {
jsContent = getJavaScriptContent(compress); content = getJavaScriptContent(compress);
cache.put(cacheKey, jsContent); cache.put(cacheKey, content);
}
else if(!javascript && content == null) {
} }
response.setContentLength(jsContent.length); response.setContentLength(content.length);
if (compress) { if (compress) {
response.setHeader("Content-Encoding", "gzip"); response.setHeader("Content-Encoding", "gzip");
} }
// Write the content out // Write the content out
in = new ByteArrayInputStream(jsContent); in = new ByteArrayInputStream(content);
out = response.getOutputStream(); out = response.getOutputStream();
// Use a 128K buffer. // Use a 128K buffer.
...@@ -142,8 +151,9 @@ public class JavaScriptServlet extends HttpServlet { ...@@ -142,8 +151,9 @@ public class JavaScriptServlet extends HttpServlet {
} }
private static Collection<String> getJavascriptFiles() { private static Collection<String> getJavascriptFiles() {
return Arrays.asList("xmlextras.js", "connection.js", "dojo.js", return Arrays.asList("prototype.js", "getelementsbyselector.js", "sarissa.js",
"flash.js"); "connection.js", "yahoo-min.js", "dom-min.js", "event-min.js", "dragdrop-min.js",
"yui-ext.js", "spank.js");
} }
private static String getJavaScriptFile(String path) { private static String getJavaScriptFile(String path) {
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<servlet> <servlet>
<servlet-name>JavaScriptServlet</servlet-name> <servlet-name>JavaScriptServlet</servlet-name>
<servlet-class>org.jivesoftware.wildfire.http.JavaScriptServlet</servlet-class> <servlet-class>org.jivesoftware.wildfire.http.ResourceServlet</servlet-class>
<load-on-startup>1</load-on-startup> <load-on-startup>1</load-on-startup>
</servlet> </servlet>
......
/*----------------------------------------------------------------------------\
| XML Extras |
|-----------------------------------------------------------------------------|
| Created by Erik Arvidsson |
| (http://webfx.eae.net/contact.html#erik) |
| For WebFX (http://webfx.eae.net/) |
|-----------------------------------------------------------------------------|
| XML and XML HTTP request abstraction. |
|-----------------------------------------------------------------------------|
| Copyright (c) 2001, 2002, 2003, 2006 Erik Arvidsson |
|-----------------------------------------------------------------------------|
| Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| use this file except in compliance with the License. You may obtain a copy |
| of the License at http://www.apache.org/licenses/LICENSE-2.0 |
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| License for the specific language governing permissions and limitations |
| under the License. |
|-----------------------------------------------------------------------------|
| 2001-09-27 | Original Version Posted. |
| 2006-05-29 | Changed license to Apache Software License 2.0. |
|-----------------------------------------------------------------------------|
| Created 2001-09-27 | All changes are in the log above. | Updated 2006-05-29 |
\----------------------------------------------------------------------------*/
//<script>
//////////////////
// Helper Stuff //
//////////////////
// used to find the Automation server name
function getDomDocumentPrefix() {
if (getDomDocumentPrefix.prefix)
return getDomDocumentPrefix.prefix;
var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"];
var o;
for (var i = 0; i < prefixes.length; i++) {
try {
// try to create the objects
o = new ActiveXObject(prefixes[i] + ".DomDocument");
return getDomDocumentPrefix.prefix = prefixes[i];
}
catch (ex) {};
}
throw new Error("Could not find an installed XML parser");
}
function getXmlHttpPrefix() {
if (getXmlHttpPrefix.prefix)
return getXmlHttpPrefix.prefix;
var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"];
var o;
for (var i = 0; i < prefixes.length; i++) {
try {
// try to create the objects
o = new ActiveXObject(prefixes[i] + ".XmlHttp");
return getXmlHttpPrefix.prefix = prefixes[i];
}
catch (ex) {};
}
throw new Error("Could not find an installed XML parser");
}
//////////////////////////
// Start the Real stuff //
//////////////////////////
// XmlHttp factory
function XmlHttp() {}
XmlHttp.create = function () {
try {
if (window.XMLHttpRequest) {
var req = new XMLHttpRequest();
// some versions of Moz do not support the readyState property
// and the onreadystate event so we patch it!
if (req.readyState == null) {
req.readyState = 1;
req.addEventListener("load", function () {
req.readyState = 4;
if (typeof req.onreadystatechange == "function")
req.onreadystatechange();
}, false);
}
return req;
}
if (window.ActiveXObject) {
return new ActiveXObject(getXmlHttpPrefix() + ".XmlHttp");
}
}
catch (ex) {}
// fell through
throw new Error("Your browser does not support XmlHttp objects");
};
// XmlDocument factory
function XmlDocument() {}
XmlDocument.create = function () {
try {
// DOM2
if (document.implementation && document.implementation.createDocument) {
var doc = document.implementation.createDocument("", "", null);
// some versions of Moz do not support the readyState property
// and the onreadystate event so we patch it!
if (doc.readyState == null) {
doc.readyState = 1;
doc.addEventListener("load", function () {
doc.readyState = 4;
if (typeof doc.onreadystatechange == "function")
doc.onreadystatechange();
}, false);
}
return doc;
}
if (window.ActiveXObject)
return new ActiveXObject(getDomDocumentPrefix() + ".DomDocument");
}
catch (ex) {}
throw new Error("Your browser does not support XmlDocument objects");
};
// Create the loadXML method and xml getter for Mozilla
if (window.DOMParser &&
window.XMLSerializer &&
window.Node && Node.prototype && Node.prototype.__defineGetter__) {
// XMLDocument did not extend the Document interface in some versions
// of Mozilla. Extend both!
//XMLDocument.prototype.loadXML =
Document.prototype.loadXML = function (s) {
// parse the string to a new doc
var doc2 = (new DOMParser()).parseFromString(s, "text/xml");
// remove all initial children
while (this.hasChildNodes())
this.removeChild(this.lastChild);
// insert and import nodes
for (var i = 0; i < doc2.childNodes.length; i++) {
this.appendChild(this.importNode(doc2.childNodes[i], true));
}
};
/*
* xml getter
*
* This serializes the DOM tree to an XML String
*
* Usage: var sXml = oNode.xml
*
*/
// XMLDocument did not extend the Document interface in some versions
// of Mozilla. Extend both!
/*
XMLDocument.prototype.__defineGetter__("xml", function () {
return (new XMLSerializer()).serializeToString(this);
});
*/
Document.prototype.__defineGetter__("xml", function () {
return (new XMLSerializer()).serializeToString(this);
});
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment