diff --git a/addons/web/common/http.py b/addons/web/common/http.py index 96af5afe65e..4bdd87a6764 100644 --- a/addons/web/common/http.py +++ b/addons/web/common/http.py @@ -6,11 +6,12 @@ import ast import contextlib import functools import logging -import urllib import os import pprint import sys +import threading import traceback +import urllib import uuid import xmlrpclib @@ -128,17 +129,37 @@ class JsonRequest(WebRequest): "id": null} """ - - def dispatch(self, controller, method, requestf=None, request=None): - """ Calls the method asked for by the JSON-RPC2 request + def dispatch(self, controller, method): + """ Calls the method asked for by the JSON-RPC2 or JSONP request :param controller: the instance of the controller which received the request :param method: the method which received the request - :param requestf: a file-like object containing an encoded JSON-RPC2 request - :param request: a JSON-RPC2 request - :returns: an utf8 encoded JSON-RPC2 reply + :returns: an utf8 encoded JSON-RPC2 or JSONP reply """ + args = self.httprequest.args + jsonp = args.get('jsonp', False) + requestf = None + request = None + + if jsonp and self.httprequest.method == 'POST': + # jsonp 2 steps step1 POST: save call + self.init(args) + req.session.jsonp_requests[args.get('id')] = self.httprequest.form['r'] + headers=[('Content-Type', 'text/plain; charset=utf-8')] + r = werkzeug.wrappers.Response(request_id, headers=headers) + return r + elif jsonp and args.get('id'): + # jsonp 2 steps step2 GET: run and return result + self.init(args) + request = self.session.jsonp_requests.pop(args.get(id), "") + elif jsonp and args.get('r'): + # jsonp method GET + request = args.get('r') + else: + # regular jsonrpc2 + requestf = self.httprequest.stream + response = {"jsonrpc": "2.0" } error = None try: @@ -188,10 +209,16 @@ class JsonRequest(WebRequest): if _logger.isEnabledFor(logging.DEBUG): _logger.debug("<--\n%s", pprint.pformat(response)) - content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder) - return werkzeug.wrappers.Response( - content, headers=[('Content-Type', 'application/json'), - ('Content-Length', len(content))]) + + if jsonp: + mime = 'application/javascript' + body = "%s(%s);" % (jsonp, simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder),) + else: + mime = 'application/json' + body = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder) + + r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))]) + return r def jsonrequest(f): """ Decorator marking the decorated method as being a handler for a @@ -205,8 +232,7 @@ def jsonrequest(f): """ @functools.wraps(f) def json_handler(controller, request, config): - return JsonRequest(request, config).dispatch( - controller, f, requestf=request.stream) + return JsonRequest(request, config).dispatch(controller, f) json_handler.exposed = True return json_handler @@ -281,17 +307,19 @@ STORES = {} @contextlib.contextmanager def session_context(request, storage_path, session_cookie='sessionid'): - session_store = STORES.get(storage_path) + session_store, session_lock = STORES.get(storage_path, (None, None)) if not session_store: session_store = werkzeug.contrib.sessions.FilesystemSessionStore( storage_path) - STORES[storage_path] = session_store + session_lock = threading.Lock() + STORES[storage_path] = session_store, session_lock sid = request.cookies.get(session_cookie) - if sid: - request.session = session_store.get(sid) - else: - request.session = session_store.new() + with session_lock: + if sid: + request.session = session_store.get(sid) + else: + request.session = session_store.new() try: yield request.session @@ -300,32 +328,42 @@ def session_context(request, storage_path, session_cookie='sessionid'): # either by login process or by HTTP requests without an OpenERP # session id, and are generally noise for key, value in request.session.items(): - if isinstance(value, session.OpenERPSession) and not value._uid: + if (isinstance(value, session.OpenERPSession) + and not value._uid + and not value.jsonp_requests + ): + _logger.info('remove session %s: %r', key, value.jsonp_requests) del request.session[key] - # FIXME: remove this when non-literals disappear - if sid: - # Re-load sessions from storage and merge non-literal - # contexts and domains (they're indexed by hash of the - # content so conflicts should auto-resolve), otherwise if - # two requests alter those concurrently the last to finish - # will overwrite the previous one, leading to loss of data - # (a non-literal is lost even though it was sent to the - # client and client errors) - # - # note that domains_store and contexts_store are append-only (we - # only ever add items to them), so we can just update one with the - # other to get the right result, if we want to merge the - # ``context`` dict we'll need something smarter - in_store = session_store.get(sid) - for k, v in request.session.iteritems(): - stored = in_store.get(k) - if stored and isinstance(v, session.OpenERPSession)\ - and v != stored: - v.contexts_store.update(stored.contexts_store) - v.domains_store.update(stored.domains_store) + with session_lock: + if sid: + # Re-load sessions from storage and merge non-literal + # contexts and domains (they're indexed by hash of the + # content so conflicts should auto-resolve), otherwise if + # two requests alter those concurrently the last to finish + # will overwrite the previous one, leading to loss of data + # (a non-literal is lost even though it was sent to the + # client and client errors) + # + # note that domains_store and contexts_store are append-only (we + # only ever add items to them), so we can just update one with the + # other to get the right result, if we want to merge the + # ``context`` dict we'll need something smarter + in_store = session_store.get(sid) + for k, v in request.session.iteritems(): + stored = in_store.get(k) + if stored and isinstance(v, session.OpenERPSession)\ + and v != stored: + v.contexts_store.update(stored.contexts_store) + v.domains_store.update(stored.domains_store) + v.jsonp_requests.update(stored.jsonp_requests) - session_store.save(request.session) + # add missing keys + for k, v in in_store.iteritems(): + if k not in request.session: + request.session[k] = v + + session_store.save(request.session) #---------------------------------------------------------- # OpenERP Web Module/Controller Loading and URL Routing diff --git a/addons/web/common/session.py b/addons/web/common/session.py index 1fa76e30b1d..be1a2b749da 100644 --- a/addons/web/common/session.py +++ b/addons/web/common/session.py @@ -37,6 +37,7 @@ class OpenERPSession(object): self.context = {} self.contexts_store = {} self.domains_store = {} + self.jsonp_requests = {} # FIXME use a LRU def __getstate__(self): state = dict(self.__dict__) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 09b5299a555..fc9ae1dec16 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -10,6 +10,7 @@ import os import re import simplejson import time +import urllib2 import xmlrpclib import zlib from xml.etree import ElementTree @@ -242,6 +243,21 @@ class WebClient(openerpweb.Controller): "version": web.common.release.version } +class Proxy(openerpweb.Controller): + _cp_path = '/web/proxy' + + @openerpweb.jsonrequest + def load(self, req, path): + #req.config.socket_port + #if not re.match('^/[^/]+/static/.*', path): + # return werkzeug.exceptions.BadRequest() + + env = req.httprequest.environ + port = env['SERVER_PORT'] + + o = urllib2.urlopen('http://127.0.0.1:%s%s' % (port, path)) + return o.read() + class Database(openerpweb.Controller): _cp_path = "/web/database" @@ -359,7 +375,6 @@ class Session(openerpweb.Controller): @openerpweb.jsonrequest def get_session_info(self, req): - req.session.assert_valid(force=True) return { "uid": req.session._uid, "context": req.session.get_context() if req.session._uid else False, diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index e27dc343961..50fb8018918 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -629,9 +629,6 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{ callback: continuation || function() {} }); }, - on_logout: function() { - this.session.logout(); - } }); openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# */{ @@ -995,47 +992,52 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie this._super(null, element_id); openerp.webclient = this; - var params = {}; - if (jQuery.param != undefined && jQuery.deparam(jQuery.param.querystring()).kitten != undefined) { - this.$element.addClass("kitten-mode-activated"); - this.$element.delegate('img.oe-record-edit-link-img', 'hover', function(e) { - self.$element.toggleClass('clark-gable'); - }); - } - this.$element.html(QWeb.render("Interface", params)); - this.notification = new openerp.web.Notification(this); this.loading = new openerp.web.Loading(this); this.crashmanager = new openerp.web.CrashManager(); this.header = new openerp.web.Header(this); this.login = new openerp.web.Login(this); - this.header.on_logout.add(this.login.on_logout); + this.header.on_logout.add(this.on_logout); this.header.on_action.add(this.on_menu_action); - this.session.on_session_invalid.add(this.login.do_ask_login); - this.session.on_session_valid.add_last(this.header.do_update); - this.session.on_session_invalid.add_last(this.header.do_update); - this.session.on_session_valid.add_last(this.on_logged); - this.session.on_session_invalid.add_last(this.on_logged_out); - - this.menu = new openerp.web.Menu(this, "oe_menu", "oe_secondary_menu"); - this.menu.on_action.add(this.on_menu_action); - this._current_state = null; - }, start: function() { this._super.apply(this, arguments); - this.notification.prependTo(this.$element); - this.loading.appendTo($('#oe_loading')); - this.header.appendTo($("#oe_header")); - this.session.start(); - this.login.appendTo($('#oe_login')); - this.menu.start(); + var self = this; + this.session.bind().then(function() { + var params = {}; + if (jQuery.param != undefined && jQuery.deparam(jQuery.param.querystring()).kitten != undefined) { + this.$element.addClass("kitten-mode-activated"); + this.$element.delegate('img.oe-record-edit-link-img', 'hover', function(e) { + self.$element.toggleClass('clark-gable'); + }); + } + self.$element.html(QWeb.render("Interface", params)); + self.menu = new openerp.web.Menu(self, "oe_menu", "oe_secondary_menu"); + self.menu.on_action.add(self.on_menu_action); + + self.notification.prependTo(self.$element); + self.loading.appendTo($('#oe_loading')); + self.header.appendTo($("#oe_header")); + self.login.appendTo($('#oe_login')); + self.menu.start(); + self.login.on_login_invalid(); + }); + this.session.ready.then(function() { + self.login.on_login_valid(); + self.header.do_update(); + self.menu.do_reload(); + if(self.action_manager) + self.action_manager.stop(); + self.action_manager = new openerp.web.ActionManager(this); + self.action_manager.appendTo($("#oe_app")); + self.bind_hashchange(); + }); }, do_reload: function() { - return $.when(this.session.session_restore(),this.menu.do_reload()); + return $.when(this.session.session_init(),this.menu.do_reload()); }, do_notify: function() { var n = this.notification; @@ -1045,24 +1047,10 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie var n = this.notification; n.warn.apply(n, arguments); }, - on_logged: function() { - this.menu.do_reload(); - if(this.action_manager) - this.action_manager.stop(); - this.action_manager = new openerp.web.ActionManager(this); - this.action_manager.appendTo($("#oe_app")); - - if (openerp._modules_loaded) { // TODO: find better option than this - this.bind_hashchange(); - } else { - this.session.on_modules_loaded.add({ // XXX what about a $.Deferred ? - callback: $.proxy(this, 'bind_hashchange'), - unique: true, - position: 'last' - }) - } - }, - on_logged_out: function() { + on_logout: function() { + this.session.session_logout(); + this.login.on_login_invalid(); + this.header.do_update(); $(window).unbind('hashchange', this.on_hashchange); this.do_push_state({}); if(this.action_manager) @@ -1105,6 +1093,47 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie }, }); +openerp.currentScript = function() { + var currentScript = document.currentScript; + if (!currentScript) { + var sc = document.getElementsByTagName('script'); + currentScript = sc[sc.length-1]; + } + return currentScript; +}; + +openerp.web.EmbeddedClient = openerp.web.Widget.extend({ + template: 'EmptyComponent', + init: function(action_id, options) { + this._super(); + // TODO take the xmlid of a action instead of its id + this.action_id = action_id; + this.options = options || {}; + this.am = new openerp.web.ActionManager(this); + }, + + start: function() { + var self = this; + + this.am.appendTo(this.$element.addClass('openerp')); + + return this.rpc("/web/action/load", { action_id: this.action_id }, function(result) { + var action = result.result; + action.flags = _.extend({ + //views_switcher : false, + search_view : false, + action_buttons : false, + sidebar : false + //pager : false + }, self.options, action.flags || {}); + + self.am.do_action(action); + }); + }, + +}); + + }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index d3734d29278..3d66c4f4ccf 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/core.js @@ -350,11 +350,19 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. * @param {String} [server] JSON-RPC endpoint hostname * @param {String} [port] JSON-RPC endpoint port */ - init: function(server, port) { + init: function() { this._super(); - this.server = (server == undefined) ? location.hostname : server; - this.port = (port == undefined) ? location.port : port; - this.rpc_mode = (server == location.hostname) ? "ajax" : "jsonp"; + // TODO: session store in cookie should be optional + this.name = openerp._session_id; + }, + bind: function(host, protocol) { + var self = this; + this.host = (host == undefined) ? location.host : host; + this.protocol = (protocol == undefined) ? location.protocol : protocol; + this.prefix = this.protocol + '//' + this.host; + openerp.web.qweb.default_dict['_s'] = this.prefix + this.rpc_mode = (this.host == location.host) ? "json" : "jsonp"; + this.rpc_function = (this.host == location.host) ? this.rpc_json : this.rpc_jsonp; this.debug = (window.location.search.indexOf('?debug') !== -1); this.session_id = false; this.uid = false; @@ -366,14 +374,8 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. this.context = {}; this.shortcuts = []; this.active_id = null; - // TODO: session should have an optional name indicating that they'll - // be saved to (and revived from) cookies - this.name = 'session'; - this.do_load_qweb(['/web/webclient/qweb']); - }, - - start: function() { - this.session_restore(); + this.ready = $.Deferred(); + return this.session_init(); }, /** * Executes an RPC call, registering the provided callbacks. @@ -390,82 +392,133 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. */ rpc: function(url, params, success_callback, error_callback) { var self = this; + // url can be an $.ajax option object + if (_.isString(url)) { + url = { url: url }; + } // Construct a JSON-RPC2 request, method is currently unused params.session_id = this.session_id; if (this.debug) params.debug = 1; - - // Call using the rpc_mode - var deferred = $.Deferred(); - this.rpc_ajax(url, { - jsonrpc: "2.0", - method: "call", + var payload = { + jsonrpc: '2.0', + method: 'call', params: params, - id: _.uniqueId('browser-client-') - }).then(function () {deferred.resolve.apply(deferred, arguments);}, - function(error) {deferred.reject(error, $.Event());}); - return deferred.fail(function() { + id: _.uniqueId('r') + }; + var deferred = $.Deferred(); + this.on_rpc_request(); + this.rpc_function(url, payload).then( + function (response, textStatus, jqXHR) { + self.on_rpc_response(); + if (!response.error) { + deferred.resolve(response["result"], textStatus, jqXHR); + } else if (response.error.data.type === "session_invalid") { + self.uid = false; + // TODO deprecate or use a deferred on login.do_ask_login() + self.on_session_invalid(function() { + self.rpc(url, payload.params, + function() { deferred.resolve.apply(deferred, arguments); }, + function() { deferred.reject.apply(deferred, arguments); }); + }); + } else { + deferred.reject(response.error, $.Event()); + } + }, + function(jqXHR, textStatus, errorThrown) { + self.on_rpc_response(); + var error = { + code: -32098, + message: "XmlHttpRequestError " + errorThrown, + data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] } + }; + deferred.reject(error, $.Event()); + }); + // Allow deferred user to disable on_rpc_error in fail + deferred.fail(function() { deferred.fail(function(error, event) { if (!event.isDefaultPrevented()) { self.on_rpc_error(error, event); } }); }).then(success_callback, error_callback).promise(); + return deferred; }, /** * Raw JSON-RPC call * * @returns {jQuery.Deferred} ajax-webd deferred object */ - rpc_ajax: function(url, payload) { + rpc_json: function(url, payload) { var self = this; - this.on_rpc_request(); - // url can be an $.ajax option object - if (_.isString(url)) { - url = { - url: url - } - } var ajax = _.extend({ type: "POST", - url: url, dataType: 'json', contentType: 'application/json', data: JSON.stringify(payload), - processData: false + processData: false, }, url); - var deferred = $.Deferred(); - $.ajax(ajax).done(function(response, textStatus, jqXHR) { - self.on_rpc_response(); - if (!response.error) { - deferred.resolve(response["result"], textStatus, jqXHR); - return; - } - if (response.error.data.type !== "session_invalid") { - deferred.reject(response.error); - return; - } - self.uid = false; - self.on_session_invalid(function() { - self.rpc(url, payload.params, - function() { - deferred.resolve.apply(deferred, arguments); - }, - function(error, event) { - event.preventDefault(); - deferred.reject.apply(deferred, arguments); - }); - }); - }).fail(function(jqXHR, textStatus, errorThrown) { - self.on_rpc_response(); - var error = { - code: -32098, - message: "XmlHttpRequestError " + errorThrown, - data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] } + return $.ajax(ajax); + }, + rpc_jsonp: function(url, payload) { + var self = this; + // extracted from payload to set on the url + var data = { + session_id: this.session_id, + id: payload.id, + }; + url.url = this.get_url(url.url); + var ajax = _.extend({ + type: "GET", + dataType: 'jsonp', + jsonp: 'jsonp', + cache: false, + data: data + }, url); + var payload_str = JSON.stringify(payload); + var payload_url = $.param({r:payload_str}); + if(playload_url.length < 2000) { + // Direct jsonp request + ajax.data.r = payload_str; + return $.ajax(ajax); + } else { + // Indirect jsonp request + var ifid = _.uniqueId('oe_rpc_iframe'); + var display = options.openerp.debug ? 'block' : 'none'; + var $iframe = $(_.str.sprintf("", ifid, ifid, display)); + var $form = $('