From b58b83e56e458efd749fb9966cfe087cfbf41c3d Mon Sep 17 00:00:00 2001 From: carlfeberhard Date: Mon, 19 Sep 2016 14:36:15 -0400 Subject: [PATCH 1/4] Allow JSONP in api calls --- lib/galaxy/web/framework/decorators.py | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/web/framework/decorators.py b/lib/galaxy/web/framework/decorators.py index 315c65f9de1..1c4680d38d2 100644 --- a/lib/galaxy/web/framework/decorators.py +++ b/lib/galaxy/web/framework/decorators.py @@ -15,6 +15,8 @@ from galaxy.web.framework import url_for log = logging.getLogger( __name__ ) JSON_CONTENT_TYPE = "application/json" +JSONP_CONTENT_TYPE = "application/javascript" +JSONP_CALLBACK_KEY = 'callback' def error( message ): @@ -233,7 +235,12 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ error_code = error_codes.USER_INVALID_JSON return __api_error_response( trans, status_code=400, err_code=error_code ) - trans.response.set_content_type( JSON_CONTENT_TYPE ) + # pull out any callback argument to the api endpoint and set the content type to json or javascript + js_callback_fn_name = kwargs.pop( JSONP_CALLBACK_KEY, None ) + if js_callback_fn_name: + trans.response.set_content_type( JSONP_CONTENT_TYPE ) + else: + trans.response.set_content_type( JSON_CONTENT_TYPE ) # send 'do not cache' headers to handle IE's caching of ajax get responses trans.response.headers[ 'Cache-Control' ] = "max-age=0,no-cache,no-store" @@ -258,11 +265,9 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ error_code = error_codes.USER_INVALID_RUN_AS return __api_error_response( trans, err_code=error_code, status_code=400 ) try: - rval = func( self, trans, *args, **kwargs) - if to_json and trans.debug: - rval = safe_dumps( rval, indent=4, sort_keys=True ) - elif to_json: - rval = safe_dumps( rval ) + rval = func( self, trans, *args, **kwargs ) + if to_json: + rval = _format_return_as_json( rval, js_callback_fn_name, debug=trans.debug ) return rval except MessageException as e: traceback_string = format_exc() @@ -288,6 +293,19 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ return decorator +def _format_return_as_json( rval, js_callback_fn_name=None, debug=False ): + """ + Formats a return value as JSON or JSONP if `js_callback_fn_name` is present. + + Use `debug=True` to return pretty printed json. + """ + dumps_kwargs = dict( indent=4, sort_keys=True ) if debug else {} + json = safe_dumps( rval, **dumps_kwargs ) + if js_callback_fn_name: + json = "{}({})".format( js_callback_fn_name, json ) + return json + + def __api_error_message( trans, **kwds ): exception = kwds.get( "exception", None ) if exception: From ffd0ac78cd3920768ed84f6ab780c13d69532a45 Mon Sep 17 00:00:00 2001 From: carlfeberhard Date: Mon, 19 Sep 2016 15:09:01 -0400 Subject: [PATCH 2/4] Extend JSONP to older decorators --- lib/galaxy/web/framework/decorators.py | 41 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/galaxy/web/framework/decorators.py b/lib/galaxy/web/framework/decorators.py index 1c4680d38d2..094e85c4690 100644 --- a/lib/galaxy/web/framework/decorators.py +++ b/lib/galaxy/web/framework/decorators.py @@ -38,15 +38,22 @@ def expose( func ): return func -def json( func, **json_kwargs ): +def json( func, pretty=False ): """ Format the response as JSON and set the response content type to JSON_CONTENT_TYPE. """ @wraps(func) def call_and_format( self, trans, *args, **kwargs ): - trans.response.set_content_type( JSON_CONTENT_TYPE ) - return safe_dumps( func( self, trans, *args, **kwargs ), **json_kwargs ) + # pull out any callback argument to the api endpoint and set the content type to json or javascript + js_callback_fn_name = kwargs.pop( JSONP_CALLBACK_KEY, None ) + if js_callback_fn_name: + trans.response.set_content_type( JSONP_CONTENT_TYPE ) + else: + trans.response.set_content_type( JSON_CONTENT_TYPE ) + rval = func( self, trans, *args, **kwargs ) + return _format_return_as_json( rval, js_callback_fn_name, pretty=( pretty or trans.debug ) ) + if not hasattr( func, '_orig' ): call_and_format._orig = func return expose( _save_orig_fn( call_and_format, func ) ) @@ -56,7 +63,7 @@ def json_pretty( func ): """ Indent and sort returned JSON. """ - return json( func, indent=4, sort_keys=True ) + return json( func, pretty=True ) def require_login( verb="perform this action", use_panels=False, webapp='galaxy' ): @@ -115,9 +122,17 @@ def expose_api( func, to_json=True, user_required=True ): error_status = '400 Bad Request' error_message = 'Your request did not appear to be valid JSON, please consult the API documentation' return error - trans.response.set_content_type( "application/json" ) + + # pull out any callback argument to the api endpoint and set the content type to json or javascript + js_callback_fn_name = kwargs.pop( JSONP_CALLBACK_KEY, None ) + if js_callback_fn_name: + trans.response.set_content_type( JSONP_CONTENT_TYPE ) + else: + trans.response.set_content_type( JSON_CONTENT_TYPE ) + # send 'do not cache' headers to handle IE's caching of ajax get responses trans.response.headers[ 'Cache-Control' ] = "max-age=0,no-cache,no-store" + # Perform api_run_as processing, possibly changing identity if 'payload' in kwargs and 'run_as' in kwargs['payload']: if not trans.user_can_do_run_as(): @@ -137,10 +152,8 @@ def expose_api( func, to_json=True, user_required=True ): return "That user does not exist." try: rval = func( self, trans, *args, **kwargs) - if to_json and trans.debug: - rval = safe_dumps( rval, indent=4, sort_keys=True ) - elif to_json: - rval = safe_dumps( rval ) + if to_json: + rval = _format_return_as_json( rval, js_callback_fn_name, pretty=trans.debug ) return rval except paste.httpexceptions.HTTPException: raise # handled @@ -267,7 +280,7 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ try: rval = func( self, trans, *args, **kwargs ) if to_json: - rval = _format_return_as_json( rval, js_callback_fn_name, debug=trans.debug ) + rval = _format_return_as_json( rval, js_callback_fn_name, pretty=trans.debug ) return rval except MessageException as e: traceback_string = format_exc() @@ -293,16 +306,16 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ return decorator -def _format_return_as_json( rval, js_callback_fn_name=None, debug=False ): +def _format_return_as_json( rval, js_callback_fn_name=None, pretty=False ): """ Formats a return value as JSON or JSONP if `js_callback_fn_name` is present. - Use `debug=True` to return pretty printed json. + Use `pretty=True` to return pretty printed json. """ - dumps_kwargs = dict( indent=4, sort_keys=True ) if debug else {} + dumps_kwargs = dict( indent=4, sort_keys=True ) if pretty else {} json = safe_dumps( rval, **dumps_kwargs ) if js_callback_fn_name: - json = "{}({})".format( js_callback_fn_name, json ) + json = "{}({});".format( js_callback_fn_name, json ) return json From c69a20f76109754c846ac2d42e22ce87cdc96bca Mon Sep 17 00:00:00 2001 From: carlfeberhard Date: Tue, 20 Sep 2016 10:15:48 -0400 Subject: [PATCH 3/4] API, jsonp: better var name --- lib/galaxy/web/framework/decorators.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/galaxy/web/framework/decorators.py b/lib/galaxy/web/framework/decorators.py index 094e85c4690..1159ea45ab5 100644 --- a/lib/galaxy/web/framework/decorators.py +++ b/lib/galaxy/web/framework/decorators.py @@ -46,13 +46,13 @@ def json( func, pretty=False ): @wraps(func) def call_and_format( self, trans, *args, **kwargs ): # pull out any callback argument to the api endpoint and set the content type to json or javascript - js_callback_fn_name = kwargs.pop( JSONP_CALLBACK_KEY, None ) - if js_callback_fn_name: + jsonp_callback = kwargs.pop( JSONP_CALLBACK_KEY, None ) + if jsonp_callback: trans.response.set_content_type( JSONP_CONTENT_TYPE ) else: trans.response.set_content_type( JSON_CONTENT_TYPE ) rval = func( self, trans, *args, **kwargs ) - return _format_return_as_json( rval, js_callback_fn_name, pretty=( pretty or trans.debug ) ) + return _format_return_as_json( rval, jsonp_callback, pretty=( pretty or trans.debug ) ) if not hasattr( func, '_orig' ): call_and_format._orig = func @@ -124,8 +124,8 @@ def expose_api( func, to_json=True, user_required=True ): return error # pull out any callback argument to the api endpoint and set the content type to json or javascript - js_callback_fn_name = kwargs.pop( JSONP_CALLBACK_KEY, None ) - if js_callback_fn_name: + jsonp_callback = kwargs.pop( JSONP_CALLBACK_KEY, None ) + if jsonp_callback: trans.response.set_content_type( JSONP_CONTENT_TYPE ) else: trans.response.set_content_type( JSON_CONTENT_TYPE ) @@ -153,7 +153,7 @@ def expose_api( func, to_json=True, user_required=True ): try: rval = func( self, trans, *args, **kwargs) if to_json: - rval = _format_return_as_json( rval, js_callback_fn_name, pretty=trans.debug ) + rval = _format_return_as_json( rval, jsonp_callback, pretty=trans.debug ) return rval except paste.httpexceptions.HTTPException: raise # handled @@ -249,8 +249,8 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ return __api_error_response( trans, status_code=400, err_code=error_code ) # pull out any callback argument to the api endpoint and set the content type to json or javascript - js_callback_fn_name = kwargs.pop( JSONP_CALLBACK_KEY, None ) - if js_callback_fn_name: + jsonp_callback = kwargs.pop( JSONP_CALLBACK_KEY, None ) + if jsonp_callback: trans.response.set_content_type( JSONP_CONTENT_TYPE ) else: trans.response.set_content_type( JSON_CONTENT_TYPE ) @@ -280,7 +280,7 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ try: rval = func( self, trans, *args, **kwargs ) if to_json: - rval = _format_return_as_json( rval, js_callback_fn_name, pretty=trans.debug ) + rval = _format_return_as_json( rval, jsonp_callback, pretty=trans.debug ) return rval except MessageException as e: traceback_string = format_exc() @@ -306,16 +306,16 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ return decorator -def _format_return_as_json( rval, js_callback_fn_name=None, pretty=False ): +def _format_return_as_json( rval, jsonp_callback=None, pretty=False ): """ - Formats a return value as JSON or JSONP if `js_callback_fn_name` is present. + Formats a return value as JSON or JSONP if `jsonp_callback` is present. Use `pretty=True` to return pretty printed json. """ dumps_kwargs = dict( indent=4, sort_keys=True ) if pretty else {} json = safe_dumps( rval, **dumps_kwargs ) - if js_callback_fn_name: - json = "{}({});".format( js_callback_fn_name, json ) + if jsonp_callback: + json = "{}({});".format( jsonp_callback, json ) return json From a312ef29e113f510d7fffa0343824b4491206c0a Mon Sep 17 00:00:00 2001 From: carlfeberhard Date: Tue, 20 Sep 2016 10:21:56 -0400 Subject: [PATCH 4/4] API, jsonp: switch off for certain decorators --- lib/galaxy/web/framework/decorators.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/web/framework/decorators.py b/lib/galaxy/web/framework/decorators.py index 1159ea45ab5..94d639c7e16 100644 --- a/lib/galaxy/web/framework/decorators.py +++ b/lib/galaxy/web/framework/decorators.py @@ -221,7 +221,7 @@ def expose_api_anonymous( func, to_json=True ): # ----------------------------------------------------------------------------- (new) api decorators # TODO: rename as expose_api and make default. -def _future_expose_api( func, to_json=True, user_required=True, user_or_session_required=True ): +def _future_expose_api( func, to_json=True, user_required=True, user_or_session_required=True, handle_jsonp=True ): """ Expose this function via the API. """ @@ -249,7 +249,8 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ return __api_error_response( trans, status_code=400, err_code=error_code ) # pull out any callback argument to the api endpoint and set the content type to json or javascript - jsonp_callback = kwargs.pop( JSONP_CALLBACK_KEY, None ) + # TODO: use handle_jsonp to NOT overwrite existing tool_shed JSONP + jsonp_callback = kwargs.pop( JSONP_CALLBACK_KEY, None ) if handle_jsonp else None if jsonp_callback: trans.response.set_content_type( JSONP_CONTENT_TYPE ) else: @@ -393,4 +394,11 @@ def _future_expose_api_raw_anonymous( func ): def _future_expose_api_raw_anonymous_and_sessionless( func ): - return _future_expose_api( func, to_json=False, user_required=False, user_or_session_required=False ) + # TODO: tool_shed api implemented JSONP first on a method-by-method basis, don't overwrite that for now + return _future_expose_api( + func, + to_json=False, + user_required=False, + user_or_session_required=False, + handle_jsonp=False + )