5db23a2011-04-04Jonas Wallden /*global ROXEN, YAHOO */
9b44092015-06-26Jonas Walldén /* jshint indent: 2 */
5db23a2011-04-04Jonas Wallden  /** * Module interface to the new Action File System * * @module afs * @class AFS * @namespace ROXEN * @static */ ROXEN.AFS = function () { // 1 - Log AFS calls and responses. // 2 - Also log calls to tagged callbacks, i.e. the single-response // callbacks passed to call(). // 3 - Also log calls to global and error callbacks.
91e3be2017-05-17Anders Johansson  var debug_log = parseInt(ROXEN.getQueryVariable(window.location.href, "__afs-debug"), 10);
5db23a2011-04-04Jonas Wallden  var session = ROXEN.config.session; /** * AFS actions path prefix. */ var actions_prefix = "/actions/"; /** * AFS session variable name in action URLs. */ var session_var = "session_id";
35c0b22016-11-18Anders Johansson 
5db23a2011-04-04Jonas Wallden  /** * How long a poll for new msgs is held by the server. * A value of 0 disables blocking polls. * A value of -1 means automatic (parameters decided by server). * A value of -2 disables polling altogether. * * @property poll_timeout * @type {Int} * @private */ var poll_timeout = -1; // Auto mode. /** * Seconds to wait after the last AFS response before issuing * another poll. When the client is idle, this is effectively the * interval between polls. * * @property poll_delay * @type {Int} * @private */ var poll_delay = 30; /** * Seconds to wait after a low-level AFS error before issuing * another poll. * * @property poll_error_delay * @type {Int} * @private */ var poll_error_delay = 30; // True as long as the last AFS call was successful. var connection_ok = false; // Number of ongoing AFS calls. var open_connections = 0; var poll_delay_timeout_id; // Maps the tag for the ongoing calls to the callback function and an // optional group identifier when the response comes. var tagged_callbacks = {}; // Track groups of connections so they can aborted. var groups = {}; // Counter to produce unique tags. var tag_count = 0; // Set of callback functions that will receive all AFS responses. var global_callbacks = []; // Set of callback functions that get called when there are // low-level AFS errors (connection errors or JSON format errors). var error_callbacks = [];
ff60d72016-09-26Anders Johansson  // Set on init(). var is_initialized = false;
84d3bf2011-04-27Martin Stjernholm  function encode_afs_args (args) { var query = []; var json_args = {}, got_json_args = false; // Send strings as ordinary variables. Everything else is sent as // json in the special __afs variable. That way it's possible to // send variables that are parsed by the server outside the main // afs query handler. for (var name in args) if (args.hasOwnProperty (name)) {
35c0b22016-11-18Anders Johansson  var val = args[name]; if (YAHOO.lang.isString (val)) // Assume the name only contains ordinary characters. May // escape it later should it be a problem. query.push (name + "=" + ROXEN.escapeURIComponent (val)); else { json_args[name] = val; got_json_args = true; }
84d3bf2011-04-27Martin Stjernholm  } if (got_json_args)
12a01f2011-12-01Anders Johansson  query.push ("__afs=" + ROXEN.escapeURIComponent(YAHOO.lang.JSON.stringify (json_args)));
84d3bf2011-04-27Martin Stjernholm  return query.join ("&"); }
5db23a2011-04-04Jonas Wallden  function request_failure (resp) { ROXEN.log("ROXEN.AFS: connection error: " +
35c0b22016-11-18Anders Johansson  resp.status + " " + resp.statusText);
5db23a2011-04-04Jonas Wallden 
e2575d2011-12-01Anders Johansson  for (var i = 0; i < error_callbacks.length; i++) {
5db23a2011-04-04Jonas Wallden  var cb = error_callbacks[i]; if (debug_log > 2)
35c0b22016-11-18Anders Johansson  ROXEN.log (" AFS calling error callback: " + cb.name);
9b44092015-06-26Jonas Walldén  try {
35c0b22016-11-18Anders Johansson  cb (resp);
9b44092015-06-26Jonas Walldén  } catch (err) {
35c0b22016-11-18Anders Johansson  ROXEN.log ("AFS: error in callback " + cb.name + ": " + err); if (ROXEN.debug) throw err;
9b44092015-06-26Jonas Walldén  }
5db23a2011-04-04Jonas Wallden  } // Forget all ongoing calls since we cannot hope to receive any // useful responses to them anyway after this. tagged_callbacks = {}; groups = {}; if (open_connections) { open_connections = 0; restart_poll (true); } connection_ok = false; } function json_parse_failure (err) {
9b44092015-06-26Jonas Walldén  ROXEN.log("ROXEN.AFS: JSON parse error: " + err);
5db23a2011-04-04Jonas Wallden 
e2575d2011-12-01Anders Johansson  for (var i = 0; i < error_callbacks.length; i++) {
5db23a2011-04-04Jonas Wallden  var cb = error_callbacks[i]; if (debug_log > 2)
35c0b22016-11-18Anders Johansson  ROXEN.log (" AFS calling error callback: " + cb.name);
5db23a2011-04-04Jonas Wallden  // FIXME: Need a flag to tell it apart from a connection error?
9b44092015-06-26Jonas Walldén  try {
35c0b22016-11-18Anders Johansson  cb (err);
55209f2016-11-18Anders Johansson  } catch (e) { ROXEN.log ("AFS: error in callback " + cb.name + ": " + e);
35c0b22016-11-18Anders Johansson  if (ROXEN.debug)
55209f2016-11-18Anders Johansson  throw e;
9b44092015-06-26Jonas Walldén  }
5db23a2011-04-04Jonas Wallden  } // Forget all ongoing calls since we cannot hope to receive any // useful responses to them anyway after this. tagged_callbacks = {}; groups = {}; if (open_connections) { open_connections = 0; restart_poll (true); } connection_ok = false; } function request_success (resp) {
b62e8f2017-03-09Anders Johansson  var msgs, cb, ct; // YAHOO.util.Connect.asyncRequest follows redirect transparently and // we want to detect the case when this is a redirect to a login page. ct = resp.getResponseHeader["Content-Type"]; if (ct && !ct.match(/application\/json/)) { json_parse_failure(new Error("ContentTypeError")); return; }
5db23a2011-04-04Jonas Wallden  try { msgs = YAHOO.lang.JSON.parse(resp.responseText); } catch (err) { json_parse_failure (err); return; } msgs._etag = resp.getResponseHeader.Etag || resp.getResponseHeader.ETag; for (var i = 0; i < msgs.length; i++) { var msg = msgs[i]; var tag = msg.tag; if (tag) {
35c0b22016-11-18Anders Johansson  if (debug_log) ROXEN.log ("AFS response: " + msg.msg_type + ", tag " + tag);
5db23a2011-04-04Jonas Wallden  var ent = tagged_callbacks[tag];
35c0b22016-11-18Anders Johansson  cb = ent && ent[0]; if (ent === undefined) ROXEN.log ("ROXEN.AFS: Warning: Got AFS response with " + "unknown tag: " + msg.msg_type);
5db23a2011-04-04Jonas Wallden  else if (cb == -1) { if (debug_log > 1)
35c0b22016-11-18Anders Johansson  ROXEN.log (" AFS tagged callback was canceled"); delete tagged_callbacks[tag];
5db23a2011-04-04Jonas Wallden  } else {
35c0b22016-11-18Anders Johansson  if (debug_log > 1) ROXEN.log (" AFS calling tagged callback: " + cb.name);
9b44092015-06-26Jonas Walldén  if (ROXEN.debug) cb(msg); else { try { cb(msg); } catch (err) { ROXEN.log("AFS: error in callback " + cb.name + ": " + err); } }
35c0b22016-11-18Anders Johansson  // Assume no more than one response with a given tag. See // also AFS.ClientSession.push_response. delete tagged_callbacks[tag]; var group = ent[1];
5db23a2011-04-04Jonas Wallden  if (group) { var g = groups[group]; g.splice(g.indexOf(tag), 1); }
35c0b22016-11-18Anders Johansson  }
5db23a2011-04-04Jonas Wallden  } else {
35c0b22016-11-18Anders Johansson  if (debug_log) ROXEN.log ("AFS response: " + msg.msg_type);
5db23a2011-04-04Jonas Wallden  } for (var j = 0; j < global_callbacks.length; j++) {
35c0b22016-11-18Anders Johansson  cb = global_callbacks[j]; if (debug_log > 2) ROXEN.log (" AFS calling global callback: " + cb.name);
9b44092015-06-26Jonas Walldén  try { cb (msg); } catch (err) { ROXEN.log ("AFS: error in callback " + cb.name + ": " + err);
f8c8af2017-05-17Anders Johansson  if (ROXEN.debug) throw err;
9b44092015-06-26Jonas Walldén  }
5db23a2011-04-04Jonas Wallden  } } if (open_connections > 0) { open_connections--; if (!open_connections) restart_poll (false); } connection_ok = true; } /** * Calls an AFS action. * * @param {String} action Requested action name. * @param {Object} args Action arguments. "session_id" (or whatever * name the session variable is given) and "tag" * arguments get added to it. * @param {Function} fn Optional callback function to run when the * corresponding AFS response comes back. * It gets a single argument that is the * response message in JSON. If this isn't * given then the AFS action is untagged, * and any response it produces will be * sent to the global callbacks only. * @param {Object} scope Scope correction (optional). * @param {String} group Group identifier (optional). * @return {Object} Returns the connection object. */ function call(action, args, fn, scope, group) { return call_or_post(action, "GET", args, 0, fn, scope, group); }
35c0b22016-11-18Anders Johansson 
5db23a2011-04-04Jonas Wallden  function post(action, args, postargs, fn, scope, group) {
123acc2015-07-22Jonas Walldén  if (ROXEN.isString(postargs)) { // ID or name of a <form> tag that should be encoded YAHOO.util.Connect.setForm(postargs); postargs = undefined; } else if (!ROXEN.isObject(postargs)) {
5db23a2011-04-04Jonas Wallden  postargs = { };
123acc2015-07-22Jonas Walldén  }
35c0b22016-11-18Anders Johansson 
5db23a2011-04-04Jonas Wallden  var postdata = [ ];
123acc2015-07-22Jonas Walldén  if (postargs) { var item = 0; for (var idx in postargs)
35c0b22016-11-18Anders Johansson  if (postargs.hasOwnProperty(idx)) { postdata[item++] = encodeURIComponent(idx) + "=" + encodeURIComponent(postargs[idx]); }
123acc2015-07-22Jonas Walldén  }
5db23a2011-04-04Jonas Wallden  return call_or_post(action, "POST", args, postdata.join("&"), fn, scope); }
36dc272015-12-14Jonas Walldén  function post_files(action, args, files, fn, scope, group) { // File should come from the browser's FileList API. We will use FormData // together with XHR to post directly to the server without involving YUI. // Prepare AFS call if (!ROXEN.isObject(args)) args = { }; if (scope) fn = ROXEN.bind(scope, fn); if (fn) { var tag = ++tag_count + ""; if (debug_log)
35c0b22016-11-18Anders Johansson  ROXEN.log ("AFS call: " + action + " " + YAHOO.lang.JSON.stringify (args) + ", callback " + fn.name + ", tag " + tag);
36dc272015-12-14Jonas Walldén  args.tag = tag; tagged_callbacks[tag] = [ fn, group ]; } else if (debug_log) { ROXEN.log ("AFS call: " + action + " " +
35c0b22016-11-18Anders Johansson  YAHOO.lang.JSON.stringify (args));
36dc272015-12-14Jonas Walldén  } if (fn && group) { if (!groups[group]) groups[group] = [ ]; groups[group].push(args.tag); }
35c0b22016-11-18Anders Johansson 
36dc272015-12-14Jonas Walldén  args[session_var] = session; open_connections++; // Initiate transfer var url = actions_prefix + action + "?" + encode_afs_args(args); var fd = new FormData();
1ab5992017-08-25Karl Gustav Sterneberg  for (var i = 0; i < files.length; i++) { fd.append("upload-file-" + i, files[i], files[i].name); }
36dc272015-12-14Jonas Walldén  var xhr = new XMLHttpRequest(); xhr.open("POST", url, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) {
35c0b22016-11-18Anders Johansson  if (xhr.status === 200) { // Invoke success callback request_success(xhr); } else if (xhr.status === 0 || xhr.status >= 400) { // Invoke failure callback request_failure(xhr); }
36dc272015-12-14Jonas Walldén  } }; xhr.send(fd); }
35c0b22016-11-18Anders Johansson 
5db23a2011-04-04Jonas Wallden  function call_or_post(action, method, args, postdata, fn, scope, group) { if (!ROXEN.isObject(args)) { args = {}; } if (scope) fn = ROXEN.bind (scope, fn); if (fn) { var tag = ++tag_count+""; if (debug_log)
35c0b22016-11-18Anders Johansson  ROXEN.log ("AFS call: " + action + " " + YAHOO.lang.JSON.stringify (args) + ", callback " + fn.name + ", tag " + tag);
5db23a2011-04-04Jonas Wallden  args.tag = tag; tagged_callbacks[tag] = [ fn, group ]; } else { if (debug_log)
35c0b22016-11-18Anders Johansson  ROXEN.log ("AFS call: " + action + " " + YAHOO.lang.JSON.stringify (args));
5db23a2011-04-04Jonas Wallden  } args[session_var] = session; open_connections++;
84d3bf2011-04-27Martin Stjernholm  var url = actions_prefix + action + "?" + encode_afs_args (args);
5db23a2011-04-04Jonas Wallden  var con = YAHOO.util.Connect.asyncRequest ( method, url, { cache: false, success: request_success, failure: request_failure }, postdata); if (fn && group) { if (!groups[group]) groups[group] = [ ]; groups[group].push(args.tag); } return con; } /** * Abort all requests created with specified group. * * @param {String} group * Group identifier. */ function abort(group_name) { var group = groups[group_name]; if (!group) return; for (var i = 0; i < group.length; i++) { tagged_callbacks[group[i]][0] = -1; } delete groups[group_name]; }
35c0b22016-11-18Anders Johansson 
5db23a2011-04-04Jonas Wallden  /** * Returns the status of the AFS connection, without querying the * server. * * @return {Boolean} * Returns true if the last AFS call returned successfully, false * otherwise. */ function has_connection() { return connection_ok; } /** * Adds a global callback, i.e. a callback that will be called for * every AFS response from the server. * * @param {Function} fn * Callback function. It will be called with a single argument * that is the AFS response message in JSON. * @param {Object} scope * Optional scope correction. * @return {Function} * Returns the function actually added, which can be used in a later * call to remove_global_callback. */ function add_global_callback (fn, scope) { if (scope) fn = ROXEN.bind (scope, fn); global_callbacks.push (fn); return fn; } /** * Removes a callback from the set of global callbacks. * * @param {Function} fn * Callback to remove. Nothing happens if it doesn't match any * registered callback. */ function remove_global_callback (fn) { for (var i = 0; i < global_callbacks.length; i++) if (global_callbacks[i] === fn) {
35c0b22016-11-18Anders Johansson  global_callbacks.splice (i, 1); break;
5db23a2011-04-04Jonas Wallden  } } /** * Adds a callback that will be called whenever there is a low-level * AFS error; either a connection error or a syntax error in a JSON * response. * * @param {Function} fn * Callback function. It will be called with a single argument
36dc272015-12-14Jonas Walldén  * which is either a YAHOO.util.Connect.asyncRequest or XMLHttpRequest * failure handler response, or an exception object thrown by
5db23a2011-04-04Jonas Wallden  * YAHOO.lang.JSON.parse. * @param {Object} scope * Optional scope correction. * @return {Function} * Returns the function actually added, which can be used in a later * call to remove_global_callback. */ function add_error_callback (fn, scope) { if (scope) fn = ROXEN.bind (scope, fn); error_callbacks.push (fn); return fn; } /** * Removes a callback from the set of callbacks called for low-level * AFS errors. * * @param {Function} fn * Callback to remove. Nothing happens if it doesn't match any * registered callback. */ function remove_error_callback (fn) { for (var i = 0; i < error_callbacks.length; i++) if (error_callbacks[i] === fn) {
35c0b22016-11-18Anders Johansson  error_callbacks.splice (i, 1); break;
5db23a2011-04-04Jonas Wallden  } } function poll_callback (response) { if (poll_timeout == -1 && !ROXEN.isUndefined (response.poll_interval)) poll_delay = response.poll_interval; } function restart_poll (after_error) { if (poll_timeout >= -1 && !poll_delay_timeout_id) { var delay = (after_error ? poll_error_delay : poll_delay) * 1000; if (debug_log > 2)
35c0b22016-11-18Anders Johansson  ROXEN.log ("AFS poll delay " + delay + " ms");
5db23a2011-04-04Jonas Wallden  poll_delay_timeout_id =
35c0b22016-11-18Anders Johansson  window.setTimeout (function () { poll_delay_timeout_id = undefined; call ("poll", {timeout : poll_timeout, interval : poll_delay}, poll_callback); }, delay);
5db23a2011-04-04Jonas Wallden  } } /** * (Re)initializes the AFS lib. All open connections are forgotten, * and a poll action is started right away (provided polling is * enabled). * * Global and error callbacks are not forgotten, and responses from * old open connections can still be delivered to them (subject to * change if necessary). *
9b44092015-06-26Jonas Walldén  * @param {String} actions_prefix Path prefix for all AFS actions. * If not provided /actions/ is used. * @param {String} session_var Session variable name. Will be set to * "session_var" If not provided. * @param {Number} poll_timeout Poll timeout in seconds, or -1 for * auto mode (default), and -2 to disable.
5db23a2011-04-04Jonas Wallden  */ function init(options) { if (options) {
55209f2016-11-18Anders Johansson  if (options.actions_prefix) actions_prefix = options.actions_prefix; if (options.session_var) session_var = options.session_var; if (options.poll_timeout) poll_timeout = options.poll_timeout;
5db23a2011-04-04Jonas Wallden  }
35c0b22016-11-18Anders Johansson 
5db23a2011-04-04Jonas Wallden  if (debug_log)
9b44092015-06-26Jonas Walldén  ROXEN.log ("AFS init");
5db23a2011-04-04Jonas Wallden  connection_ok = false; open_connections = 0; tagged_callbacks = {}; if (poll_delay_timeout_id) { clearTimeout (poll_delay_timeout_id); poll_delay_timeout_id = undefined; } if (poll_timeout >= -1 && !open_connections) call ("poll",
35c0b22016-11-18Anders Johansson  {timeout : poll_timeout, interval : poll_delay}, poll_callback);
ff60d72016-09-26Anders Johansson  is_initialized = true; } /** * Will return true after init() has been called once. */ function initialized() { return is_initialized;
5db23a2011-04-04Jonas Wallden  } /** * Makes an asynchronous GET request to any URL. * * Note that this really has nothing to do with AFS. * * @method request * @param {String} url Requested URL. * @param {Function} fn Callback function to run when done. * fn's argument list is (result, status). * status is AFS_REQUEST_SUCCESS or * AFS_REQUEST_FAILURE. * @param {Object} scope Scope correction (optional). * @return {Object} Returns the connection object. */ function request(url, fn, scope) { var cb = { cache: false, success: function (o) { fn.call(scope, o, "AFS_REQUEST_SUCCESS"); }, failure: function (o) { ROXEN.log("ROXEN.AFS: connection error: " +
9b44092015-06-26Jonas Walldén  o.status + " " + o.statusText);
5db23a2011-04-04Jonas Wallden  fn.call(scope, o, "AFS_REQUEST_FAILURE"); } }; return YAHOO.util.Connect.asyncRequest("GET", url, cb); } return { call: call, post: post,
36dc272015-12-14Jonas Walldén  post_files: post_files,
5db23a2011-04-04Jonas Wallden  abort: abort, has_connection: has_connection, add_global_callback: add_global_callback, remove_global_callback: remove_global_callback, add_error_callback: add_error_callback, remove_error_callback: remove_error_callback, init: init,
ff60d72016-09-26Anders Johansson  initialized: initialized,
5db23a2011-04-04Jonas Wallden  request: request }; }();