You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by dd...@apache.org on 2012/02/10 23:12:22 UTC

svn commit: r1242959 - in /shindig/trunk: config/ content/samplecontainer/examples/ features/ features/src/main/javascript/features/ features/src/main/javascript/features/container.util/ features/src/main/javascript/features/core.io/ features/src/main/...

Author: ddumont
Date: Fri Feb 10 22:12:21 2012
New Revision: 1242959

URL: http://svn.apache.org/viewvc?rev=1242959&view=rev
Log:
SHINDIG-1695 new core-gadget feature "proxied-form-post" gadgets.proxiedMultipartFormPost

Added:
    shindig/trunk/content/samplecontainer/examples/ImageUploadGadget.xml   (with props)
    shindig/trunk/features/src/main/javascript/features/proxied-form-post/
    shindig/trunk/features/src/main/javascript/features/proxied-form-post/feature.xml   (with props)
    shindig/trunk/features/src/main/javascript/features/proxied-form-post/post.js   (with props)
    shindig/trunk/features/src/main/javascript/features/proxied-form-post/taming.js   (with props)
    shindig/trunk/java/server/src/main/
    shindig/trunk/java/server/src/main/webapp/
    shindig/trunk/java/server/src/main/webapp/META-INF/
    shindig/trunk/java/server/src/main/webapp/META-INF/MANIFEST.MF   (with props)
Modified:
    shindig/trunk/config/container.js
    shindig/trunk/features/pom.xml
    shindig/trunk/features/src/main/javascript/features/container.util/util.js
    shindig/trunk/features/src/main/javascript/features/core.io/io.js
    shindig/trunk/features/src/main/javascript/features/features.txt
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetException.java
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpRequest.java
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java

Modified: shindig/trunk/config/container.js
URL: http://svn.apache.org/viewvc/shindig/trunk/config/container.js?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/config/container.js (original)
+++ shindig/trunk/config/container.js Fri Feb 10 22:12:21 2012
@@ -140,6 +140,9 @@
 // Enables whitelist checks
 "gadgets.admin.enableGadgetWhitelist" : "false",
 
+// Max post size for posts through the makeRequest proxy.
+"gadgets.jsonProxyUrl.maxPostSize" : 5242880, // 5 MiB
+
 // This config data will be passed down to javascript. Please
 // configure your object using the feature name rather than
 // the javascript name.

Added: shindig/trunk/content/samplecontainer/examples/ImageUploadGadget.xml
URL: http://svn.apache.org/viewvc/shindig/trunk/content/samplecontainer/examples/ImageUploadGadget.xml?rev=1242959&view=auto
==============================================================================
--- shindig/trunk/content/samplecontainer/examples/ImageUploadGadget.xml (added)
+++ shindig/trunk/content/samplecontainer/examples/ImageUploadGadget.xml Fri Feb 10 22:12:21 2012
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+	<ModulePrefs title="Proxy Tester" height="500">
+		<Require feature="dynamic-height"/>
+		<Require feature="embedded-experiences"/>
+		<Require feature="proxied-form-post"/>
+	</ModulePrefs>
+	<Content type="html"><![CDATA[
+	  <script>
+	    function init() {
+	      gadgets.window.adjustHeight();
+	    }
+
+	    function doPostFile() {
+	      document.getElementById("result").innerHTML = '';
+	      document.getElementById("progbar").style.width = 0;
+
+	      var form = document.getElementById("imageform"),
+	          params = {};
+	      params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.TEXT;
+	      params[gadgets.io.RequestParameters.HEADERS] = {
+	        Referer: 'http://bayimg.com/',
+	        Origin: 'http://bayimg.com'
+	      };
+
+	      gadgets.io.proxiedMultipartFormPost(form, params, function(response) {
+	        document.getElementById("progbar").style.width = "100%";
+	        window.doAbort = null;
+	        if (response && response.errors && response.errors.length) {
+	          try {
+	            document.getElementById("result").innerHTML = /<body>(.*)<\/body>/.exec(response.errors[0])[1];
+	          } catch(e) {
+	            document.getElementById("result").innerHTML = 'Error: ' + response.errors[0];
+	          }
+	        } else {
+	          try {
+	            var url = /<img[^>]+alt="Image"[^>]+src="([^"]+)"/.exec(response.text)[1];
+	            document.getElementById("result").innerHTML = '<img src="' + url + '" onload="init();">';
+	          } catch(e) {
+	            document.getElementById("result").innerHTML = 'Error';
+	          }
+	        }
+	        gadgets.window.adjustHeight();
+	      },
+	      function(event, abort) {
+	        if (!window.doAbort)
+	          window.doAbort = abort;
+	        if (event && event.lengthComputable) {
+	          var percent = Math.ceil((event.loaded / event.total) * 100) + "%";
+	          document.getElementById("progbar").style.width = percent;
+	        }
+	      });
+	    }
+
+	     gadgets.util.registerOnLoadHandler(init);
+	  </script>
+
+	  <h3>Upload an image file to bayimg.com</h3>
+	  <form id="imageform" action="http://upload.bayimg.com/upload">
+	    <fieldset>
+	      File: <input type="file" name="file"></input>
+	      <input type="hidden" name="code" value="opensocial"></input>
+	      <input type="hidden" name="tags" value=""></input>
+	    </fieldset>
+	    <input type="button" value="Upload" onClick="doPostFile();"/>
+	    <input type="button" value="Abort" onClick="if (window.doAbort) doAbort();"/>
+	  </form>
+
+	  <div style="border:1px solid rgb(90,90,115); background-color:#ffffff; padding:0px; width:200px; height:20px;overflow:hidden;">
+	    <div id="progbar" style="width:0%; height:100%; background-color:rgb(108,157,222)"></div>
+	  </div>
+
+	  <div id="result"></div>
+	]]></Content>
+</Module>
\ No newline at end of file

