Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / data / proxy / JsonP.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @author Ed Spencer
17  *
18  * The JsonP proxy is useful when you need to load data from a domain other than the one your application is running on. If
19  * your application is running on http://domainA.com it cannot use {@link Ext.data.proxy.Ajax Ajax} to load its data
20  * from http://domainB.com because cross-domain ajax requests are prohibited by the browser.
21  *
22  * We can get around this using a JsonP proxy. JsonP proxy injects a `<script>` tag into the DOM whenever an AJAX request
23  * would usually be made. Let's say we want to load data from http://domainB.com/users - the script tag that would be
24  * injected might look like this:
25  *
26  *     <script src="http://domainB.com/users?callback=someCallback"></script>
27  *
28  * When we inject the tag above, the browser makes a request to that url and includes the response as if it was any
29  * other type of JavaScript include. By passing a callback in the url above, we're telling domainB's server that we want
30  * to be notified when the result comes in and that it should call our callback function with the data it sends back. So
31  * long as the server formats the response to look like this, everything will work:
32  *
33  *     someCallback({
34  *         users: [
35  *             {
36  *                 id: 1,
37  *                 name: "Ed Spencer",
38  *                 email: "ed@sencha.com"
39  *             }
40  *         ]
41  *     });
42  *
43  * As soon as the script finishes loading, the 'someCallback' function that we passed in the url is called with the JSON
44  * object that the server returned.
45  *
46  * JsonP proxy takes care of all of this automatically. It formats the url you pass, adding the callback parameter
47  * automatically. It even creates a temporary callback function, waits for it to be called and then puts the data into
48  * the Proxy making it look just like you loaded it through a normal {@link Ext.data.proxy.Ajax AjaxProxy}. Here's how
49  * we might set that up:
50  *
51  *     Ext.define('User', {
52  *         extend: 'Ext.data.Model',
53  *         fields: ['id', 'name', 'email']
54  *     });
55  *
56  *     var store = Ext.create('Ext.data.Store', {
57  *         model: 'User',
58  *         proxy: {
59  *             type: 'jsonp',
60  *             url : 'http://domainB.com/users'
61  *         }
62  *     });
63  *
64  *     store.load();
65  *
66  * That's all we need to do - JsonP proxy takes care of the rest. In this case the Proxy will have injected a script tag
67  * like this:
68  *
69  *     <script src="http://domainB.com/users?callback=callback1"></script>
70  *
71  * # Customization
72  *
73  * This script tag can be customized using the {@link #callbackKey} configuration. For example:
74  *
75  *     var store = Ext.create('Ext.data.Store', {
76  *         model: 'User',
77  *         proxy: {
78  *             type: 'jsonp',
79  *             url : 'http://domainB.com/users',
80  *             callbackKey: 'theCallbackFunction'
81  *         }
82  *     });
83  *
84  *     store.load();
85  *
86  * Would inject a script tag like this:
87  *
88  *     <script src="http://domainB.com/users?theCallbackFunction=callback1"></script>
89  *
90  * # Implementing on the server side
91  *
92  * The remote server side needs to be configured to return data in this format. Here are suggestions for how you might
93  * achieve this using Java, PHP and ASP.net:
94  *
95  * Java:
96  *
97  *     boolean jsonP = false;
98  *     String cb = request.getParameter("callback");
99  *     if (cb != null) {
100  *         jsonP = true;
101  *         response.setContentType("text/javascript");
102  *     } else {
103  *         response.setContentType("application/x-json");
104  *     }
105  *     Writer out = response.getWriter();
106  *     if (jsonP) {
107  *         out.write(cb + "(");
108  *     }
109  *     out.print(dataBlock.toJsonString());
110  *     if (jsonP) {
111  *         out.write(");");
112  *     }
113  *
114  * PHP:
115  *
116  *     $callback = $_REQUEST['callback'];
117  *
118  *     // Create the output object.
119  *     $output = array('a' => 'Apple', 'b' => 'Banana');
120  *
121  *     //start output
122  *     if ($callback) {
123  *         header('Content-Type: text/javascript');
124  *         echo $callback . '(' . json_encode($output) . ');';
125  *     } else {
126  *         header('Content-Type: application/x-json');
127  *         echo json_encode($output);
128  *     }
129  *
130  * ASP.net:
131  *
132  *     String jsonString = "{success: true}";
133  *     String cb = Request.Params.Get("callback");
134  *     String responseString = "";
135  *     if (!String.IsNullOrEmpty(cb)) {
136  *         responseString = cb + "(" + jsonString + ")";
137  *     } else {
138  *         responseString = jsonString;
139  *     }
140  *     Response.Write(responseString);
141  */
142 Ext.define('Ext.data.proxy.JsonP', {
143     extend: 'Ext.data.proxy.Server',
144     alternateClassName: 'Ext.data.ScriptTagProxy',
145     alias: ['proxy.jsonp', 'proxy.scripttag'],
146     requires: ['Ext.data.JsonP'],
147
148     defaultWriterType: 'base',
149
150     /**
151      * @cfg {String} callbackKey
152      * See {@link Ext.data.JsonP#callbackKey}.
153      */
154     callbackKey : 'callback',
155
156     /**
157      * @cfg {String} recordParam
158      * The param name to use when passing records to the server (e.g. 'records=someEncodedRecordString'). Defaults to
159      * 'records'
160      */
161     recordParam: 'records',
162
163     /**
164      * @cfg {Boolean} autoAppendParams
165      * True to automatically append the request's params to the generated url. Defaults to true
166      */
167     autoAppendParams: true,
168
169     constructor: function(){
170         this.addEvents(
171             /**
172              * @event
173              * Fires when the server returns an exception
174              * @param {Ext.data.proxy.Proxy} this
175              * @param {Ext.data.Request} request The request that was sent
176              * @param {Ext.data.Operation} operation The operation that triggered the request
177              */
178             'exception'
179         );
180         this.callParent(arguments);
181     },
182
183     /**
184      * @private
185      * Performs the read request to the remote domain. JsonP proxy does not actually create an Ajax request,
186      * instead we write out a <script> tag based on the configuration of the internal Ext.data.Request object
187      * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute
188      * @param {Function} callback A callback function to execute when the Operation has been completed
189      * @param {Object} scope The scope to execute the callback in
190      */
191     doRequest: function(operation, callback, scope) {
192         //generate the unique IDs for this request
193         var me      = this,
194             writer  = me.getWriter(),
195             request = me.buildRequest(operation),
196             params = request.params;
197
198         if (operation.allowWrite()) {
199             request = writer.write(request);
200         }
201
202         // apply JsonP proxy-specific attributes to the Request
203         Ext.apply(request, {
204             callbackKey: me.callbackKey,
205             timeout: me.timeout,
206             scope: me,
207             disableCaching: false, // handled by the proxy
208             callback: me.createRequestCallback(request, operation, callback, scope)
209         });
210
211         // prevent doubling up
212         if (me.autoAppendParams) {
213             request.params = {};
214         }
215
216         request.jsonp = Ext.data.JsonP.request(request);
217         // restore on the request
218         request.params = params;
219         operation.setStarted();
220         me.lastRequest = request;
221
222         return request;
223     },
224
225     /**
226      * @private
227      * Creates and returns the function that is called when the request has completed. The returned function
228      * should accept a Response object, which contains the response to be read by the configured Reader.
229      * The third argument is the callback that should be called after the request has been completed and the Reader has decoded
230      * the response. This callback will typically be the callback passed by a store, e.g. in proxy.read(operation, theCallback, scope)
231      * theCallback refers to the callback argument received by this function.
232      * See {@link #doRequest} for details.
233      * @param {Ext.data.Request} request The Request object
234      * @param {Ext.data.Operation} operation The Operation being executed
235      * @param {Function} callback The callback function to be called when the request completes. This is usually the callback
236      * passed to doRequest
237      * @param {Object} scope The scope in which to execute the callback function
238      * @return {Function} The callback function
239      */
240     createRequestCallback: function(request, operation, callback, scope) {
241         var me = this;
242
243         return function(success, response, errorType) {
244             delete me.lastRequest;
245             me.processResponse(success, operation, request, response, callback, scope);
246         };
247     },
248
249     // inherit docs
250     setException: function(operation, response) {
251         operation.setException(operation.request.jsonp.errorType);
252     },
253
254
255     /**
256      * Generates a url based on a given Ext.data.Request object. Adds the params and callback function name to the url
257      * @param {Ext.data.Request} request The request object
258      * @return {String} The url
259      */
260     buildUrl: function(request) {
261         var me      = this,
262             url     = me.callParent(arguments),
263             params  = Ext.apply({}, request.params),
264             filters = params.filters,
265             records,
266             filter, i;
267
268         delete params.filters;
269
270         if (me.autoAppendParams) {
271             url = Ext.urlAppend(url, Ext.Object.toQueryString(params));
272         }
273
274         if (filters && filters.length) {
275             for (i = 0; i < filters.length; i++) {
276                 filter = filters[i];
277
278                 if (filter.value) {
279                     url = Ext.urlAppend(url, filter.property + "=" + filter.value);
280                 }
281             }
282         }
283
284         //if there are any records present, append them to the url also
285         records = request.records;
286
287         if (Ext.isArray(records) && records.length > 0) {
288             url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.recordParam, me.encodeRecords(records)));
289         }
290
291         return url;
292     },
293
294     //inherit docs
295     destroy: function() {
296         this.abort();
297         this.callParent();
298     },
299
300     /**
301      * Aborts the current server request if one is currently running
302      */
303     abort: function() {
304         var lastRequest = this.lastRequest;
305         if (lastRequest) {
306             Ext.data.JsonP.abort(lastRequest.jsonp);
307         }
308     },
309
310     /**
311      * Encodes an array of records into a string suitable to be appended to the script src url. This is broken out into
312      * its own function so that it can be easily overridden.
313      * @param {Ext.data.Model[]} records The records array
314      * @return {String} The encoded records string
315      */
316     encodeRecords: function(records) {
317         var encoded = "",
318             i = 0,
319             len = records.length;
320
321         for (; i < len; i++) {
322             encoded += Ext.Object.toQueryString(records[i].data);
323         }
324
325         return encoded;
326     }
327 });
328