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