Propchange: shindig/trunk/content/samplecontainer/examples/ImageUploadGadget.xml
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: shindig/trunk/features/pom.xml
URL: http://svn.apache.org/viewvc/shindig/trunk/features/pom.xml?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/features/pom.xml (original)
+++ shindig/trunk/features/pom.xml Fri Feb 10 22:12:21 2012
@@ -170,7 +170,7 @@
                 <source>opensocial-base/fieldtranslations.js</source>
                 <source>opensocial-base/jsonactivity.js</source>
                 <source>opensocial-base/jsonalbum.js</source>
-                <source>opensocial-base/jsonmediaitem.js</source> 
+                <source>opensocial-base/jsonmediaitem.js</source>
                 <source>opensocial-base/jsonperson.js</source>
                 <source>opensocial-jsonrpc/jsonrpccontainer.js</source>
                 <source>osapi.base/osapi.js</source>
@@ -188,6 +188,7 @@
                 <source>embeddedexperiences/embedded_experiences_container.js</source>
                 <source>open-views/viewenhancements-container.js</source>
                 <source>open-views/viewenhancements.js</source>
+                <source>proxied-form-post/post.js</source>
               </sources>
               <testSourceDirectory>${basedir}/src/test/javascript/features</testSourceDirectory>
               <testSuites>

Modified: shindig/trunk/features/src/main/javascript/features/container.util/util.js
URL: http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/container.util/util.js?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/container.util/util.js (original)
+++ shindig/trunk/features/src/main/javascript/features/container.util/util.js Fri Feb 10 22:12:21 2012
@@ -174,18 +174,18 @@ osapi.container.util.createIframeHtml = 
   // requires more code, and creating an element with it results in a click
   // sound in IE (unconfirmed), setAttribute('class') may need browser-specific
   // variants.
-  var out = [];
-  out.push('<iframe ');
+  var out = [], n = 0;
+  out[n++] = '<iframe ';
   for (var key in iframeParams) {
       var value = iframeParams[key];
       if (typeof(value) != 'undefined') {
-          out.push(key);
-          out.push('="');
-          out.push(value);
-          out.push('" ');
+          out[n++] = key;
+          out[n++] = '="';
+          out[n++] = value;
+          out[n++] = '" ';
       }
   }
-  out.push('></iframe>');
+  out[n++] = '></iframe>';
 
   return out.join('');
 };

Modified: shindig/trunk/features/src/main/javascript/features/core.io/io.js
URL: http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/core.io/io.js?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/core.io/io.js (original)
+++ shindig/trunk/features/src/main/javascript/features/core.io/io.js Fri Feb 10 22:12:21 2012
@@ -541,7 +541,12 @@ gadgets.io = function() {
         ret = window.location.protocol + ret;
       }
       return ret;
-    }
+    },
+
+    /**
+     * @private
+     */
+    processResponse_: processResponse
   };
 }();
 

Modified: shindig/trunk/features/src/main/javascript/features/features.txt
URL: http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/features.txt?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/features.txt (original)
+++ shindig/trunk/features/src/main/javascript/features/features.txt Fri Feb 10 22:12:21 2012
@@ -6,9 +6,9 @@
 #  to you 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
@@ -80,6 +80,7 @@ features/open-views/feature.xml
 features/osapi.base/feature.xml
 features/osapi/feature.xml
 features/osml/feature.xml
+features/proxied-form-post/feature.xml
 features/pubsub/feature.xml
 features/rpc/feature.xml
 features/shared-script-frame/feature.xml

Added: shindig/trunk/features/src/main/javascript/features/proxied-form-post/feature.xml
URL: http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/proxied-form-post/feature.xml?rev=1242959&view=auto
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/proxied-form-post/feature.xml (added)
+++ shindig/trunk/features/src/main/javascript/features/proxied-form-post/feature.xml Fri Feb 10 22:12:21 2012
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+<!--
+  Required configuration:
+
+  jsonProxyUrl: A url pointing to the JSON proxy endpoint, used by
+      gadgets.io.makeRequest. All data passed to this end point will be
+      encoded inside of the POST body.
+-->
+  <name>proxied-form-post</name>
+  <dependency>core.io</dependency>
+  <dependency>security-token</dependency>
+  <dependency>taming</dependency>
+  <gadget>
+    <script src="post.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.io.proxiedMultipartFormPost</exports>
+    </api>
+  </gadget>
+</feature>

