1 /*global jQuery: true, GCN: true */
  2 /**
  3  * admin.js defines the AdminAPI, that will
  4  * give access to global data (like all existing constructs).
  5  * A single instance is of this API is exposed as GCN.Admin
  6  */
  7 (function (GCN) {
  8 
  9 	'use strict';
 10 
 11 	/**
 12 	 * Maps constructcategories that were fetched via the Rest API
 13 	 * into a sorted nested array of constructs.
 14 	 *
 15 	 * @param {object<string, object>} constructs
 16 	 * @return {object<string, object>}
 17 	 */
 18 	function mapConstructCategories(constructs) {
 19 		var constructKeyword;
 20 		var categoryMap = { categories: {}, categorySortorder: [] };
 21 		var constructCategoryArray = [];
 22 
 23 		for (constructKeyword in constructs) {
 24 			if (constructs.hasOwnProperty(constructKeyword)) {
 25 				var construct = constructs[constructKeyword];
 26 
 27 				var constructCategoryName = construct.category;
 28 				var categorySortorder = construct.categorySortorder;
 29 
 30 				// Use a custom name for constructs that have not been assigned to a category
 31 				if (!constructCategoryName) {
 32 					constructCategoryName = "GCN_UNCATEGORIZED";
 33 					categorySortorder = -1;
 34 				}
 35 
 36 				// Initialize the inner array of constructs
 37 				if (!categoryMap.categories[constructCategoryName]) {
 38 					var newCategory = {};
 39 					newCategory.constructs = {};
 40 					newCategory.sortorder = categorySortorder;
 41 					newCategory.name = constructCategoryName;
 42 					categoryMap.categories[constructCategoryName] = newCategory;
 43 					constructCategoryArray.push(newCategory);
 44 				}
 45 
 46 				// Add the construct to the category
 47 				categoryMap.categories[constructCategoryName].constructs[constructKeyword] = construct;
 48 			}
 49 		}
 50 
 51 		// Sort the categories by the sortorder
 52 		constructCategoryArray.sort(function (a, b) {
 53 			return a.sortorder - b.sortorder;
 54 		});
 55 
 56 		// Add the sorted category names to the sortorder field
 57 		var k;
 58 		for (k in constructCategoryArray) {
 59 			if (constructCategoryArray.hasOwnProperty(k)) {
 60 				var category = constructCategoryArray[k];
 61 				if (typeof category.sortorder !== 'undefined' && category.sortorder !== -1) {
 62 					categoryMap.categorySortorder.push(category.name);
 63 				}
 64 			}
 65 		}
 66 
 67 		return categoryMap;
 68 	}
 69 
 70 	/**
 71 	 * Maps constructs, that were fetched via the Rest API, using their keyword
 72 	 * as the keys.
 73 	 *
 74 	 * @param {object<string, object>} constructs Consturcts mapped against
 75 	 *                                            their id.
 76 	 * @return {object<string, object>} Constructs mapped against their keys.
 77 	 */
 78 	function mapConstructs(constructs) {
 79 		if (!constructs) {
 80 			return {};
 81 		}
 82 		var map = {};
 83 		var constructId;
 84 		for (constructId in constructs) {
 85 			if (constructs.hasOwnProperty(constructId)) {
 86 				map[constructs[constructId].keyword] = constructs[constructId];
 87 			}
 88 		}
 89 		return map;
 90 	}
 91 
 92 	/**
 93 	 * The AdminAPI is exposed via {@link GCN.Admin}. The AdminAPI is not meant
 94 	 * to be instatiated by the implementer. Stick to using GCN.Admin.
 95 	 * 
 96 	 * @name AdminAPI
 97 	 * @class
 98 	 * @augments Chainback
 99 	 */
100 	var AdminAPI = GCN.defineChainback({
101 		/** @lends AdminAPI */
102 
103 		__chainbacktype__: 'AdminAPI',
104 		_type: 'admin',
105 
106 		/**
107 		 * @private
108 		 * @type {object<string, number} Constructs for this node are cached
109 		 *                               here so that we only need to fetch
110 		 *                               this once.
111 		 */
112 		_constructs: null,
113 
114 		/**
115 		 * @private
116 		 * @type {object<string, object} Constructs categories for this node.
117 		 *                               Cached here so that we only need to
118 		 *                               fetch this once.
119 		 */
120 		_constructCategories: null,
121 
122 		/**
123 		 * Initialize
124 		 * @private
125 		 */
126 		_init: function (id, success, error, settings) {
127 			if (typeof success === 'function') {
128 				this._invoke(success, [this]);
129 			}
130 		},
131 
132 		/**
133 		 * Wrapper for internal chainback _ajax method.
134 		 *
135 		 * @private
136 		 * @param {object<string, *>} settings Settings for the ajax request.
137 		 *                                     The settings object is identical
138 		 *                                     to that of the `GCN.ajax'
139 		 *                                     method, which handles the actual
140 		 *                                     ajax transportation.
141 		 * @throws AJAX_ERROR
142 		 */
143 		'!_ajax': function (settings) {
144 			var that = this;
145 
146 			// force no cache for all API calls
147 			settings.cache = false;
148 			settings.success = (function (onSuccess, onError) {
149 				return function (data) {
150 					// Ajax calls that do not target the REST API servlet do
151 					// not response data with a `responseInfo' object.
152 					// "/CNPortletapp/alohatag" is an example.  So we cannot
153 					// just assume that it exists.
154 					if (data.responseInfo) {
155 						switch (data.responseInfo.responseCode) {
156 						case 'OK':
157 							break;
158 						case 'AUTHREQUIRED':
159 							GCN.clearSession();
160 							that._authAjax(settings);
161 							return;
162 						default:
163 							GCN.handleResponseError(data, onError);
164 							return;
165 						}
166 					}
167 
168 					if (onSuccess) {
169 						that._invoke(onSuccess, [data]);
170 					}
171 				};
172 			}(settings.success, settings.error, settings.url));
173 
174 			this._queueAjax(settings);
175 		},
176 
177 		/**
178 		 * Similar to `_ajax', except that it prefixes the ajax url with the
179 		 * current session's `sid', and will trigger an
180 		 * `authentication-required' event if the session is not authenticated.
181 		 *
182 		 * @TODO(petro): Consider simplifiying this function signature to read:
183 		 *               `_auth( url, success, error )'
184 		 *
185 		 * @private
186 		 * @param {object<string, *>} settings Settings for the ajax request.
187 		 * @throws AUTHENTICATION_FAILED
188 		 */
189 		_authAjax: function (settings) {
190 			var that = this;
191 
192 			if (GCN.isAuthenticating) {
193 				GCN.afterNextAuthentication(function () {
194 					that._authAjax(settings);
195 				});
196 
197 				return;
198 			}
199 
200 			if (!GCN.sid) {
201 				var cancel;
202 
203 				if (settings.error) {
204 					/**
205 					 * @ignore
206 					 */
207 					cancel = function (error) {
208 						GCN.handleError(
209 							error || GCN.createError('AUTHENTICATION_FAILED'),
210 							settings.error
211 						);
212 					};
213 				} else {
214 					/**
215 					 * @ignore
216 					 */
217 					cancel = function (error) {
218 						if (error) {
219 							GCN.error(error.code, error.message, error.data);
220 						} else {
221 							GCN.error('AUTHENTICATION_FAILED');
222 						}
223 					};
224 				}
225 
226 				GCN.afterNextAuthentication(function () {
227 					that._authAjax(settings);
228 				});
229 
230 				if (GCN.usingSSO) {
231 					// First, try to automatically authenticate via
232 					// Single-SignOn
233 					GCN.loginWithSSO(GCN.onAuthenticated, function () {
234 						// ... if SSO fails, then fallback to requesting user
235 						// credentials: broadcast `authentication-required'
236 						// message.
237 						GCN.authenticate(cancel);
238 					});
239 				} else {
240 					// Trigger the `authentication-required' event to request
241 					// user credentials.
242 					GCN.authenticate(cancel);
243 				}
244 
245 				return;
246 			}
247 
248 			// Append "?sid=..." or "&sid=..." if needed.
249 
250 			var urlFragment = settings.url.substr(
251 				GCN.settings.BACKEND_PATH.length
252 			);
253 			var isSidInUrl = /[\?\&]sid=/.test(urlFragment);
254 			if (!isSidInUrl) {
255 				var isFirstParam = (jQuery.inArray('?',
256 					urlFragment.split('')) === -1);
257 
258 				settings.url += (isFirstParam ? '?' : '&') + 'sid='
259 				             + (GCN.sid || '');
260 			}
261 
262 			this._ajax(settings);
263 		},
264 
265 		/**
266 		 * Retrieves a list of all constructs and constructs categories and
267 		 * passes it as the only argument into the `success()' callback.
268 		 *
269 		 * @param {function(Array.<object>)=} success Callback to receive an
270 		 *                                            array of constructs.
271 		 * @param {function(GCNError):boolean=} error Custom error handler.
272 		 * @return undefined
273 		 * @throws INVALID_ARGUMENTS
274 		 */
275 		constructs: function (success, error) {
276 			var that = this;
277 			if (!success) {
278 				GCN.error('INVALID_ARGUMENTS', 'the `constructs()\' method ' +
279 					'requires at least a success callback to be given');
280 			}
281 			if (this._constructs) {
282 				this._invoke(success, [this._constructs]);
283 			} else {
284 				this._authAjax({
285 					url: GCN.settings.BACKEND_PATH + '/rest/construct/list.json',
286 					type: 'GET',
287 					error: function (xhr, status, msg) {
288 						GCN.handleHttpError(xhr, msg, error);
289 					},
290 					success: function (response) {
291 						if (GCN.getResponseCode(response) === 'OK') {
292 							that._constructs = mapConstructs(response.constructs);
293 							that._invoke(success, [that._constructs]);
294 						} else {
295 							GCN.handleResponseError(response, error);
296 						}
297 					}
298 				});
299 			}
300 		},
301 
302 		/**
303 		 * Helper method that will load the constructs of this node.
304 		 *
305 		 * @private
306 		 * @this {AdminAPI}
307 		 * @param {function(Array.<object>)} success callback
308 		 * @param {function(GCNError):boolean=} error callback
309 		 */
310 		constructCategories: function (success, error) {
311 			if (this._constructCategories) {
312 				this._invoke(success, [this._constructCategories]);
313 			} else {
314 				var that = this;
315 				this.constructs(function (constructs) {
316 					that._constructCategories = mapConstructCategories(constructs);
317 					that._invoke(success, [that._constructCategories]);
318 				}, error);
319 			}
320 		},
321 
322 		/**
323 		 * Internal method, to fetch this object's data from the server.
324 		 *
325 		 * @private
326 		 * @override
327 		 * @param {function(ContentObjectAPI)=} success Optional callback that
328 		 *                                              receives this object as
329 		 *                                              its only argument.
330 		 * @param {function(GCNError):boolean=} error Optional customer error
331 		 *                                            handler.
332 		 */
333 		'!_read': function (success, error) {
334 			this._invoke(success, [this]);
335 		}
336 	});
337 
338 	/**
339 	 * Exposes an instance of the AdminAPI via GCN.Admin.
340 	 * 
341 	 * @see AdminAPI
342 	 */
343 	GCN.Admin = new AdminAPI();
344 
345 }(GCN));
346