diff options
author | Petr Vobornik <pvoborni@redhat.com> | 2014-02-13 13:10:18 +0100 |
---|---|---|
committer | Petr Vobornik <pvoborni@redhat.com> | 2014-03-27 14:54:08 +0100 |
commit | d5cf0b273ad511d6cb99fbcb900eff528fe172df (patch) | |
tree | 4a9b78d4e6f0dfbad7627d7135cfcbdebcd54910 /install/ui/src/freeipa/rpc.js | |
parent | e7bfac1e63360993aea2be91082ca69c478f3cf1 (diff) | |
download | freeipa-d5cf0b273ad511d6cb99fbcb900eff528fe172df.tar.gz freeipa-d5cf0b273ad511d6cb99fbcb900eff528fe172df.tar.xz freeipa-d5cf0b273ad511d6cb99fbcb900eff528fe172df.zip |
webui: move RPC code from IPA module to its own module
- moves RPC code from ipa.js to it's own module
- part of ongoing effort where the ultimate goal is to get rid of ipa.js
and IPA namespace
Reviewed-By: Adam Misnyovszki <amisnyov@redhat.com>
Diffstat (limited to 'install/ui/src/freeipa/rpc.js')
-rw-r--r-- | install/ui/src/freeipa/rpc.js | 930 |
1 files changed, 930 insertions, 0 deletions
diff --git a/install/ui/src/freeipa/rpc.js b/install/ui/src/freeipa/rpc.js new file mode 100644 index 000000000..f2878dda3 --- /dev/null +++ b/install/ui/src/freeipa/rpc.js @@ -0,0 +1,930 @@ +/* Authors: + * Pavel Zuna <pzuna@redhat.com> + * Adam Young <ayoung@redhat.com> + * Endi Dewata <edewata@redhat.com> + * John Dennis <jdennis@redhat.com> + * Petr Vobornik <pvoborni@redhat.com> + * + * Copyright (C) 2014 Red Hat + * see file 'COPYING' for use and warranty information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +define([ + 'dojo/_base/lang', + './ipa', + './text', + 'exports' + ], + function(lang, IPA, text, exports) { + +var rpc = {}; + +/** + * Call an IPA command over JSON-RPC. + * + * @class rpc.command + * + * @param {Object} spec - construct specification + * @param {string} spec.name - command name (optional) + * @param {string} spec.entity - command entity(name) (optional) + * @param {string} spec.method - command method + * @param {string[]} spec.args - list of arguments, e.g. ['username'] + * @param {Object} spec.options - dict of options, e.g. {givenname: 'Petr'} + * @param {Function} spec.on_success - callback function if command succeeds + * @param {Function} spec.on_error - callback function if command fails + * + */ +rpc.command = function(spec) { + + spec = spec || {}; + + var that = IPA.object(); + + /** @property {string} name Name */ + that.name = spec.name; + + /** @property {entity.entity} entity Entity */ + that.entity = spec.entity; + + /** @property {string} method Method */ + that.method = spec.method; + + /** @property {string[]} args Command Arguments */ + that.args = $.merge([], spec.args || []); + + /** @property {Object} options Option map */ + that.options = $.extend({}, spec.options || {}); + + /** + * Success handler + * @property {Function} + * @param {Object} data + * @param {string} text_status + * @param {XMLHttpRequest} xhr + */ + that.on_success = spec.on_success; + + /** + * Error handler + * @property {Function} + * @param {XMLHttpRequest} xhr + * @param {string} text_status + * @param {{name:string,message:string}} error_thrown + */ + that.on_error = spec.on_error; + + /** + * Allow retrying of execution if previous ended as error + * + * Manifested by error dialog. Set it to `false` for custom error dialogs or + * error handling without any dialog. + * @property {Boolean} retry=true + */ + that.retry = typeof spec.retry == 'undefined' ? true : spec.retry; + + /** @property {string} error_message Default error message */ + that.error_message = text.get(spec.error_message || '@i18n:dialogs.batch_error_message', 'Some operations failed.'); + + /** @property {ordered_map.<number,string>} error_messages Error messages map */ + that.error_messages = $.ordered_map({ + 911: 'Missing HTTP referer. <br/> You have to configure your browser to send HTTP referer header.' + }); + + /** + * Get command name + * + * - it's `entity.name + '_' + method` + * - or `method` + * @return {string} + */ + that.get_command = function() { + return (that.entity ? that.entity+'_' : '') + that.method; + }; + + /** + * Add argument + * @param {string} arg + */ + that.add_arg = function(arg) { + that.args.push(arg); + }; + + /** + * Add arguments + * @param {string[]} args + */ + that.add_args = function(args) { + $.merge(that.args, args); + }; + + /** + * Set option + * @param {string} name + * @param {Mixed} value + */ + that.set_option = function(name, value) { + that.options[name] = value; + }; + + /** + * Extends options map with another options map + * + * @param {{opt1:Mixed, opt2:Mixed}} options + */ + that.set_options = function(options) { + $.extend(that.options, options); + }; + + /** + * Add value to an option + * + * - creates a new option if it does not exist yet + * - for option overriding use `set_option` method + * @param {string} name + * @param {Mixed} value + */ + that.add_option = function(name, value) { + var values = that.options[name]; + if (!values) { + values = []; + that.options[name] = values; + } + values.push(value); + }; + + /** + * Get option value + * @return {Mixed} + */ + that.get_option = function(name) { + return that.options[name]; + }; + + /** + * Remove option from option map + */ + that.remove_option = function(name) { + delete that.options[name]; + }; + + /** + * Execute the command. + * + * Set `on_success` and/or `on_error` handlers to be informed about result. + */ + that.execute = function() { + + function dialog_open(xhr, text_status, error_thrown) { + + var ajax = this; + + var dialog = IPA.error_dialog({ + xhr: xhr, + text_status: text_status, + error_thrown: error_thrown, + command: that + }); + + dialog.on_cancel = function() { + dialog.close(); + if (that.on_error) { + that.on_error.call(ajax, xhr, text_status, error_thrown); + } + }; + + dialog.open(); + } + + function auth_dialog_open(xhr, text_status, error_thrown) { + + var ajax = this; + + var dialog = IPA.unauthorized_dialog({ + xhr: xhr, + text_status: text_status, + error_thrown: error_thrown, + close_on_escape: false, + command: that + }); + + dialog.open(); + } + + /* + * Special error handler used the first time this command is + * submitted. It checks to see if the session credentials need + * to be acquired and if so sends a request to a special url + * to establish the sesion credentials. If acquiring the + * session credentials is successful it simply resubmits the + * exact same command after setting the error handler back to + * the normal error handler. If aquiring the session + * credentials fails the normal error handler is invoked to + * process the error returned from the attempt to aquire the + * session credentials. + */ + function error_handler_login(xhr, text_status, error_thrown) { + if (xhr.status === 401) { + var login_status = IPA.get_credentials(); + + if (login_status === 200) { + that.request.error = error_handler; + $.ajax(that.request); + return; + } + } + // error_handler() calls IPA.hide_activity_icon() + error_handler.call(this, xhr, text_status, error_thrown); + } + + /* + * Normal error handler, handles all errors. + * error_handler_login() is initially used to trap the + * special case need to aquire session credentials, this is + * not a true error, rather it's an indication an extra step + * needs to be taken before normal processing can continue. + */ + function error_handler(xhr, text_status, error_thrown) { + + IPA.hide_activity_icon(); + + if (xhr.status === 401) { + auth_dialog_open(xhr, text_status, error_thrown); + return; + } else if (!error_thrown) { + error_thrown = { + name: xhr.responseText || text.get('@i18n:errors.unknown_error', 'Unknown Error'), + message: xhr.statusText || text.get('@i18n:errors.unknown_error', 'Unknown Error') + }; + + } else if (typeof error_thrown == 'string') { + error_thrown = { + name: error_thrown, + message: error_thrown + }; + } + + // custom messages for set of codes + var error_msg = that.error_messages.get(error_thrown.code); + if (error_msg) { + error_msg = error_msg.replace('${message}', error_thrown.message); + error_thrown.message = error_msg; + } + + // global specical cases error handlers section + + // With trusts, user from trusted domain can use his ticket but he + // doesn't have rights for LDAP modify. It will throw internal errror. + // We should offer form base login. + if (xhr.status === 500 && IPA.ui.logged_kerberos && !IPA.ui.initialized) { + auth_dialog_open(xhr, text_status, error_thrown); + return; + } + + if (that.retry) { + dialog_open.call(this, xhr, text_status, error_thrown); + + } else if (that.on_error) { + //custom error handling, maintaining AJAX call's context + that.on_error.call(this, xhr, text_status, error_thrown); + } + } + + function success_handler(data, text_status, xhr) { + + if (!data) { + // error_handler() calls IPA.hide_activity_icon() + error_handler.call(this, xhr, text_status, /* error_thrown */ { + name: text.get('@i18n:errors.http_error', 'HTTP Error')+' '+xhr.status, + url: this.url, + message: data ? xhr.statusText : text.get('@i18n:errors.no_response', 'No response') + }); + + } else if (IPA.version && data.version && IPA.version !== data.version) { + window.location.reload(); + + } else if (IPA.principal && data.principal && IPA.principal !== data.principal) { + window.location.reload(); + + } else if (data.error) { + // error_handler() calls IPA.hide_activity_icon() + error_handler.call(this, xhr, text_status, /* error_thrown */ { + name: text.get('@i18n:errors.ipa_error', 'IPA Error') + ' ' + + data.error.code + ': ' + data.error.name, + code: data.error.code, + message: data.error.message, + data: data + }); + + } else { + IPA.hide_activity_icon(); + + var ajax = this; + var failed = that.get_failed(that, data.result, text_status, xhr); + if (!failed.is_empty()) { + var dialog = IPA.error_dialog({ + xhr: xhr, + text_status: text_status, + error_thrown: { + name: text.get('@i18n:dialogs.batch_error_title', 'Operations Error'), + message: that.error_message + }, + command: that, + errors: failed.errors, + visible_buttons: ['ok'] + }); + + dialog.on_ok = function() { + dialog.close(); + if (that.on_success) that.on_success.call(ajax, data, text_status, xhr); + }; + + dialog.open(); + + } else { + //custom success handling, maintaining AJAX call's context + if (that.on_success) that.on_success.call(this, data, text_status, xhr); + } + } + } + + that.data = { + method: that.get_command(), + params: [that.args, that.options] + }; + + that.request = { + url: IPA.json_url || IPA.json_path + '/' + (that.name || that.data.method) + '.json', + data: JSON.stringify(that.data), + success: success_handler, + error: error_handler_login + }; + + IPA.display_activity_icon(); + $.ajax(that.request); + }; + + /** + * Parse successful command result and get all errors. + * @protected + * @param {IPA.command} command + * @param {Object} result + * @param {string} text_status + * @param {XMLHttpRequest} xhr + * @return {IPA.error_list} + */ + that.get_failed = function(command, result, text_status, xhr) { + var errors = IPA.error_list(); + if(result && result.failed) { + for(var association in result.failed) { + for(var member_name in result.failed[association]) { + var member = result.failed[association][member_name]; + for(var i = 0; i < member.length; i++) { + if(member[i].length > 1) { + var name = text.get('@i18n:errors.ipa_error', 'IPA Error'); + var message = member[i][1]; + if(member[i][0]) + message = member[i][0] + ': ' + message; + errors.add(command, name, message, text_status); + } + } + } + } + } + return errors; + }; + + /** + * Check if command accepts option + * @param {string} option_name + * @return {Boolean} + */ + that.check_option = function(option_name) { + + var metadata = IPA.get_command_option(that.get_command(), option_name); + return metadata !== null; + }; + + /** + * Encodes command into JSON-RPC command object + * @return {Object} + */ + that.to_json = function() { + var json = {}; + + json.method = that.get_command(); + + json.params = []; + json.params[0] = that.args || []; + json.params[1] = that.options || {}; + + return json; + }; + + /** + * Encodes command into CLI command string + * @return {string} + */ + that.to_string = function() { + var string = that.get_command().replace(/_/g, '-'); + + for (var i=0; i<that.args.length; i++) { + string += ' '+that.args[i]; + } + + for (var name in that.options) { + string += ' --'+name+'=\''+that.options[name]+'\''; + } + + return string; + }; + + return that; +}; + +/** + * Call multiple IPA commands in a batch over JSON-RPC. + * + * @class rpc.batch_command + * @extends rpc.command + * + * @param {Object} spec + * @param {Array.<IPA.command>} spec.commands - IPA commands to be executed + * @param {Function} spec.on_success - callback function if command succeeds + * @param {Function} spec.on_error - callback function if command fails + */ +rpc.batch_command = function(spec) { + + spec = spec || {}; + + spec.method = 'batch'; + + var that = IPA.command(spec); + + /** @property {IPA.command[]} commands Commands */ + that.commands = []; + /** @property {IPA.error_list} errors Errors */ + that.errors = IPA.error_list(); + + /** + * Show error if some command fail + * @property {Boolean} show_error=true + */ + that.show_error = typeof spec.show_error == 'undefined' ? + true : spec.show_error; + + /** + * Add command + * @param {IPA.command} command + */ + that.add_command = function(command) { + that.commands.push(command); + that.add_arg(command.to_json()); + }; + + /** + * Add commands + * @param {IPA.command[]} commands + */ + that.add_commands = function(commands) { + for (var i=0; i<commands.length; i++) { + that.add_command(commands[i]); + } + }; + + /** + * @inheritDoc + */ + that.execute = function() { + that.errors.clear(); + + var command = IPA.command({ + name: that.name, + entity: that.entity, + method: that.method, + args: that.args, + options: that.options, + retry: that.retry + }); + + command.on_success = that.batch_command_on_success; + command.on_error = that.batch_command_on_error; + + command.execute(); + }; + + /** + * Internal XHR success handler + * + * Parses data and looks for errors. `on_success` or `on_error` is then + * called. + * @protected + * @param {Object} data + * @param {string} text_status + * @param {XMLHttpRequest} xhr + */ + that.batch_command_on_success = function(data, text_status, xhr) { + + for (var i=0; i<that.commands.length; i++) { + var command = that.commands[i]; + var result = data.result.results[i]; + + var name = ''; + var message = ''; + + if (!result) { + name = text.get('@i18n:errors.internal_error', 'Internal Error')+' '+xhr.status; + message = result ? xhr.statusText : text.get('@i18n:errors.internal_error', 'Internal Error'); + + that.errors.add(command, name, message, text_status); + + if (command.on_error) command.on_error.call( + this, + xhr, + text_status, + { + name: name, + message: message + } + ); + + } else if (result.error) { + var code = result.error.code || result.error_code; + name = text.get('@i18n:errors.ipa_error', 'IPA Error')+(code ? ' '+code : ''); + message = result.error.message || result.error; + + if (command.retry) that.errors.add(command, name, message, text_status); + + if (command.on_error) command.on_error.call( + this, + xhr, + text_status, + { + name: name, + code: code, + message: message, + data: result + } + ); + + } else { + var failed = that.get_failed(command, result, text_status, xhr); + that.errors.add_range(failed); + + if (command.on_success) command.on_success.call(this, result, text_status, xhr); + } + } + + //check for partial errors and show error dialog + if (that.show_error && that.errors.errors.length > 0) { + var ajax = this; + var dialog = IPA.error_dialog({ + xhr: xhr, + text_status: text_status, + error_thrown: { + name: text.get('@i18n:dialogs.batch_error_title', 'Operations Error'), + message: that.error_message + }, + command: that, + errors: that.errors.errors, + visible_buttons: [ 'ok' ] + }); + + dialog.on_ok = function() { + dialog.close(); + if (that.on_success) that.on_success.call(ajax, data, text_status, xhr); + }; + + dialog.open(); + + } else { + if (that.on_success) that.on_success.call(this, data, text_status, xhr); + } + }; + + /** + * Internal XHR error handler + * @protected + * @param {XMLHttpRequest} xhr + * @param {string} text_status + * @param {{name:string,message:string}} error_thrown + */ + that.batch_command_on_error = function(xhr, text_status, error_thrown) { + // TODO: undefined behavior + if (that.on_error) { + that.on_error.call(this, xhr, text_status, error_thrown); + } + }; + + return that; +}; + +/** + * Call multiple IPA commands over JSON-RPC separately and wait for every + * command's response. + * + * - concurrent command fails if any command fails + * - result is reported when each command finishes + * + * @class rpc.concurrent_command + * + * @param {Object} spec - construct specification + * @param {Array.<rpc.command>} spec.commands - IPA commands to execute + * @param {Function} spec.on_success - callback function if each command succeed + * @param {Function} spec.on_error - callback function one command fails + * + */ +rpc.concurrent_command = function(spec) { + + spec = spec || {}; + var that = IPA.object(); + + /** @property {rpc.command[]} commands Commands */ + that.commands = []; + + /** + * Success handler + * @property {Function} + */ + that.on_success = spec.on_success; + + /** + * Error handler + * @property {Function} + */ + that.on_error = spec.on_error; + + /** + * Add commands + * @param {rpc.command[]} commands + */ + that.add_commands = function(commands) { + + if(commands && commands.length) { + for(var i=0; i < commands.length; i++) { + that.commands.push({ + command: commands[i] + }); + } + } + }; + + /** + * Execute the commands one by one. + */ + that.execute = function() { + + var command_info, command, i; + + //prepare for execute + for(i=0; i < that.commands.length; i++) { + command_info = that.commands[i]; + command = command_info.command; + if(!command) { + var dialog = IPA.message_dialog({ + name: 'internal_error', + title: text.get('@i18n:errors.error', 'Error'), + message: text.get('@i18n:errors.internal_error', 'Internal error.') + }); + break; + } + command_info.completed = false; + command_info.success = false; + command_info.on_success = command_info.on_success || command.on_success; + command_info.on_error = command_info.on_error || command.on_error; + command.on_success = function(command_info) { + return function(data, text_status, xhr) { + that.success_handler.call(this, command_info, data, text_status, xhr); + }; + }(command_info); + command.on_error = function(command_info) { + return function(xhr, text_status, error_thrown) { + that.error_handler.call(this, command_info, xhr, text_status, error_thrown); + }; + }(command_info); + } + + //execute + for(i=0; i < that.commands.length; i++) { + command = that.commands[i].command; + command.execute(); + } + }; + + /** + * Internal error handler + * @protected + */ + that.error_handler = function(command_info, xhr, text_status, error_thrown) { + + command_info.completed = true; + command_info.success = false; + command_info.xhr = xhr; + command_info.text_status = text_status; + command_info.error_thrown = error_thrown; + command_info.context = this; + that.command_completed(); + }; + + /** + * Internal success handler + * @protected + */ + that.success_handler = function(command_info, data, text_status, xhr) { + + command_info.completed = true; + command_info.success = true; + command_info.data = data; + command_info.text_status = text_status; + command_info.xhr = xhr; + command_info.context = this; + that.command_completed(); + }; + + /** + * Check if all commands finished. + * If so, report it. + * @protected + */ + that.command_completed = function() { + + var all_completed = true; + var all_success = true; + + for(var i=0; i < that.commands.length; i++) { + var command_info = that.commands[i]; + all_completed = all_completed && command_info.completed; + all_success = all_success && command_info.success; + } + + if(all_completed) { + if(all_success) { + that.on_success_all(); + } else { + that.on_error_all(); + } + } + }; + + /** + * Call each command's success handler and `on_success`. + * @protected + */ + that.on_success_all = function() { + + for(var i=0; i < that.commands.length; i++) { + var command_info = that.commands[i]; + if(command_info.on_success) { + command_info.on_success.call( + command_info.context, + command_info.data, + command_info.text_status, + command_info.xhr); + } + } + + if(that.on_success) { + that.on_success(); + } + }; + + /** + * Call each command's error handler and `on_success`. + * @protected + */ + that.on_error_all = function() { + + if(that.on_error) { + that.on_error(); + + } else { + var dialog = IPA.message_dialog({ + name: 'operation_error', + title: text.get('@i18n:dialogs.batch_error_title', 'Operations Error'), + message: text.get('@i18n:dialogs.batch_error_message', 'Some operations failed.') + }); + + dialog.open(); + } + }; + + that.add_commands(spec.commands); + + return that; +}; + +/** + * Error list + * + * Collection for RPC command errors. + * + * @class IPA.error_list + * @private + */ +rpc.error_list = function() { + var that = IPA.object(); + + /** Clear errors */ + that.clear = function() { + that.errors = []; + }; + + /** Add error */ + that.add = function(command, name, message, status) { + that.errors.push({ + command: command, + name: name, + message: message, + status: status + }); + }; + + /** Add errors */ + that.add_range = function(error_list) { + that.errors = that.errors.concat(error_list.errors); + }; + + /** + * Check if there are no errors + * @return {Boolean} + */ + that.is_empty = function () { + return that.errors.length === 0; + }; + + that.clear(); + return that; +}; + +/** + * Error handler for IPA.command which handles error #4304 as success. + * + * 4304 is raised when part of an operation succeeds and the part that failed + * isn't critical. + * @member IPA + * @param {IPA.entity_adder_dialog} adder_dialog + */ +rpc.create_4304_error_handler = function(adder_dialog) { + + var set_pkey = function(result) { + + var pkey_name = adder_dialog.entity.metadata.primary_key; + var args = adder_dialog.command.args; + var pkey = args[args.length-1]; + result[pkey_name] = pkey; + }; + + return function (xhr, text_status, error_thrown) { + + var ajax = this; + var command = adder_dialog.command; + var data = error_thrown.data; + var dialog = null; + + if (data && data.error && data.error.code === 4304) { + dialog = IPA.message_dialog({ + name: 'error_4304_info', + message: data.error.message, + title: adder_dialog.title, + on_ok: function() { + data.result = { result: {} }; + set_pkey(data.result.result); + command.on_success.call(ajax, data, text_status, xhr); + } + }); + } else { + dialog = IPA.error_dialog({ + xhr: xhr, + text_status: text_status, + error_thrown: error_thrown, + command: command + }); + } + + dialog.open(); + }; +}; + +// backwards compatibility: +IPA.error_list = rpc.error_list; +IPA.create_4304_error_handler = rpc.create_4304_error_handler; +IPA.command = rpc.command; +IPA.batch_command = rpc.batch_command; +IPA.concurrent_command = rpc.concurrent_command; + +lang.mixin(exports, rpc); + +return rpc; +});
\ No newline at end of file |