Propchange: shindig/trunk/features/src/main/javascript/features/proxied-form-post/feature.xml
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: shindig/trunk/features/src/main/javascript/features/proxied-form-post/post.js
URL: http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/proxied-form-post/post.js?rev=1242959&view=auto
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/proxied-form-post/post.js (added)
+++ shindig/trunk/features/src/main/javascript/features/proxied-form-post/post.js Fri Feb 10 22:12:21 2012
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This provides the ability to upload a file through the shindig
+ *         proxy by posting a form element.
+ */
+
+(function () {
+  var config,
+      iframe,
+      work,
+      workQ = [],
+      workTimout;
+
+  /**
+   * @param {Object} configuration Configuration settings.
+   * @private
+   */
+  function init(configuration) {
+    config = configuration['core.io'] || {};
+  }
+  gadgets.config.register('core.io', {
+    "jsonProxyUrl": gadgets.config.NonEmptyStringValidator
+  }, init);
+
+  // IE and FF3.6 only
+  function getIFrame() {
+    if (!iframe) {
+      var container = gadgets.util.createElement('div');
+      container.innerHTML =
+          '<iframe name="os-xhrframe"'
+        + ' style="position:absolute;left:1px;top:1px;height:1px;width:1px;visibility:hidden"'
+        + ' onload="gadgets.io.proxiedMultipartFormPostCB_();"></iframe>';
+      gadgets.util.getBodyElement().appendChild(iframe = container.firstChild);
+    }
+    return iframe;
+  }
+
+  gadgets.io.proxiedMultipartFormPostCB_ = function(event) {
+    if (!work) {
+      return;
+    }
+
+    try {
+      var doc = iframe.contentDocument || iframe.document,
+          data = doc.getElementsByTagName('textarea')[0].value;
+    } catch (e) {}
+    var xhrobj = {
+      readyState: 4,
+      status: data ? 200 : 500,
+      responseText: data ? data : 'Unknown error.'
+    };
+    work.form.setAttribute('action', work.url);
+
+    gadgets.io.processResponse_.call(null, work.url, work.onresult, work.params, xhrobj);
+    work = 0;
+    if (workQ.length) {
+      work = workQ.shift();
+      work.form.submit();
+    }
+  };
+
+  /**
+   * Posts a form through the proxy to a remote service.
+   *
+   * @param {Element} form The form element to be posted. This form element must
+   *           include the action attribute for where you want the data to be posted.
+   * @param {Object} params The request options. Similar to gadgets.io.makeRequest
+   * @param {function} onresult The callback to process the success or failure of
+   *           the post.  Similar to gadgets.io.makeRequest's callback param.
+   * @param {function=} opt_onprogress The callback to call with progress updates.
+   *           This callback may not be called if the browser does not
+   *           support the api. Please note that this only reflects the progress of
+   *           uploading to the shindig proxy and does not account for progress of
+   *           the request from the shindig proxy to the remote server.
+   *           This callback takes 2 arguments:
+   *             {Event} event The progress event.
+   *             {function} abort Function to call to abort the post.
+   * @param {FormData=} opt_formdata The FormData object to post. If provided, this
+   *           object will be used as the data to post the provided form and the form
+   *           element provided should contain the action attribute of where to post
+   *           the form. If ommitted and the browser supports it, a FormData object
+   *           will be created from the provided form element.
+   */
+  gadgets.io.proxiedMultipartFormPost = function (form, params, onresult, onprogress, formdata) {
+    params = params || {};
+
+    var auth, signOwner,
+        signViewer = signOwner = true,
+        st = shindig.auth.getSecurityToken(),
+        url = form.getAttribute('action'),
+        contentType = 'multipart/form-data',
+        headers = params['HEADERS'] || (params['HEADERS'] = {}),
+        urlParams = gadgets.util.getUrlParameters();
+
+    if (params['AUTHORIZATION'] && params['AUTHORIZATION'] !== 'NONE') {
+      auth = params['AUTHORIZATION'].toLowerCase();
+    }
+    // Include owner information?
+    if (typeof params['OWNER_SIGNED'] !== 'undefined') {
+      signOwner = params['OWNER_SIGNED'];
+    }
+    // Include viewer information?
+    if (typeof params['VIEWER_SIGNED'] !== 'undefined') {
+      signViewer = params['VIEWER_SIGNED'];
+    }
+
+    if (!url) {
+      throw new Error('Form missing action attribute.');
+    }
+    if (!st) {
+      throw new Error('Something went wrong, security token is unavailable.');
+    }
+
+    form.setAttribute('enctype', headers['Content-Type'] = contentType);
+
+    // Info that the proxy endpoint needs.
+    var query = {
+      'MPFP': 1, // This will force an alternate route in the makeRequest proxy endpoint
+      'url': url,
+      'httpMethod': 'POST',
+      'headers': gadgets.io.encodeValues(headers, false),
+      'authz': auth || '',
+      'st': st,
+      'contentType': params['CONTENT_TYPE'] || 'TEXT',
+      'signOwner': signOwner,
+      'signViewer': signViewer,
+      // should we bypass gadget spec cache (e.g. to read OAuth provider URLs)
+      'bypassSpecCache': gadgets.util.getUrlParameters()['nocache'] || '',
+      'getFullHeaders': !!params['GET_FULL_HEADERS']
+    };
+
+    delete params['OAUTH_RECEIVED_CALLBACK'];
+    // OAuth goodies
+    if (auth === 'oauth' || auth === 'signed' || auth === 'oauth2') {
+      // Just copy the OAuth parameters into the req to the server
+      for (var opt in params) {
+        if (params.hasOwnProperty(opt)) {
+          if (opt.indexOf('OAUTH_') === 0 || opt === 'code') {
+            query[opt] = params[opt];
+          }
+        }
+      }
+    }
+
+    var proxyUrl = config['jsonProxyUrl'].replace('%host%', document.location.host)
+      + '?' + gadgets.io.encodeValues(query);
+
+    if (window.FormData) {
+      var xhr = new XMLHttpRequest(),
+          data = formdata || new FormData(form);
+
+      if (xhr.upload) {
+        xhr.upload.onprogress = function(event) {
+          onprogress.call(null, event, xhr.abort);
+        };
+      }
+      xhr.onreadystatechange = gadgets.util.makeClosure(
+        null, gadgets.io.processResponse_, url, onresult, params, xhr
+      );
+      xhr.open("POST", proxyUrl);
+      xhr.send(data);
+    } else {
+      // IE and FF3.6 only
+      proxyUrl += '&iframe=1';
+      form.setAttribute('action', proxyUrl);
+      form.setAttribute('target', getIFrame().name);
+      form.setAttribute('method', 'POST');
+
+      // This transport can only support 1 request at a time, so we serialize
+      // them.
+      var job = {
+        form: form,
+        onresult: onresult,
+        params: params,
+        url: url
+      };
+      if (work) {
+        workQ.push(job);
+      } else {
+        work = job;
+        form.submit();
+      }
+    }
+  };
+
+})();
\ No newline at end of file

