source: Dev/branches/rest-dojo-ui/client/dojox/io/proxy/xip.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 13.4 KB
Line 
1define(['dojo/main', 'dojo/io/iframe', 'dojox/data/dom', 'dojo/_base/xhr', 'dojo/_base/url'], function(dojo, iframe, dom){
2        dojo.getObject("io.proxy.xip", true, dojox);
3
4dojox.io.proxy.xip = {
5        //summary: Object that implements the iframe handling for XMLHttpRequest
6        //IFrame Proxying.
7        //description: Do not use this object directly. See the Dojo Book page
8        //on XMLHttpRequest IFrame Proxying:
9        //http://dojotoolkit.org/book/dojo-book-0-4/part-5-connecting-pieces/i-o/cross-domain-xmlhttprequest-using-iframe-proxy
10        //Usage of XHR IFrame Proxying does not work from local disk in Safari.
11
12        /*
13        This code is really focused on just sending one complete request to the server, and
14        receiving one complete response per iframe. The code does not expect to reuse iframes for multiple XHR request/response
15        sequences. This might be reworked later if performance indicates a need for it.
16       
17        xip fragment identifier/hash values have the form:
18        #id:cmd:realEncodedMessage
19
20        id: some ID that should be unique among message fragments. No inherent meaning,
21                just something to make sure the hash value is unique so the message
22                receiver knows a new message is available.
23               
24        cmd: command to the receiver. Valid values are:
25                 - init: message used to init the frame. Sent as the first URL when loading
26                         the page. Contains some config parameters.
27                 - loaded: the remote frame is loaded. Only sent from xip_client.html to this module.
28                 - ok: the message that this page sent was received OK. The next message may
29                       now be sent.
30                 - start: the start message of a block of messages (a complete message may
31                          need to be segmented into many messages to get around the limitiations
32                          of the size of an URL that a browser accepts.
33                 - part: indicates this is a part of a message.
34                 - end: the end message of a block of messages. The message can now be acted upon.
35                        If the message is small enough that it doesn't need to be segmented, then
36                        just one hash value message can be sent with "end" as the command.
37       
38        To reassemble a segmented message, the realEncodedMessage parts just have to be concatenated
39        together.
40        */
41
42        xipClientUrl: ((dojo.config || djConfig)["xipClientUrl"]) || dojo.moduleUrl("dojox.io.proxy", "xip_client.html").toString(),
43
44
45        //MSIE has the lowest limit for URLs with fragment identifiers,
46        //at around 4K. Choosing a slightly smaller number for good measure.
47        urlLimit: 4000,
48
49        _callbackName: (dojox._scopeName || "dojox") + ".io.proxy.xip.fragmentReceived",
50        _state: {},
51        _stateIdCounter: 0,
52        _isWebKit: navigator.userAgent.indexOf("WebKit") != -1,
53
54
55        send: function(/*Object*/facade){
56                //summary: starts the xdomain request using the provided facade.
57                //This method first does some init work, then delegates to _realSend.
58
59                var url = this.xipClientUrl;
60                //Make sure we are not dealing with javascript urls, just to be safe.
61                if(url.split(":")[0].match(/javascript/i) || facade._ifpServerUrl.split(":")[0].match(/javascript/i)){
62                        return null;
63                }
64               
65                //Make xip_client a full URL.
66                var colonIndex = url.indexOf(":");
67                var slashIndex = url.indexOf("/");
68                if(colonIndex == -1 || slashIndex < colonIndex){
69                        //No colon or we are starting with a / before a colon, so we need to make a full URL.
70                        var loc = window.location.href;
71                        if(slashIndex == 0){
72                                //Have a full path, just need the domain.
73                                url = loc.substring(0, loc.indexOf("/", 9)) + url; //Using 9 to get past http(s)://
74                        }else{
75                                url = loc.substring(0, (loc.lastIndexOf("/") + 1)) + url;
76                        }
77                }
78                this.fullXipClientUrl = url;
79
80                //Set up an HTML5 messaging listener if postMessage exists.
81                //As of this writing, this is only useful to get Opera 9.25+ to work.
82                if(typeof document.postMessage != "undefined"){
83                        document.addEventListener("message", dojo.hitch(this, this.fragmentReceivedEvent), false);
84                }
85
86                //Now that we did first time init, always use the realSend method.
87                this.send = this._realSend;
88                return this._realSend(facade); //Object
89        },
90
91        _realSend: function(facade){
92                //summary: starts the actual xdomain request using the provided facade.
93                var stateId = "XhrIframeProxy" + (this._stateIdCounter++);
94                facade._stateId = stateId;
95
96                var frameUrl = facade._ifpServerUrl + "#0:init:id=" + stateId + "&client="
97                        + encodeURIComponent(this.fullXipClientUrl) + "&callback=" + encodeURIComponent(this._callbackName);
98
99                this._state[stateId] = {
100                        facade: facade,
101                        stateId: stateId,
102                        clientFrame: iframe.create(stateId, "", frameUrl),
103                        isSending: false,
104                        serverUrl: facade._ifpServerUrl,
105                        requestData: null,
106                        responseMessage: "",
107                        requestParts: [],
108                        idCounter: 1,
109                        partIndex: 0,
110                        serverWindow: null
111                };
112
113                return stateId; //Object
114        },
115
116        receive: function(/*String*/stateId, /*String*/urlEncodedData){
117                /* urlEncodedData should have the following params:
118                                - responseHeaders
119                                - status
120                                - statusText
121                                - responseText
122                */
123                //Decode response data.
124                var response = {};
125                var nvPairs = urlEncodedData.split("&");
126                for(var i = 0; i < nvPairs.length; i++){
127                        if(nvPairs[i]){
128                                var nameValue = nvPairs[i].split("=");
129                                response[decodeURIComponent(nameValue[0])] = decodeURIComponent(nameValue[1]);
130                        }
131                }
132
133                //Set data on facade object.
134                var state = this._state[stateId];
135                var facade = state.facade;
136
137                facade._setResponseHeaders(response.responseHeaders);
138                if(response.status == 0 || response.status){
139                        facade.status = parseInt(response.status, 10);
140                }
141                if(response.statusText){
142                        facade.statusText = response.statusText;
143                }
144                if(response.responseText){
145                        facade.responseText = response.responseText;
146                       
147                        //Fix responseXML.
148                        var contentType = facade.getResponseHeader("Content-Type");
149                        if(contentType){
150                                var mimeType = contentType.split(";")[0];
151                                if(mimeType.indexOf("application/xml") == 0 || mimeType.indexOf("text/xml") == 0){
152                                        facade.responseXML = dom.createDocument(response.responseText, contentType);
153                                }
154                        }
155                }
156                facade.readyState = 4;
157               
158                this.destroyState(stateId);
159        },
160
161        frameLoaded: function(/*String*/stateId){
162                var state = this._state[stateId];
163                var facade = state.facade;
164
165                var reqHeaders = [];
166                for(var param in facade._requestHeaders){
167                        reqHeaders.push(param + ": " + facade._requestHeaders[param]);
168                }
169
170                var requestData = {
171                        uri: facade._uri
172                };
173                if(reqHeaders.length > 0){
174                        requestData.requestHeaders = reqHeaders.join("\r\n");
175                }
176                if(facade._method){
177                        requestData.method = facade._method;
178                }
179                if(facade._bodyData){
180                        requestData.data = facade._bodyData;
181                }
182
183                this.sendRequest(stateId, dojo.objectToQuery(requestData));
184        },
185       
186        destroyState: function(/*String*/stateId){
187                var state = this._state[stateId];
188                if(state){
189                        delete this._state[stateId];
190                        var parentNode = state.clientFrame.parentNode;
191                        parentNode.removeChild(state.clientFrame);
192                        state.clientFrame = null;
193                        state = null;
194                }
195        },
196
197        createFacade: function(){
198                if(arguments && arguments[0] && arguments[0].iframeProxyUrl){
199                        return new dojox.io.proxy.xip.XhrIframeFacade(arguments[0].iframeProxyUrl);
200                }else{
201                        return dojox.io.proxy.xip._xhrObjOld.apply(dojo, arguments);
202                }
203        },
204       
205        //**** State-bound methods ****
206        sendRequest: function(stateId, encodedData){
207                var state = this._state[stateId];
208                if(!state.isSending){
209                        state.isSending = true;
210
211                        state.requestData = encodedData || "";
212
213                        //Get a handle to the server iframe.
214                        state.serverWindow = frames[state.stateId];
215                        if (!state.serverWindow){
216                                state.serverWindow = document.getElementById(state.stateId).contentWindow;
217                        }
218
219                        //Make sure we have contentWindow, but only do this for non-postMessage
220                        //browsers (right now just opera is postMessage).
221                        if(typeof document.postMessage == "undefined"){
222                                if(state.serverWindow.contentWindow){
223                                        state.serverWindow = state.serverWindow.contentWindow;
224                                }
225                        }
226
227                        this.sendRequestStart(stateId);
228                }
229        },
230
231        sendRequestStart: function(stateId){
232                //Break the message into parts, if necessary.
233                var state = this._state[stateId];
234                state.requestParts = [];
235                var reqData = state.requestData;
236                var urlLength = state.serverUrl.length;
237                var partLength = this.urlLimit - urlLength;
238                var reqIndex = 0;
239
240                while((reqData.length - reqIndex) + urlLength > this.urlLimit){
241                        var part = reqData.substring(reqIndex, reqIndex + partLength);
242                        //Safari will do some extra hex escaping unless we keep the original hex
243                        //escaping complete.
244                        var percentIndex = part.lastIndexOf("%");
245                        if(percentIndex == part.length - 1 || percentIndex == part.length - 2){
246                                part = part.substring(0, percentIndex);
247                        }
248                        state.requestParts.push(part);
249                        reqIndex += part.length;
250                }
251                state.requestParts.push(reqData.substring(reqIndex, reqData.length));
252               
253                state.partIndex = 0;
254                this.sendRequestPart(stateId);
255
256        },
257       
258        sendRequestPart: function(stateId){
259                var state = this._state[stateId];
260
261                if(state.partIndex < state.requestParts.length){
262                        //Get the message part.
263                        var partData = state.requestParts[state.partIndex];
264
265                        //Get the command.
266                        var cmd = "part";
267                        if(state.partIndex + 1 == state.requestParts.length){
268                                cmd = "end";
269                        }else if (state.partIndex == 0){
270                                cmd = "start";
271                        }
272                       
273                        this.setServerUrl(stateId, cmd, partData);
274                        state.partIndex++;
275                }
276        },
277
278        setServerUrl: function(stateId, cmd, message){
279                var serverUrl = this.makeServerUrl(stateId, cmd, message);
280                var state = this._state[stateId];
281
282                //Safari won't let us replace across domains.
283                if(this._isWebKit){
284                        state.serverWindow.location = serverUrl;
285                }else{
286                        state.serverWindow.location.replace(serverUrl);
287                }
288        },
289
290        makeServerUrl: function(stateId, cmd, message){
291                var state = this._state[stateId];
292                var serverUrl = state.serverUrl + "#" + (state.idCounter++) + ":" + cmd;
293                if(message){
294                        serverUrl += ":" + message;
295                }
296                return serverUrl;
297        },
298
299        fragmentReceivedEvent: function(evt){
300                //summary: HTML5 document messaging endpoint. Unpack the event to see
301                //if we want to use it.
302                if(evt.uri.split("#")[0] == this.fullXipClientUrl){
303                        this.fragmentReceived(evt.data);
304                }
305        },
306
307        fragmentReceived: function(frag){
308                var index = frag.indexOf("#");
309                var stateId = frag.substring(0, index);
310                var encodedData = frag.substring(index + 1, frag.length);
311
312                var msg = this.unpackMessage(encodedData);
313                var state = this._state[stateId];
314
315                switch(msg.command){
316                        case "loaded":
317                                this.frameLoaded(stateId);
318                                break;
319                        case "ok":
320                                this.sendRequestPart(stateId);
321                                break;
322                        case "start":
323                                state.responseMessage = "" + msg.message;
324                                this.setServerUrl(stateId, "ok");
325                                break;
326                        case "part":
327                                state.responseMessage += msg.message;
328                                this.setServerUrl(stateId, "ok");
329                                break;
330                        case "end":
331                                this.setServerUrl(stateId, "ok");
332                                state.responseMessage += msg.message;
333                                this.receive(stateId, state.responseMessage);
334                                break;
335                }
336        },
337       
338        unpackMessage: function(encodedMessage){
339                var parts = encodedMessage.split(":");
340                var command = parts[1];
341                encodedMessage = parts[2] || "";
342
343                var config = null;
344                if(command == "init"){
345                        var configParts = encodedMessage.split("&");
346                        config = {};
347                        for(var i = 0; i < configParts.length; i++){
348                                var nameValue = configParts[i].split("=");
349                                config[decodeURIComponent(nameValue[0])] = decodeURIComponent(nameValue[1]);
350                        }
351                }
352                return {command: command, message: encodedMessage, config: config};
353        }
354}
355
356//Replace the normal XHR factory with the proxy one.
357dojox.io.proxy.xip._xhrObjOld = dojo._xhrObj;
358dojo._xhrObj = dojox.io.proxy.xip.createFacade;
359
360/**
361        Using this a reference: http://www.w3.org/TR/XMLHttpRequest/
362
363        Does not implement the onreadystate callback since dojo.xhr* does
364        not use it.
365*/
366dojox.io.proxy.xip.XhrIframeFacade = function(ifpServerUrl){
367        //summary: XMLHttpRequest facade object used by dojox.io.proxy.xip.
368       
369        //description: Do not use this object directly. See the Dojo Book page
370        //on XMLHttpRequest IFrame Proxying:
371        //http://dojotoolkit.org/book/dojo-book-0-4/part-5-connecting-pieces/i-o/cross-domain-xmlhttprequest-using-iframe-proxy
372        this._requestHeaders = {};
373        this._allResponseHeaders = null;
374        this._responseHeaders = {};
375        this._method = null;
376        this._uri = null;
377        this._bodyData = null;
378        this.responseText = null;
379        this.responseXML = null;
380        this.status = null;
381        this.statusText = null;
382        this.readyState = 0;
383       
384        this._ifpServerUrl = ifpServerUrl;
385        this._stateId = null;
386}
387
388dojo.extend(dojox.io.proxy.xip.XhrIframeFacade, {
389        //The open method does not properly reset since Dojo does not reuse XHR objects.
390        open: function(/*String*/method, /*String*/uri){
391                this._method = method;
392                this._uri = uri;
393
394                this.readyState = 1;
395        },
396       
397        setRequestHeader: function(/*String*/header, /*String*/value){
398                this._requestHeaders[header] = value;
399        },
400       
401        send: function(/*String*/stringData){
402                this._bodyData = stringData;
403               
404                this._stateId = dojox.io.proxy.xip.send(this);
405               
406                this.readyState = 2;
407        },
408        abort: function(){
409                dojox.io.proxy.xip.destroyState(this._stateId);
410        },
411       
412        getAllResponseHeaders: function(){
413                return this._allResponseHeaders; //String
414        },
415       
416        getResponseHeader: function(/*String*/header){
417                return this._responseHeaders[header]; //String
418        },
419       
420        _setResponseHeaders: function(/*String*/allHeaders){
421                if(allHeaders){
422                        this._allResponseHeaders = allHeaders;
423                       
424                        //Make sure ther are now CR characters in the headers.
425                        allHeaders = allHeaders.replace(/\r/g, "");
426                        var nvPairs = allHeaders.split("\n");
427                        for(var i = 0; i < nvPairs.length; i++){
428                                if(nvPairs[i]){
429                                        var nameValue = nvPairs[i].split(": ");
430                                        this._responseHeaders[nameValue[0]] = nameValue[1];
431                                }
432                        }
433                }
434        }
435});
436
437return dojox.io.proxy.xip;
438
439});
Note: See TracBrowser for help on using the repository browser.