diff --git a/src/jio.storage/xwikistorage.js b/src/jio.storage/xwikistorage.js
index cf74821873acf7cf589ba529a1c10825195f1dd0..bb2428c75a666703366e4dee13c674dc983a5d97 100644
--- a/src/jio.storage/xwikistorage.js
+++ b/src/jio.storage/xwikistorage.js
@@ -1,341 +1,641 @@
-/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
-/*global toSend: true, jIO: true, jQuery: true, btoa: true */
+/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true, vars: true */
+/*global
+    jIO: true,
+    $: true,
+    XMLHttpRequest: true,
+    Blob: true,
+    FormData: true,
+    window: true
+*/
+/**
+ * JIO XWiki Storage. Type = 'xwiki'.
+ * XWiki Document/Attachment storage.
+ */
+jIO.addStorageType('xwiki', function (spec, my) {
+
+  spec = spec || {};
+  var that, priv, xwikistorage;
+  that = my.basicStorage(spec, my);
+  priv = {};
 
   /**
-   * JIO XWiki based storage. Type = 'xwiki'.
-   * Edits XWiki documents as html using html editor.
-   * Test this code using the following inputs:
+   * Get the Space and Page components of a documkent ID.
    *
-{"type":"xwiki","username":"Admin","password":"admin","xwikiurl":"http://127.0.0
-.1:8080/xwiki","space":"OfficeJS"}
+   * @param id the document id.
+   * @return a map of { 'space':<Space>, 'page':<Page> }
    */
-
-(function ($) {
-
-  var newXWikiStorage = function (spec, my) {
-    var that, priv, escapeDocId, restoreDocId,
-      doWithFormToken, getDates, super_serialized;
-
-    /** The input configuration. */
-    spec = spec || {};
-
-    /** The "public" object which will have methods called on it. */
-    that = my.basicStorage(spec, my);
-
-    /** "private" fields. */
-    priv = {
-      username: spec.username || '',
-      password: spec.password || '',
-      xwikiurl: spec.xwikiurl || '',
-      space: spec.space || ''
+  var getParts = function (id) {
+    if (id.indexOf('/') === -1) {
+      return {
+        space: 'Main',
+        page: id
+      };
+    }
+    return {
+      space: id.substring(0, id.indexOf('/')),
+      page: id.substring(id.indexOf('/') + 1)
     };
+  };
 
-    //--------------------- Private Functions ---------------------//
-    /** Escape a document ID by URL escaping all '/' characters. */
-    escapeDocId = function (docId) {
-      // jslint: replaced "." with [\w\W]
-      return docId.replace(/[\w\W]html$/, '').split('/').join('%2F');
-    };
+  /**
+   * Get the Anti-CSRF token and do something with it.
+   *
+   * @param andThen function which is called with (formToken, err)
+   *                as parameters.
+   */
+  var doWithFormToken = function (andThen) {
+    $.ajax({
+      url: priv.formTokenPath,
+      type: "GET",
+      async: true,
+      dataType: 'text',
+      success: function (html) {
+        // this is unreliable
+        //var token = $('meta[name=form_token]', html).attr("content");
+        var m = html.match(/<meta name="form_token" content="(\w*)"\/>/);
+        var token = (m && m[1]) || null;
+        if (!token) {
+          andThen(null, {
+            "status": 404,
+            "statusText": "Not Found",
+            "error": "err_form_token_not_found",
+            "message": "Anti-CSRF form token was not found in page",
+            "reason": "XWiki main page did not contain expected " +
+                      "Anti-CSRF form token"
+          });
+        } else {
+          andThen(token, null);
+        }
+      },
+      error: function (jqxhr, err, cause) {
+        andThen(null, {
+          "status": jqxhr.status,
+          "statusText": jqxhr.statusText,
+          "error": err,
+          "message": "Could not get Anti-CSRF form token from [" +
+              priv.xwikiurl + "]",
+          "reason": cause
+        });
+      },
+    });
+  };
 
-    /** Restore a document id from the escaped form. */
-    restoreDocId = function (escapedDocId) {
-      return escapedDocId.split('%2F').join('/') + '.html';
-    };
+  /**
+   * Get the REST read URL for a document.
+   *
+   * @param docId the id of the document.
+   * @return the REST URL for accessing this document.
+   */
+  var getDocRestURL = function (docId) {
+    var parts = getParts(docId);
+    return priv.xwikiurl + '/rest/wikis/'
+      + priv.wiki + '/spaces/' + parts.space + '/pages/' + parts.page;
+  };
 
+  /*
+   * Wrapper for the xwikistorage based on localstorage JiO store.
+   */
+  xwikistorage = {
     /**
-     * Get the Anti-CSRF token and do something with it.
+     * Get content of an XWikiDocument.
      *
-     * @param docId document id of document which you have permission to edit.
-     * @param whatToDo function which is called with form token as parameter.
+     * @param docId the document ID.
+     * @param andThen a callback taking (doc, err), doc being the document
+     *                json object and err being the error if any.
      */
-    doWithFormToken = function (docId, whatToDo) {
-      var url = priv.xwikiurl + '/bin/edit/' + priv.space + '/' +
-        escapeDocId(docId) + '?editor=wiki&cachebuster=' + Date.now();
+    getItem: function (docId, andThen) {
       $.ajax({
-        url: url,
+        url: getDocRestURL(docId),
         type: "GET",
         async: true,
-        dataType: 'text',
-        headers: {
-          'Authorization': 'Basic ' + btoa(priv.username + ':' +
-            priv.password)
+        dataType: 'xml',
+        success: function (xmlData) {
+          var out = {};
+          var xd = $(xmlData);
+          xd.find('modified').each(function () {
+            out._last_modified = Date.parse($(this).text());
+          });
+          xd.find('created').each(function () {
+            out._creation_date = Date.parse($(this).text());
+          });
+          xd.find('title').each(function () { out.title = $(this).text(); });
+          xd.find('parent').each(function () { out.parent = $(this).text(); });
+          xd.find('syntax').each(function () { out.syntax = $(this).text(); });
+          xd.find('content').each(function () {
+            out.content = $(this).text();
+          });
+          out._id = docId;
+
+          andThen(out, null);
         },
-        success: function (html) {
-          whatToDo($(html).find('input[name=form_token]').attr('value'));
+        error: function (jqxhr, err, cause) {
+          if (jqxhr.status === 404) {
+            andThen(null, null);
+            return;
+          }
+          andThen(null, {
+            "status": jqxhr.status,
+            "statusText": jqxhr.statusText,
+            "error": err,
+            "message": "Failed to get document [" + docId + "]",
+            "reason": cause
+          });
         }
       });
-    };
+    },
+
     /**
-     * Get the creation and modification dates for a page.
+     * Get content of an XWikiAttachment.
      *
-     * @param docId the ID of the document.
-     * @param callWhenDone callback, will be called when function finishes.
+     * @param attachId the attachment ID.
+     * @param andThen a callback taking (attach, err), attach being the
+     *                attachment blob and err being the error if any.
      */
-    getDates = function (docId, callWhenDone) {
-    // http://127.0.0.1:8080/xwiki/rest/wikis/xwiki/
-    //  spaces/Main/pages/<pageName>
-      var map = {};
-      $.ajax({
-        url: priv.xwikiurl + '/rest/wikis/' + 'xwiki' + '/spaces/' + priv.space
-          + '/pages/' + escapeDocId(docId) + '?cachebuster=' + Date.now(),
-        type: "GET",
-        async: true,
-        dataType: 'xml',
-        headers: {
-          'Authorization': 'Basic ' + btoa(priv.username + ':' +
-            priv.password)
-        },
-        success: function (xmlData) {
-          $(xmlData).find('modified').each(function () {
-            map._last_modified = Date.parse($(this).text());
-          });
-          $(xmlData).find('created').each(function () {
-            map._creation_date = Date.parse($(this).text());
+    getAttachment: function (docId, fileName, andThen) {
+      // need to do this manually, jquery doesn't support returning blobs.
+      var xhr = new XMLHttpRequest();
+      var parts = getParts(docId);
+      var url = priv.xwikiurl + '/bin/download/' + parts.space +
+          "/" + parts.page + "/" + fileName;
+      xhr.open('GET', url, true);
+      xhr.responseType = 'blob';
+
+      xhr.onload = function (e) {
+        if (xhr.status === 200) {
+          var contentType = xhr.getResponseHeader("Content-Type");
+          if (contentType.indexOf(';') > -1) {
+            contentType = contentType.substring(0, contentType.indexOf(';'));
+          }
+          var blob = new Blob([xhr.response], {type: contentType});
+          andThen(blob);
+        } else {
+          andThen(null, {
+            "status": xhr.status,
+            "statusText": xhr.statusText,
+            "error": "err_network_error",
+            "message": "Failed to get attachment ["
+                + docId + "/" + fileName + "]",
+            "reason": "Error getting data from network"
           });
-          callWhenDone();
         }
-      });
-      return map;
-    };
+      };
+
+      xhr.send();
+    },
 
-    //--------------------- Public Functions ---------------------//
-    /** Get a serialized form of the module state. */
-    super_serialized = that.serialized;
-    that.serialized = function () {
-      var o = super_serialized(), key;
-      for (key in priv) {
-        if (priv.hasOwnProperty(key)) {
-          o[key] = priv[key];
-        }
-      }
-      return o;
-    };
-    /** Check that the storage module is properly setup. */
-    that.validateState = function () {
-      var key;
-      for (key in priv) {
-        if (priv.hasOwnProperty(key) && !priv[key]) {
-          return 'Must specify "' + key + '".';
-        }
-      }
-      return '';
-    };
-    /** Alias to put() */
-    that.post = function (command) {
-      that.put(command);
-    };
     /**
-     * Saves a document as an XWikiDocument.
+     * Store an XWikiDocument.
      *
-     * @param command must contain document ID and document content.
+     * @param id the document identifier.
+     * @param doc the document JSON object containing
+     *            "parent", "title", "content", and/or "syntax" keys.
+     * @param andThen a callback taking (err), err being the error if any.
      */
-    that.put = function (command) {
-      doWithFormToken(command.getDocId(), function (formToken) {
-        if (!formToken) {
-          throw new Error("missing form token");
+    setItem: function (id, doc, andThen) {
+      doWithFormToken(function (formToken, err) {
+        if (err) {
+          that.error(err);
+          return;
         }
+        var parts = getParts(id);
         $.ajax({
-          url: priv.xwikiurl + '/bin/preview/' + priv.space + '/' +
-            escapeDocId(command.getDocId()),
+          url: priv.xwikiurl + "/bin/preview/" + parts.space + '/' + parts.page,
           type: "POST",
           async: true,
           dataType: 'text',
-          headers: {
-            'Authorization': 'Basic ' + btoa(priv.username + ':' +
-              priv.password)
-          },
           data: {
-            parent: '',
-            title: '',
+            parent: doc.parent || '',
+            title: doc.title || '',
             xredirect: '',
             language: 'en',
-            RequiresHTMLConversion: 'content',
-            content_syntax: 'xwiki/2.1',
-            content: command.getDocContent(),
+//            RequiresHTMLConversion: 'content',
+//            content_syntax: doc.syntax || 'xwiki/2.1',
+            content: doc.content || '',
             xeditaction: 'edit',
-            comment: 'Saved by OfficeJS',
+            comment: 'Saved by JiO',
             action_saveandcontinue: 'Save & Continue',
-            syntaxId: 'xwiki/2.1',
+            syntaxId: doc.syntax || 'xwiki/2.1',
             xhidden: 0,
             minorEdit: 0,
             ajax: true,
             form_token: formToken
           },
           success: function () {
-            that.success({
-              ok: true,
-              id: command.getDocId()
+            andThen(null);
+          },
+          error: function (jqxhr, err, cause) {
+            andThen({
+              "status": jqxhr.status,
+              "statusText": jqxhr.statusText,
+              "error": err,
+              "message": "Failed to store document [" + id + "]",
+              "reason": cause
             });
           }
         });
       });
-    }; // end put
+    },
+
     /**
-     * Loads a document from the XWiki storage.
+     * Store an XWikiAttachment.
+     *
+     * @param docId the ID of the document to attach to.
+     * @param fileName the attachment file name.
+     * @param mimeType the MIME type of the attachment content.
+     * @param content the attachment content.
+     * @param andThen a callback taking one parameter which is the error if any.
      */
-    that.get = function (command) {
-      // /bin/view/Main/WebHomee?xpage=plain
-      /**
-       * Protocol specification:
-       * {
-       *     "_id": "somePage",
-       *     "content": "aoeu",
-       *     "_creation_date": 1348154789478,
-       *     "_last_modified": 1348154789478
-       * }
-       */
-      var doc,
-        pendingRequests = 2,
-        finishedRequest = function () {
-          pendingRequests -= 1;
-          if (pendingRequests < 1) {
-            that.success(doc);
+    setAttachment: function (docId, fileName, mimeType, content, andThen) {
+      doWithFormToken(function (formToken, err) {
+        if (err) {
+          that.error(err);
+          return;
+        }
+        var parts = getParts(docId);
+        var blob = (content.constructor === "function Blob() { [native code] }")
+            ? content : new Blob([content], {type: mimeType});
+        var fd = new FormData();
+        fd.append("filepath", blob, fileName);
+        fd.append("form_token", formToken);
+        var xhr = new XMLHttpRequest();
+        xhr.open('POST', priv.xwikiurl + "/bin/upload/" +
+                         parts.space + '/' + parts.page, true);
+        xhr.onload = function (e) {
+          if (xhr.status === 302 || xhr.status === 200) {
+            andThen(null);
+          } else {
+            andThen({
+              "status": xhr.status,
+              "statusText": xhr.statusText,
+              "error": "err_network_error",
+              "message": "Failed to store attachment ["
+                  + docId + "/" + fileName + "]",
+              "reason": "Error posting data"
+            });
           }
         };
-      doc = (function () {
-        var resultMap = getDates(command.getDocId(), finishedRequest);
+        xhr.send(fd);
+      });
+    },
+
+    removeItem: function (id, andThen) {
+      doWithFormToken(function (formToken, err) {
+        if (err) {
+          that.error(err);
+          return;
+        }
+        var parts = getParts(id);
         $.ajax({
-          url: priv.xwikiurl + '/bin/get/' + priv.space + '/' +
-            escapeDocId(command.getDocId()) + '?xpage=plain&cachebuster=' +
-            Date.now(),
-          type: "GET",
+          url: priv.xwikiurl + "/bin/delete/" + parts.space + '/' + parts.page,
+          type: "POST",
           async: true,
           dataType: 'text',
-          headers: {
-            'Authorization': 'Basic ' + btoa(priv.username + ':' +
-              priv.password)
+          data: {
+            confirm: '1',
+            form_token: formToken
+          },
+          success: function () {
+            andThen(null);
           },
-          success: function (html) {
-            resultMap.content = html;
-            finishedRequest();
+          error: function (jqxhr, err, cause) {
+            andThen({
+              "status": jqxhr.status,
+              "statusText": jqxhr.statusText,
+              "error": err,
+              "message": "Failed to delete document [" + id + "]",
+              "reason": cause
+            });
           }
         });
-        return resultMap;
-      }());
-      doc._id = command.getDocId();
-    }; // end get
+      });
+    },
 
-    /**
-     * Gets a document list from the xwiki storage.
-     * It will retreive an array containing files meta data owned by
-     * the user.
-     * @method allDocs
-     */
-    that.allDocs = function (command) {
-      // http://127.0.0.1:8080/xwiki/rest/wikis/xwiki/spaces/Main/pages
-      $.ajax({
-        url: priv.xwikiurl + '/rest/wikis/' + 'xwiki' + '/spaces/' +
-          priv.space + '/pages?cachebuster=' + Date.now(),
-        type: "GET",
-        async: true,
-        dataType: 'xml',
-        headers: {
-          'Authorization': 'Basic ' + btoa(priv.username + ':' +
-            priv.password)
-        },
-        success: function (xmlData) {
-          /** Protocol definition:
-           *  {
-           *    "total_rows":2,
-           *    "rows":[{
-           *        "id":"b",
-           *        "key":"b",
-           *        "value":{
-           *          "content":"aoeu",
-           *          "_creation_date":1348154789478,
-           *          "_last_modified":1348154789478
-           *        }
-           *      },
-           *      {
-           *        "id":"oeau",
-           *        "key":"oeau",
-           *        "value"{
-           *          "content":"oeu",
-           *          "_creation_date":1348154834680,
-           *          "_last_modified":1348154834680
-           *        }
-           *      }
-           *    ]
-           *  }
-           */
-          var totalRows = 0,
-            data = [],
-            // The number of async calls which are waiting to return.
-            outstandingCalls = 0,
-            toSend;
-          $(xmlData).find('name').each(function () {
-            outstandingCalls += 1;
-            var id = restoreDocId($(this).text()),
-              entry = {
-                'id': id,
-                'key': id,
-                'value': getDates(id, function () {
-                  outstandingCalls -= 1;
-                  if (outstandingCalls < 1) {
-                    that.success(toSend);
-                  }
-                })
-              };
-            data[totalRows += 1] = entry;
-          });
-          toSend = {
-            'total_rows': totalRows,
-            'rows': data
-          };
-          /* TODO: Include the content if requested.
-                if (!command.getOption('metadata_only')) {
-                    getContent();
-                } else {
-                    that.success(toSend);
-                }
-                */
-        },
-        error: function (type) {
-          if (type.status === 404) {
-            type.message = 'Cannot find "' + command.getDocId() +
-              '"informations.';
-            type.reason = 'missing';
-            that.error(type);
-          } else {
-            type.reason = 'Cannot get "' + command.getDocId() +
-              '"informations';
-            type.message = type.reason + '.';
-            that.retry(type);
-          }
+    removeAttachment: function (docId, fileName, andThen) {
+      var parts = getParts(docId);
+      doWithFormToken(function (formToken, err) {
+        if (err) {
+          that.error(err);
+          return;
         }
-      });
-    };
-    /**
-     * Removes a document from the XWiki storage.
-     */
-    that.remove = function (command) {
-    // http://127.0.0.1:8080/xwiki/bin/delete/Main/WebHomee?
-    // confirm=1&form_token= //r7x0oGBSk2EFm2fxVULfFA
-      doWithFormToken(command.getDocId(), function (formToken) {
         $.ajax({
-          url: priv.xwikiurl + '/bin/delete/' + priv.space + '/' +
-            escapeDocId(command.getDocId()),
+          url: priv.xwikiurl + "/bin/delattachment/" + parts.space + '/' +
+              parts.page + '/' + fileName,
           type: "POST",
           async: true,
           dataType: 'text',
-          headers: {
-            'Authorization': 'Basic ' + btoa(priv.username + ':' +
-              priv.password)
-          },
           data: {
-            confirm: 1,
+            ajax: '1',
             form_token: formToken
           },
           success: function () {
-            that.success({
-              ok: true,
-              id: command.getDocId()
+            andThen(null);
+          },
+          error: function (jqxhr, err, cause) {
+            andThen({
+              "status": jqxhr.status,
+              "statusText": jqxhr.statusText,
+              "error": err,
+              "message": "Failed to delete attachment ["
+                  + docId + '/' + fileName + "]",
+              "reason": cause
             });
           }
         });
       });
-    }; // end remove
-    return that;
+    }
+  };
+
+  // ==================== Tools ====================
+  /**
+   * Update [doc] the document object and remove [doc] keys
+   * which are not in [new_doc]. It only changes [doc] keys not starting
+   * with an underscore.
+   * ex: doc:     {key:value1,_key:value2} with
+   *     new_doc: {key:value3,_key:value4} updates
+   *     doc:     {key:value3,_key:value2}.
+   * @param  {object} doc The original document object.
+   * @param  {object} new_doc The new document object
+   */
+  priv.documentObjectUpdate = function (doc, new_doc) {
+    var k;
+    for (k in doc) {
+      if (doc.hasOwnProperty(k)) {
+        if (k[0] !== '_') {
+          delete doc[k];
+        }
+      }
+    }
+    for (k in new_doc) {
+      if (new_doc.hasOwnProperty(k)) {
+        if (k[0] !== '_') {
+          doc[k] = new_doc[k];
+        }
+      }
+    }
+  };
+
+  /**
+   * Checks if an object has no enumerable keys
+   * @method objectIsEmpty
+   * @param  {object} obj The object
+   * @return {boolean} true if empty, else false
+   */
+  priv.objectIsEmpty = function (obj) {
+    var k;
+    for (k in obj) {
+      if (obj.hasOwnProperty(k)) {
+        return false;
+      }
+    }
+    return true;
+  };
+
+  // ==================== attributes ====================
+  // the wiki to store stuff in
+  priv.wiki = spec.wiki || 'xwiki';
+
+  // unused
+  priv.username = spec.username;
+  priv.language = spec.language;
+
+  // URL location of the wiki, unused since
+  // XWiki doesn't currently allow cross-domain requests.
+  priv.xwikiurl = spec.xwikiurl ||
+     window.location.href.replace(/\/xwiki\/bin\//, '/xwiki\n').split('\n')[0];
+  // should be: s@/xwiki/bin/.*$@/xwiki@
+  // but jslint gets in the way.
+
+  // Which URL to load for getting the Anti-CSRF form token, used for testing.
+  priv.formTokenPath = spec.formTokenPath || priv.xwikiurl;
+
+
+  that.specToStore = function () {
+    return {
+      "username": priv.username,
+      "language": priv.language,
+      "xwikiurl": priv.xwikiurl,
+    };
+  };
+
+  // can't fo wrong since no parameters are required.
+  that.validateState = function () {
+    return '';
+  };
+
+  // ==================== commands ====================
+  /**
+   * Create a document in local storage.
+   * @method post
+   * @param  {object} command The JIO command
+   */
+  that.post = function (command) {
+    var docId = command.getDocId();
+    if (!(typeof docId === "string" && docId !== "")) {
+      setTimeout(function () {
+        that.error({
+          "status": 405,
+          "statusText": "Method Not Allowed",
+          "error": "method_not_allowed",
+          "message": "Cannot create document which id is undefined",
+          "reason": "Document id is undefined"
+        });
+      });
+      return;
+    }
+    xwikistorage.getItem(docId, function (doc, err) {
+      if (err) {
+        that.error(err);
+      } else if (doc === null) {
+        // the document does not exist
+        xwikistorage.setItem(command.getDocId(),
+                             command.cloneDoc(),
+                             function (err) {
+            if (err) {
+              that.error(err);
+            } else {
+              that.success({
+                "ok": true,
+                "id": command.getDocId()
+              });
+            }
+          });
+      } else {
+        // the document already exists
+        that.error({
+          "status": 409,
+          "statusText": "Conflicts",
+          "error": "conflicts",
+          "message": "Cannot create a new document",
+          "reason": "Document already exists (use 'put' to modify it)"
+        });
+      }
+    });
   };
-  jIO.addStorageType('xwiki', newXWikiStorage);
-}(jQuery));
+
+  /**
+   * Create or update a document in local storage.
+   * @method put
+   * @param  {object} command The JIO command
+   */
+  that.put = function (command) {
+    xwikistorage.getItem(command.getDocId(), function (doc, err) {
+      if (err) {
+        that.error(err);
+      } else if (doc === null) {
+        doc = command.cloneDoc();
+      } else {
+        priv.documentObjectUpdate(doc, command.cloneDoc());
+      }
+      // write
+      xwikistorage.setItem(command.getDocId(), doc, function (err) {
+        if (err) {
+          that.error(err);
+        } else {
+          that.success({
+            "ok": true,
+            "id": command.getDocId()
+          });
+        }
+      });
+    });
+  };
+
+  /**
+   * Add an attachment to a document
+   * @method  putAttachment
+   * @param  {object} command The JIO command
+   */
+  that.putAttachment = function (command) {
+    xwikistorage.getItem(command.getDocId(), function (doc, err) {
+      if (err) {
+        that.error(err);
+      } else if (doc === null) {
+        //  the document does not exist
+        that.error({
+          "status": 404,
+          "statusText": "Not Found",
+          "error": "not_found",
+          "message": "Impossible to add attachment",
+          "reason": "Document not found"
+        });
+      } else {
+        // Document exists, upload attachment.
+        xwikistorage.setAttachment(command.getDocId(),
+                                   command.getAttachmentId(),
+                                   command.getAttachmentMimeType(),
+                                   command.getAttachmentData(),
+                                   function (err) {
+            if (err) {
+              that.error(err);
+            } else {
+              that.success({
+                "ok": true,
+                "id": command.getDocId() + "/" + command.getAttachmentId()
+              });
+            }
+          });
+      }
+    });
+  };
+
+  /**
+   * Get a document or attachment
+   * @method get
+   * @param  {object} command The JIO command
+   */
+  that.get = that.getAttachment = function (command) {
+    if (typeof command.getAttachmentId() === "string") {
+      // seeking for an attachment
+      xwikistorage.getAttachment(command.getDocId(),
+                                 command.getAttachmentId(),
+                                 function (attach, err) {
+          if (err) {
+            that.error(err);
+          } else if (attach !== null) {
+            that.success(attach);
+          } else {
+            that.error({
+              "status": 404,
+              "statusText": "Not Found",
+              "error": "not_found",
+              "message": "Cannot find the attachment",
+              "reason": "Attachment does not exist"
+            });
+          }
+        });
+    } else {
+      // seeking for a document
+      xwikistorage.getItem(command.getDocId(), function (doc, err) {
+        if (err) {
+          that.error(err);
+        } else if (doc !== null) {
+          that.success(doc);
+        } else {
+          that.error({
+            "status": 404,
+            "statusText": "Not Found",
+            "error": "not_found",
+            "message": "Cannot find the document",
+            "reason": "Document does not exist"
+          });
+        }
+      });
+    }
+  };
+
+  /**
+   * Remove a document or attachment
+   * @method remove
+   * @param  {object} command The JIO command
+   */
+  that.remove = that.removeAttachment = function (command) {
+    var notFoundError = function (word) {
+      that.error({
+        "status": 404,
+        "statusText": "Not Found",
+        "error": "not_found",
+        "message": word + " not found",
+        "reason": "missing"
+      });
+    };
+
+    var objId = command.getDocId();
+    var complete = function (err) {
+      if (err) {
+        that.error(err);
+      } else {
+        that.success({
+          "ok": true,
+          "id": objId
+        });
+      }
+    };
+    if (typeof command.getAttachmentId() === "string") {
+      objId += '/' + command.getAttachmentId();
+      xwikistorage.removeAttachment(command.getDocId(),
+                                    command.getAttachmentId(),
+                                    complete);
+    } else {
+      xwikistorage.removeItem(objId, complete);
+    }
+  };
+
+  /**
+   * Get all filenames belonging to a user from the document index
+   * @method allDocs
+   * @param  {object} command The JIO command
+   */
+  that.allDocs = function () {
+    setTimeout(function () {
+      that.error({
+        "status": 405,
+        "statusText": "Method Not Allowed",
+        "error": "method_not_allowed",
+        "message": "Your are not allowed to use this command",
+        "reason": "xwikistorage forbids AllDocs command executions"
+      });
+    });
+  };
+
+  return that;
+});
diff --git a/test/jiotests.js b/test/jiotests.js
index 47d6a5117bdbd4c37eff39037d0e7a7f55d41445..f7ca0d051b0f25eb77327e21f9792cc23ae2a328 100644
--- a/test/jiotests.js
+++ b/test/jiotests.js
@@ -164,7 +164,8 @@ generateTools = function (test_namespace) {
     var o = {};
 
     o.t = test_namespace;
-    o.clock = sinon.useFakeTimers();
+    o.server = o.t.sandbox.server;
+    o.clock = o.t.sandbox.clock;
     o.clock.tick(base_tick);
     o.spy = basicSpyFunction;
     o.tick = basicTickFunction;
@@ -239,10 +240,8 @@ generateTools = function (test_namespace) {
       switch (type) {
         case "dav":
           return 'https:\\/\\/ca-davstorage:8080\\/' + path + '(\\?.*|$)';
-          break;
-        case "s3":
+        default:
           return path;
-          break;
       }
     };
     o.addFakeServerResponse = function (type, method, path, status, response) {
@@ -6281,6 +6280,442 @@ test ('Get revision List', function () {
     o.jio.stop();
 });
 */
+
+;(function() {
+// These tests will only run if we are running the suite inside of XWiki.
+module ('Jio XWikiStorage');
+var setUp = function(that, liveTest) {
+    var o = generateTools(that);
+    o.server = sinon.fakeServer.create();
+    o.jio = JIO.newJio({type:'xwiki',formTokenPath:'form_token'});
+    o.addFakeServerResponse("xwiki", "GET", "form_token", 200,
+                            '<meta name="form_token" content="OMGHAX"/>');
+    o._addFakeServerResponse = o.addFakeServerResponse;
+    o.expectedRequests = [];
+    o.addFakeServerResponse = function(a,b,c,d,e) {
+        o._addFakeServerResponse(a,b,c,d,e);
+        o.expectedRequests.push([b,c]);
+    };
+    o.assertReqs = function(count, message) {
+        o.requests = (o.requests || 0) + count;
+        ok(o.server.requests.length === o.requests,
+           message + "[expected [" + count + "] got [" +
+              (o.server.requests.length - (o.requests - count)) + "]]");
+        for (var i = 1; i <= count; i++) {
+            var req = o.server.requests[o.server.requests.length - i];
+            if (!req) {
+                break;
+            }
+            for (var j = o.expectedRequests.length - 1; j >= 0; --j) {
+                var expected = o.expectedRequests[j];
+                if (req.method === expected[0] &&
+                    req.url.indexOf(expected[1]) !== 0)
+                {
+                    o.expectedRequests.splice(j, 1);
+                }
+            }
+        }
+        var ex = o.expectedRequests.pop();
+        if (ex) {
+            ok(0, "expected [" +  ex[0] + "] request for [" + ex[1] + "]");
+        }
+    };
+    return o;
+};
+
+test ("Post", function () {
+
+    var o = setUp(this);
+
+    // post without id
+    o.spy (o, "status", 405, "Post without id");
+    o.jio.post({}, o.f);
+    o.clock.tick(5000);
+    o.assertReqs(0, "no id -> no request");
+
+    // post non empty document
+    o.addFakeServerResponse("xwiki", "POST", "myFile", 201, "HTML RESPONSE");
+    o.spy(o, "value", {"id": "myFile", "ok": true},
+          "Create = POST non empty document");
+    o.jio.post({"_id": "myFile", "title": "hello there"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(3, "put -> 1 request to get csrf token, 1 to get doc and 1 to post data");
+
+    // post but document already exists (post = error!, put = ok)
+    o.answer = JSON.stringify({"_id": "myFile", "title": "hello there"});
+    o.addFakeServerResponse("xwiki", "GET", "myFile", 200, o.answer);
+    o.spy (o, "status", 409, "Post but document already exists");
+    o.jio.post({"_id": "myFile", "title": "hello again"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(1, "post w/ existing doc -> 1 request to get doc then fail");
+
+    o.jio.stop();
+});
+
+test ("Put", function(){
+
+    var o = setUp(this);
+
+    // put without id => id required
+    o.spy (o, "status", 20, "Put without id");
+    o.jio.put({}, o.f);
+    o.clock.tick(5000);
+    o.assertReqs(0, "put w/o id -> 0 requests");
+
+    // put non empty document
+    o.addFakeServerResponse("xwiki", "POST", "put1", 201, "HTML RESPONSE");
+    o.spy (o, "value", {"ok": true, "id": "put1"},
+           "Create = PUT non empty document");
+    o.jio.put({"_id": "put1", "title": "myPut1"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(3, "put normal doc -> 1 req to get doc, 1 for csrf token, 1 to post");
+
+    // put but document already exists = update
+    o.answer = JSON.stringify({"_id": "put1", "title": "myPut1"});
+    o.addFakeServerResponse("xwiki", "GET", "put1", 200, o.answer);
+    o.addFakeServerResponse("xwiki", "POST", "put1", 201, "HTML RESPONSE");
+    o.spy (o, "value", {"ok": true, "id": "put1"}, "Updated the document");
+    o.jio.put({"_id": "put1", "title": "myPut2abcdedg"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(3, "put normal doc -> 1 req to get doc, 1 for csrf token, 1 to post");
+
+    o.jio.stop();
+});
+
+test ("PutAttachment", function(){
+
+    var o = setUp(this);
+
+    // putAttachment without doc id => id required
+    o.spy(o, "status", 20, "PutAttachment without doc id");
+    o.jio.putAttachment({}, o.f);
+    o.clock.tick(5000);
+    o.assertReqs(0, "put attach w/o doc id -> 0 requests");
+
+    // putAttachment without attachment id => attachment id required
+    o.spy(o, "status", 22, "PutAttachment without attachment id");
+    o.jio.putAttachment({"_id": "putattmt1"}, o.f);
+    o.clock.tick(5000);
+    o.assertReqs(0, "put attach w/o attach id -> 0 requests");
+
+    // putAttachment without underlying document => not found
+    o.addFakeServerResponse("xwiki", "GET", "putattmtx", 404, "HTML RESPONSE");
+    o.spy(o, "status", 404, "PutAttachment without document");
+    o.jio.putAttachment({"_id": "putattmtx", "_attachment": "putattmt2"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(1, "put attach w/o existing document -> 1 request to get doc");
+
+    // putAttachment with document without data
+    o.answer = JSON.stringify({"_id": "putattmt1", "title": "myPutAttm1"});
+    o.addFakeServerResponse("xwiki", "GET", "putattmt1", 200, o.answer);
+    o.addFakeServerResponse("xwiki", "POST", "putattmt1/putattmt2", 201,"HTML"+
+      + "RESPONSE");
+    o.spy(o, "value", {"ok": true, "id": "putattmt1/putattmt2"},
+          "PutAttachment with document, without data");
+    o.jio.putAttachment({"_id": "putattmt1", "_attachment": "putattmt2"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(3, "put attach -> 1 request to get document, 1 to put " +
+                    "attach, 1 to get csrf token");
+
+    o.jio.stop();
+});
+
+test ("Get", function(){
+
+    var o = setUp(this);
+
+    // get inexistent document
+    o.spy(o, "status", 404, "Get non existing document");
+    o.jio.get("get1", o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(1, "try to get nonexistent doc -> 1 request");
+
+    // get inexistent attachment
+    o.spy(o, "status", 404, "Get non existing attachment");
+    o.jio.get("get1/get2", o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(1, "try to get nonexistent attach -> 1 request");
+
+    // get document
+    o.answer = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
+        '<page xmlns="http://www.xwiki.org"><title>some title</title></page>';
+    o.addFakeServerResponse("xwiki", "GET", "get3", 200, o.answer);
+    o.spy(o, "value", {"_id": "get3", "title": "some title"}, "Get document");
+    o.jio.get("get3", o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(1, "get document -> 1 request");
+
+    // get inexistent attachment (document exists)
+    o.spy(o, "status", 404, "Get non existing attachment (doc exists)");
+    o.jio.get({"_id": "get3", "_attachment": "getx"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(1, "get nonexistant attachment -> 1 request");
+
+    // get attachment
+    o.answer = JSON.stringify({"_id": "get4", "title": "some attachment"});
+    o.addFakeServerResponse("xwiki", "GET", "get3/get4", 200, o.answer);
+    o.spy(o, "value", {"_id": "get4", "title": "some attachment"},
+      "Get attachment");
+    o.jio.get({"_id": "get3", "_attachment": "get4"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(1, "get attachment -> 1 request");
+
+    o.jio.stop();
+});
+
+test ("Remove", function(){
+
+    var o = setUp(this);
+
+    // remove inexistent document
+    o.addFakeServerResponse("xwiki", "GET", "remove1", 404, "HTML RESPONSE");
+    o.spy(o, "status", 404, "Remove non existening document");
+    o.jio.remove({"_id": "remove1"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(2, "remove nonexistent doc -> 1 request for csrf and 1 for doc");
+
+    // remove inexistent document/attachment
+    o.addFakeServerResponse("xwiki", "GET", "remove1/remove2", 404, "HTML" +
+      "RESPONSE");
+    o.spy(o, "status", 404, "Remove inexistent document/attachment");
+    o.jio.removeAttachment({"_id": "remove1", "_attachment": "remove2"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(2, "remove nonexistant attach -> 1 request for csrf and 1 for doc");
+
+    // remove document
+    o.answer = JSON.stringify({"_id": "remove3", "title": "some doc"});
+    //o.addFakeServerResponse("xwiki", "GET", "remove3", 200, o.answer);
+    o.addFakeServerResponse("xwiki", "POST", "bin/delete/Main/remove3",
+                            200, "HTML RESPONSE");
+    o.spy(o, "value", {"ok": true, "id": "remove3"}, "Remove document");
+    o.jio.remove({"_id": "remove3"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(2, "remove document -> 1 request for csrf and 1 for deleting doc");
+
+    o.answer = JSON.stringify({
+      "_id": "remove4",
+      "title": "some doc",
+      "_attachments": {
+            "remove5": {
+                "length": 4,
+                "digest": "md5-d41d8cd98f00b204e9800998ecf8427e"
+            }
+      }
+    });
+    // remove attachment
+    o.addFakeServerResponse("xwiki", "POST", "delattachment/Main/remove4/remove5",
+                            200, "HTML RESPONSE");
+    o.spy(o, "value", {"ok": true, "id": "remove4/remove5"},
+          "Remove attachment");
+    o.jio.removeAttachment({"_id": "remove4", "_attachment": "remove5"}, o.f);
+    o.clock.tick(5000);
+    o.server.respond();
+    o.assertReqs(2, "remove attach -> 1 request for csrf and 1 for deletion");
+
+    o.jio.stop();
+});
+/*
+test ("AllDocs", function () {
+
+  // need to make server requests before activating fakeServer
+  var davlist = getXML('responsexml/davlist'),
+    o = setUp(this);
+
+  // get allDocs, no content
+  addFakeServerResponse("xwiki", "PROPFIND", "", 200, davlist);
+  o.thisShouldBeTheAnswer = {
+      "rows": [
+        {"id": "alldocs1", "key": "alldocs1", "value": {}},
+        {"id": "alldocs2", "key": "alldocs2", "value": {}}
+      ],
+      "total_rows": 2
+  }
+  o.spy(o, "value", o.thisShouldBeTheAnswer, "allDocs (no content)");
+  o.jio.allDocs(o.f);
+  o.clock.tick(5000);
+  respond();
+
+  // allDocs with option include
+  o.all1 = {"_id": "allDocs1", "title": "a doc title"};
+  o.all2 = {"_id": "allDocs2", "title": "another doc title"};
+  o.thisShouldBeTheAnswer = {
+      "rows": [
+        {"id": "alldocs1", "key": "alldocs1", "value": {}, "doc": o.all1},
+        {"id": "alldocs2", "key": "alldocs2", "value": {}, "doc": o.all2}
+      ],
+      "total_rows": 2
+  }
+  addFakeServerResponse("xwiki", "GET", "alldocs1", 200,
+    JSON.stringify(o.all1));
+  addFakeServerResponse("xwiki", "GET", "alldocs2", 200,
+    JSON.stringify(o.all2));
+  o.spy(o, "value", o.thisShouldBeTheAnswer, "allDocs (include_docs)");
+  o.jio.allDocs({"include_docs":true}, o.f);
+  o.clock.tick(5000);
+  respond();
+
+  o.jio.stop();
+});
+*/
+
+var nThen = function(next) {
+    var funcs = [];
+    var calls = 0;
+    var waitFor = function(func) {
+        calls++;
+        return function() {
+            if (func) {
+                func.apply(null, arguments);
+            }
+            calls = (calls || 1) - 1;
+            while (!calls && funcs.length) {
+                funcs.shift()(waitFor);
+            }
+        };
+    };
+    next(waitFor);
+    var ret = {
+        nThen: function(next) {
+            funcs.push(next);
+            return ret;
+        },
+        orTimeout: function(func, milliseconds) {
+            var cto;
+            var timeout = setTimeout(function() {
+                while (funcs.shift() !== cto) ;
+                func(waitFor);
+                calls = (calls || 1) - 1;
+                while (!calls && funcs.length) { console.log("call"); funcs.shift()(waitFor); }
+            }, milliseconds);
+            funcs.push(cto = function() { clearTimeout(timeout); });
+            return ret;
+        }
+    };
+    return ret;
+};
+
+
+if (window.location.href.match(/xwiki\/bin\/view/)) (function() {
+test ("XWiki Live Server setup", function () {
+
+    var o = setUp(this);
+    o.jio.stop();
+    this.sandbox.restore();
+    o.jio.start();
+    QUnit.stop();
+
+    nThen(function(waitFor) {
+
+        // Remove the document if it exists.
+        o.jio.remove({"_id": "one.json"}, waitFor());
+
+    }).nThen(function(waitFor) {
+
+        // post a new document
+        o.spy(o, "value", {"id": "one.json", "ok": true}, "Live post document");
+        o.jio.post({"_id": "one.json", "title": "hello"}, waitFor(o.f));
+
+    }).nThen(function(waitFor) {
+
+        o.jio.get("one.json", waitFor(function(err, ret) {
+            ok(!err);
+            ok(ret._id == "one.json");
+            ok(ret.title == "hello");
+        }));
+
+    }).nThen(function(waitFor) {
+
+        // modify document
+        o.spy(o, "value", {"id": "one.json", "ok": true}, "Live modify document");
+        o.jio.put({"_id": "one.json", "title": "hello modified"}, waitFor(o.f));
+
+    }).nThen(function(waitFor) {
+
+        o.jio.get("one.json", waitFor(function(err, ret) {
+            ok(!err);
+            ok(ret.title == "hello modified");
+        }));
+
+    }).nThen(function(waitFor) {
+
+        // add attachment
+        o.spy(o, "value", {"id": "one.json/att.txt", "ok": true}, "Put attachment");
+        o.jio.putAttachment({
+          "_id": "one.json",
+          "_attachment": "att.txt",
+          "_mimetype": "text/plain",
+          "_data": "there2"
+        }, waitFor(o.f));
+
+    }).nThen(function(waitFor) {
+
+        // test allDocs
+        /*o.jio.allDocs({"include_docs":true},
+          function(s){console.log(s);},
+          function ( e ) {console.log(e);
+        }, o.f);*/
+
+    }).nThen(function(waitFor) {
+
+        // get Attachment
+        o.jio.getAttachment({"_id":"one.json", "_attachment":"att.txt"}, waitFor(function(err, ret) {
+            ok(!err);
+            var fr = new FileReader();
+            fr.onload = waitFor(function(dat) {
+                ok(dat.target.result == "there2");
+            });
+            fr.readAsText(ret);
+        }));
+
+    }).nThen(function(waitFor) {
+
+        // remove Attachment
+        o.spy(o, "value", {"id": "one.json/att.txt", "ok": true}, "Remove attachment");
+        o.jio.removeAttachment({"_id":"one.json","_attachment":"att.txt"}, waitFor(o.f));
+
+    }).nThen(function(waitFor) {
+
+        // remove Document
+        o.spy(o, "value", {"id": "one.json", "ok": true}, "Remove document");
+        o.jio.remove("one.json", waitFor(o.f));
+
+    }).nThen(function(waitFor) {
+
+        //console.log("success");
+
+    }).orTimeout(function() {
+
+        //console.log("failed");
+        ok(0);
+
+    }, 15000).nThen(function() {
+
+        //console.log("complete");
+        o.jio.stop();
+        QUnit.start();
+
+    });
+
+});
+})(); // Live XWiki
+
+})(); // xwiki
+
+
 };                              // end thisfun
 
 if (window.requirejs) {
diff --git a/test/jiotests_withoutrequirejs.html b/test/jiotests_withoutrequirejs.html
index 860508f38a3b24ab400e682a8b6cab45fb361eb4..d656af868c79cc82193aebdcb91b0c6fc3b93582 100644
--- a/test/jiotests_withoutrequirejs.html
+++ b/test/jiotests_withoutrequirejs.html
@@ -33,6 +33,8 @@
   </script>
   <script type="text/javascript" src="../complex_queries.js"></script>
   </script>
+  <script type="text/javascript" src="../src/jio.storage/xwikistorage.js">
+  </script>
   <script type="text/javascript" src="./jiotests.js"></script>
 </body>
 </html>