Propchange: shindig/trunk/features/src/main/javascript/features/proxied-form-post/post.js
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: shindig/trunk/features/src/main/javascript/features/proxied-form-post/taming.js
URL: http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/proxied-form-post/taming.js?rev=1242959&view=auto
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/proxied-form-post/taming.js (added)
+++ shindig/trunk/features/src/main/javascript/features/proxied-form-post/taming.js Fri Feb 10 22:12:21 2012
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.io.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.io, 'proxiedMultipartFormPost']
+  ]);
+});

Propchange: shindig/trunk/features/src/main/javascript/features/proxied-form-post/taming.js
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetException.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetException.java?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetException.java (original)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetException.java Fri Feb 10 22:12:21 2012
@@ -51,6 +51,7 @@ public class GadgetException extends Exc
     INVALID_PARAMETER,
     MISSING_PARAMETER,
     UNRECOGNIZED_PARAMETER,
+    POST_TOO_LARGE,
 
     // Interface component errors.
     MISSING_FEATURE_REGISTRY,

Modified: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpRequest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpRequest.java?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpRequest.java (original)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpRequest.java Fri Feb 10 22:12:21 2012
@@ -26,6 +26,7 @@ import org.apache.shindig.common.uri.Uri
 import org.apache.shindig.common.util.CharsetUtil;
 import org.apache.shindig.config.ContainerConfig;
 import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.admin.BasicGadgetAdminStore;
 import org.apache.shindig.gadgets.oauth.OAuthArguments;
 import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
 
