source: Dev/trunk/src/client/dojox/io/proxy/xip.js @ 536

Last change on this file since 536 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

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