@@ -40,12 +41,16 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  * Creates HttpRequests. A new HttpRequest should be created for every unique HttpRequest
  * being constructed.
  */
 public class HttpRequest {
+  private static final Logger LOG = Logger.getLogger(HttpRequest.class.getName());
+
   /** Automatically added to every request so that we know that the request came from our server. */
   public static final String DOS_PREVENTION_HEADER = "X-shindig-dos";
   static final String DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=utf-8";
@@ -188,20 +193,26 @@ public class HttpRequest {
    * Assigns the specified body to the request, copying all input bytes.
    */
   public HttpRequest setPostBody(byte[] postBody) {
-    if (postBody == null) {
-      this.postBody = ArrayUtils.EMPTY_BYTE_ARRAY;
-    } else {
-      this.postBody = new byte[postBody.length];
-      System.arraycopy(postBody, 0, this.postBody, 0, postBody.length);
+    try {
+      setPostBody(postBody == null ? null : new ByteArrayInputStream(postBody));
+    } catch (IOException e){
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.log(Level.WARNING, e.getMessage(), e);  // Shouldn't ever happen.
+      }
     }
     return this;
   }
 
   /**
    * Fills in the request body from an InputStream.
+   * @throws IOException
    */
   public HttpRequest setPostBody(InputStream is) throws IOException {
-    postBody = IOUtils.toByteArray(is);
+    if (postBody == null) {
+      this.postBody = ArrayUtils.EMPTY_BYTE_ARRAY;
+    } else {
+      postBody = IOUtils.toByteArray(is);
+    }
     return this;
   }
 
@@ -291,7 +302,7 @@ public class HttpRequest {
     this.oauth2Arguments = oauth2Arguments;
     return this;
   }
-  
+
   /**
    * @param followRedirects whether this request should automatically follow redirects.
    */
@@ -465,7 +476,7 @@ public class HttpRequest {
     return oauth2Arguments;
   }
 
-  
+
   /**
    * @return true if redirects should be followed.
    */
@@ -516,7 +527,7 @@ public class HttpRequest {
       ^ Arrays.hashCode(postBody)
       ^ headers.hashCode();
   }
-  
+
   @Override
   public boolean equals(Object obj) {
     if (obj == this) {

Modified: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java (original)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java Fri Feb 10 22:12:21 2012
@@ -19,7 +19,9 @@
 package org.apache.shindig.gadgets.servlet;
 
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 
@@ -54,20 +56,20 @@ import org.apache.shindig.gadgets.rewrit
 import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
 import org.apache.shindig.gadgets.rewrite.RewriterRegistry;
 import org.apache.shindig.gadgets.rewrite.RewritingException;
-import org.apache.shindig.gadgets.uri.UriCommon;
 import org.apache.shindig.gadgets.uri.UriCommon.Param;
 
+import com.google.common.collect.Maps;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 /**
  * Handles gadgets.io.makeRequest requests.
- * 
+ *
  * Unlike ProxyHandler, this may perform operations such as OAuth or signed fetch.
  */
 @Singleton
-public class MakeRequestHandler {
+public class MakeRequestHandler implements ContainerConfig.ConfigObserver {
   // Relaxed visibility for ease of integration. Try to avoid relying on these.
   public static final String UNPARSEABLE_CRUFT = "throw 1; < don't be evil' >";
   public static final String POST_DATA_PARAM = "postData";
@@ -79,6 +81,10 @@ public class MakeRequestHandler {
   public static final String GET_SUMMARIES_PARAM = "getSummaries";
   public static final String GET_FULL_HEADERS_PARAM = "getFullHeaders";
   public static final String AUTHZ_PARAM = "authz";
+  public static final String MAX_POST_SIZE_KEY = "gadgets.jsonProxyUrl.maxPostSize";
+  public static final String MULTI_PART_FORM_POST = "MPFP";
+  public static final String MULTI_PART_FORM_POST_IFRAME = "iframe";
+  public static final int MAX_POST_SIZE_DEFAULT = 5 * 1024 * 1024; // 5 MiB
 
   private final RequestPipeline requestPipeline;
   private final ResponseRewriterRegistry contentRewriterRegistry;
@@ -86,9 +92,11 @@ public class MakeRequestHandler {
   private final GadgetAdminStore gadgetAdminStore;
   private final Processor processor;
   private final LockedDomainService lockedDomainService;
+  private final Map<String, Integer> maxPostSizes;
 
   @Inject
   public MakeRequestHandler(
+          ContainerConfig config,
           RequestPipeline requestPipeline,
           @RewriterRegistry(rewriteFlow = RewriteFlow.DEFAULT) ResponseRewriterRegistry contentRewriterRegistry,
           Provider<FeedProcessor> feedProcessorProvider, GadgetAdminStore gadgetAdminStore,
@@ -100,6 +108,8 @@ public class MakeRequestHandler {
     this.gadgetAdminStore = gadgetAdminStore;
     this.processor = processor;
     this.lockedDomainService = lockedDomainService;
+    this.maxPostSizes = Maps.newConcurrentMap();
+    config.addConfigObserver(this, true);
   }
 
   /**
@@ -107,9 +117,9 @@ public class MakeRequestHandler {
    */
   public void fetch(HttpServletRequest request, HttpServletResponse response)
           throws GadgetException, IOException {
+
     HttpRequest rcr = buildHttpRequest(request);
     String container = rcr.getContainer();
-
     final Uri gadgetUri = rcr.getGadget();
     if (gadgetUri == null) {
       throw new GadgetException(GadgetException.Code.MISSING_PARAMETER,
@@ -118,10 +128,11 @@ public class MakeRequestHandler {
 
     Gadget gadget;
     GadgetContext context = new HttpGadgetContext(request) {
+      @Override
       public Uri getUrl() {
         return gadgetUri;
       }
-
+      @Override
       public boolean getIgnoreCache() {
         return getParameter("bypassSpecCache").equals("1");
       }
@@ -164,20 +175,29 @@ public class MakeRequestHandler {
 
     // Find and set the refresh interval
     setResponseHeaders(request, response, results);
-
     response.setStatus(HttpServletResponse.SC_OK);
-    response.setContentType("application/json");
     response.setCharacterEncoding("UTF-8");
-    response.getWriter().write(UNPARSEABLE_CRUFT + output);
+
+    PrintWriter out = response.getWriter();
+    if ("1".equals(getParameter(request, MULTI_PART_FORM_POST_IFRAME, null))) {
+      response.setContentType("text/html");
+      out.write("<html><head></head><body><textarea>");
+      out.write(UNPARSEABLE_CRUFT);
+      out.write(output);
+      out.write("</textarea></body></html>");
+    } else {
+      response.setContentType("application/json");
+      out.write(UNPARSEABLE_CRUFT + output);
+    }
   }
 
   /**
    * Generate a remote content request based on the parameters sent from the client.
-   * 
+   *
    * @throws GadgetException
    */
   protected HttpRequest buildHttpRequest(HttpServletRequest request) throws GadgetException {
-    String urlStr = request.getParameter(Param.URL.getKey());
+    String urlStr = getParameter(request, Param.URL.getKey(), null);
     if (urlStr == null) {
       throw new GadgetException(GadgetException.Code.INVALID_PARAMETER, Param.URL.getKey()
               + " parameter is missing.", HttpResponse.SC_BAD_REQUEST);
@@ -191,10 +211,38 @@ public class MakeRequestHandler {
               + Param.URL.getKey() + " parameter", HttpResponse.SC_BAD_REQUEST);
     }
 
+    SecurityToken token = AuthInfoUtil.getSecurityTokenFromRequest(request);
+    String container = null;
+    Uri gadgetUri = null;
+    if ("1".equals(getParameter(request, MULTI_PART_FORM_POST, null))) {
+      // This endpoint is being used by the proxied-form-post feature.
+      // Require a token.
+      if (token == null) {
+        throw new GadgetException(GadgetException.Code.INVALID_SECURITY_TOKEN);
+      }
+    }
+
+    // If we have a token, we should use it.
+    if (token != null && !token.isAnonymous()) {
+      container = token.getContainer();
+      String appurl = token.getAppUrl();
+      if (appurl != null) {
+        gadgetUri = Uri.parse(appurl);
+      }
+    } else {
+      container = getContainer(request);
+      String gadgetUrl = getParameter(request, Param.GADGET.getKey(), null);
+      if (gadgetUrl != null) {
+        gadgetUri = Uri.parse(gadgetUrl);
+      }
+    }
+
     HttpRequest req = new HttpRequest(url).setMethod(getParameter(request, METHOD_PARAM, "GET"))
-            .setContainer(getContainer(request));
+            .setContainer(container).setGadget(gadgetUri);
 
-    setPostData(request, req);
+    if ("POST".equals(req.getMethod())) {
+      setPostData(container, request, req);
+    }
 
     String headerData = getParameter(request, HEADERS_PARAM, "");
     if (headerData.length() > 0) {
@@ -215,28 +263,28 @@ public class MakeRequestHandler {
     // Set the default content type for post requests when a content type is not specified
     if ("POST".equals(req.getMethod()) && req.getHeader("Content-Type") == null) {
       req.addHeader("Content-Type", "application/x-www-form-urlencoded");
+    } else if ("1".equals(getParameter(request, MULTI_PART_FORM_POST, null))) {
+      // We need the entire header from the original request because it comes with a boundry value we need to reuse.
+      req.addHeader("Content-Type", request.getHeader("Content-Type"));
     }
 
-    req.setIgnoreCache("1".equals(request.getParameter(Param.NO_CACHE.getKey())));
+    req.setIgnoreCache("1".equals(getParameter(request, Param.NO_CACHE.getKey(), null)));
+
 
-    if (request.getParameter(Param.GADGET.getKey()) != null) {
-      req.setGadget(Uri.parse(request.getParameter(Param.GADGET.getKey())));
-    }
 
     // If the proxy request specifies a refresh param then we want to force the min TTL for
     // the retrieved entry in the cache regardless of the headers on the content when it
     // is fetched from the original source.
-    if (request.getParameter(Param.REFRESH.getKey()) != null) {
+    String refresh = getParameter(request, Param.REFRESH.getKey(), null);
+    if (refresh != null) {
       try {
-        req.setCacheTtl(Integer.parseInt(request.getParameter(Param.REFRESH.getKey())));
-      } catch (NumberFormatException nfe) {
-        // Ignore
-      }
+        req.setCacheTtl(Integer.parseInt(refresh));
+      } catch (NumberFormatException ignore) {}
     }
     // Allow the rewriter to use an externally forced mime type. This is needed
     // allows proper rewriting of <script src="x"/> where x is returned with
     // a content type like text/html which unfortunately happens all too often
-    req.setRewriteMimeType(request.getParameter(Param.REWRITE_MIME_TYPE.getKey()));
+    req.setRewriteMimeType(getParameter(request, Param.REWRITE_MIME_TYPE.getKey(), null));
 
     // Figure out whether authentication is required
     AuthType auth = AuthType.parse(getParameter(request, AUTHZ_PARAM, null));
@@ -259,18 +307,34 @@ public class MakeRequestHandler {
    * Set http request post data according to servlet request. It uses header encoding if available,
    * and defaulted to utf8 Override the function if different behavior is needed.
    */
-  protected void setPostData(HttpServletRequest request, HttpRequest req) throws GadgetException {
+  protected void setPostData(String container, HttpServletRequest request, HttpRequest req) throws GadgetException {
+    if (maxPostSizes.get(container) < request.getContentLength()) {
+      throw new GadgetException(GadgetException.Code.POST_TOO_LARGE, "Posted data too large.",
+          HttpResponse.SC_REQUEST_ENTITY_TOO_LARGE);
+    }
+
     String encoding = request.getCharacterEncoding();
     if (encoding == null) {
       encoding = "UTF-8";
     }
     try {
-      req.setPostBody(getParameter(request, POST_DATA_PARAM, "").getBytes(encoding.toUpperCase()));
+      String contentType = request.getHeader("Content-Type");
+      if (contentType != null && contentType.startsWith("multipart/form-data")) {
+        // TODO: This will read the entire posted response in server memory.
+        // Is there a way to stream this even with OAUTH flows?
+        req.setPostBody(request.getInputStream());
+      } else {
+        req.setPostBody(getParameter(request, POST_DATA_PARAM, "").getBytes(encoding.toUpperCase()));
+      }
     } catch (UnsupportedEncodingException e) {
       // We might consider enumerating at least a small list of encodings
       // that we must always honor. For now, we return SC_BAD_REQUEST since
       // the encoding parameter could theoretically be anything.
       throw new GadgetException(Code.HTML_PARSE_ERROR, e, HttpResponse.SC_BAD_REQUEST);
+    } catch (IOException e) {
+      // Something went wrong reading the request data.
+      // TODO: perhaps also support a max post size and enforce it by throwing and catching exceptions here.
+      throw new GadgetException(Code.INTERNAL_SERVER_ERROR, e, HttpResponse.SC_BAD_REQUEST);
     }
   }
 
@@ -281,10 +345,10 @@ public class MakeRequestHandler {
           HttpResponse results) throws GadgetException {
     boolean getFullHeaders = Boolean.parseBoolean(getParameter(request, GET_FULL_HEADERS_PARAM,
             "false"));
-    String originalUrl = request.getParameter(Param.URL.getKey());
+    String originalUrl = getParameter(request, Param.URL.getKey(), null);
     String body = results.getResponseAsString();
     if (body.length() > 0) {
-      if ("FEED".equals(request.getParameter(CONTENT_TYPE_PARAM))) {
+      if ("FEED".equals(getParameter(request, CONTENT_TYPE_PARAM, null))) {
         body = processFeed(originalUrl, request, body);
       }
     }
@@ -339,9 +403,9 @@ public class MakeRequestHandler {
    */
   @SuppressWarnings("deprecation")
   protected static String getContainer(HttpServletRequest request) {
-    String container = request.getParameter(Param.CONTAINER.getKey());
+    String container = getParameter(request, Param.CONTAINER.getKey(), null);
     if (container == null) {
-      container = request.getParameter(Param.SYND.getKey());
+      container = getParameter(request, Param.SYND.getKey(), null);
     }
     return container != null ? container : ContainerConfig.DEFAULT_CONTAINER;
   }
@@ -362,11 +426,11 @@ public class MakeRequestHandler {
           HttpServletResponse response, HttpResponse results) throws GadgetException {
     int refreshInterval = 0;
     if (results.isStrictNoCache()
-            || "1".equals(request.getParameter(UriCommon.Param.NO_CACHE.getKey()))) {
+            || "1".equals(getParameter(request, Param.NO_CACHE.getKey(), null))) {
       refreshInterval = 0;
-    } else if (request.getParameter(UriCommon.Param.REFRESH.getKey()) != null) {
+    } else if (getParameter(request, Param.REFRESH.getKey(), null) != null) {
       try {
-        refreshInterval = Integer.valueOf(request.getParameter(UriCommon.Param.REFRESH.getKey()));
+        refreshInterval = Integer.valueOf(getParameter(request, Param.REFRESH.getKey(), null));
       } catch (NumberFormatException nfe) {
         throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
                 "refresh parameter is not a number", HttpResponse.SC_BAD_REQUEST);
@@ -376,11 +440,37 @@ public class MakeRequestHandler {
     }
     HttpUtil.setCachingHeaders(response, refreshInterval, false);
 
-    // Always set Content-Disposition header as XSS prevention mechanism.
-    response.setHeader("Content-Disposition", "attachment;filename=p.txt");
+    /*
+     * The proxied-form-post feature uses this endpoint to post a form
+     * element (in order to support file upload).
+     *
+     * For cross-browser support (IE) it requires that we use a hidden iframe
+     * to post the request. Setting Content-Disposition breaks that solution.
+     * In this particular case, we will always have a security token, so we
+     * shouldn't need to be as cautious here.
+     */
+    if (!"1".equals(getParameter(request, MULTI_PART_FORM_POST, null))) {
+      // Always set Content-Disposition header as XSS prevention mechanism.
+      response.setHeader("Content-Disposition", "attachment;filename=p.txt");
+    }
 
     if (response.getContentType() == null) {
       response.setContentType("application/octet-stream");
     }
   }
+
+  public void containersChanged(ContainerConfig config, Collection<String> changed,
+      Collection<String> removed) {
+    for (String container : changed) {
+      Integer maxPostSize = config.getInt(container, MAX_POST_SIZE_KEY);
+      if (maxPostSize == null) {
+        maxPostSize = MAX_POST_SIZE_DEFAULT;
+      } else {
+        maxPostSizes.put(container, maxPostSize);
+      }
+    }
+    for (String container : removed) {
+      maxPostSizes.remove(container);
+    }
+  }
 }

Modified: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java (original)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java Fri Feb 10 22:12:21 2012
@@ -69,7 +69,7 @@ public class MakeRequestHandlerTest exte
   private static final Uri REQUEST_URL = Uri.parse("http://example.org/file");
   private static final String REQUEST_BODY = "I+am+the+request+body!foo=baz%20la";
   private static final String RESPONSE_BODY = "makeRequest response body";
-  private static final SecurityToken DUMMY_TOKEN = new FakeGadgetToken();
+  private static final FakeGadgetToken DUMMY_TOKEN = new FakeGadgetToken();
 
   private final GadgetAdminStore gadgetAdminStore = mock(GadgetAdminStore.class);
   private ContainerConfig containerConfig;
@@ -125,8 +125,10 @@ public class MakeRequestHandlerTest exte
 
     containerConfig = new JsonContainerConfig(config, Expressions.forTesting());
     ldService = new HashLockedDomainService(containerConfig, false, new HashShaLockedDomainPrefixGenerator());
-    handler = new MakeRequestHandler(pipeline, rewriterRegistry, feedProcessorProvider, gadgetAdminStore, processor, ldService);
+    handler = new MakeRequestHandler(containerConfig, pipeline, rewriterRegistry, feedProcessorProvider, gadgetAdminStore, processor, ldService);
 
+    DUMMY_TOKEN.setAppUrl("http://some/gadget.xml");
+    DUMMY_TOKEN.setContainer(ContainerConfig.DEFAULT_CONTAINER);
     expect(request.getParameter(Param.GADGET.getKey())).andReturn("http://some/gadget.xml").anyTimes();
     expect(processor.process(capture(context))).andReturn(gadget).anyTimes();
     expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(true);
@@ -440,7 +442,10 @@ public class MakeRequestHandlerTest exte
     // Doesn't actually sign since it returns the standard fetcher.
     // Signing tests are in SigningFetcherTest
     expectGetAndReturnBody(AuthType.SIGNED, RESPONSE_BODY);
-    FakeGadgetToken authToken = new FakeGadgetToken().setUpdatedToken("updated");
+    FakeGadgetToken authToken = new FakeGadgetToken()
+      .setUpdatedToken("updated")
+      .setAppUrl(DUMMY_TOKEN.getAppUrl())
+      .setContainer(DUMMY_TOKEN.getContainer());
     expect(request.getAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId()))
         .andReturn(authToken).atLeastOnce();
     expect(request.getParameter(MakeRequestHandler.AUTHZ_PARAM))
@@ -461,7 +466,10 @@ public class MakeRequestHandlerTest exte
     // Doesn't actually do oauth dance since it returns the standard fetcher.
     // OAuth tests are in OAuthRequestTest
     expectGetAndReturnBody(AuthType.OAUTH, RESPONSE_BODY);
-    FakeGadgetToken authToken = new FakeGadgetToken().setUpdatedToken("updated");
+    FakeGadgetToken authToken = new FakeGadgetToken()
+      .setUpdatedToken("updated")
+      .setAppUrl(DUMMY_TOKEN.getAppUrl())
+      .setContainer(DUMMY_TOKEN.getContainer());
     expect(request.getAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId()))
         .andReturn(authToken).atLeastOnce();
     expect(request.getParameter(MakeRequestHandler.AUTHZ_PARAM))

Modified: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java?rev=1242959&r1=1242958&r2=1242959&view=diff
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java (original)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java Fri Feb 10 22:12:21 2012
@@ -87,7 +87,7 @@ public class MakeRequestServletTest exte
     Capture<GadgetContext> context = new Capture<GadgetContext>();
     expect(processor.process(EasyMock.capture(context))).andReturn(gadget).anyTimes();
     ldService = new HashLockedDomainService(containerConfig, false, mock(LockedDomainPrefixGenerator.class));
-    handler = new MakeRequestHandler(pipeline, rewriterRegistry, feedProcessorProvider, gadgetAdminStore, processor, ldService);
+    handler = new MakeRequestHandler(containerConfig, pipeline, rewriterRegistry, feedProcessorProvider, gadgetAdminStore, processor, ldService);
 
     servlet.setMakeRequestHandler(handler);
     expect(request.getHeaderNames()).andReturn(EMPTY_ENUM).anyTimes();

Added: shindig/trunk/java/server/src/main/webapp/META-INF/MANIFEST.MF
URL: http://svn.apache.org/viewvc/shindig/trunk/java/server/src/main/webapp/META-INF/MANIFEST.MF?rev=1242959&view=auto
==============================================================================
--- shindig/trunk/java/server/src/main/webapp/META-INF/MANIFEST.MF (added)
+++ shindig/trunk/java/server/src/main/webapp/META-INF/MANIFEST.MF Fri Feb 10 22:12:21 2012
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+

Propchange: shindig/trunk/java/server/src/main/webapp/META-INF/MANIFEST.MF
------------------------------------------------------------------------------
    svn:mime-type = text/plain