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 							GCN.handleResponseError(data, onError);
168 							return;
169 						}
170 					}
171 
172 					if (onSuccess) {
173 						that._invoke(onSuccess, [data]);
174 					}
175 				};
176 			}(settings.success, settings.error, settings.url));
177 
178 			this._queueAjax(settings);
179 		},
180 
181 		/**
182 		 * Similar to `_ajax', except that it prefixes the ajax url with the
183 		 * current session's `sid', and will trigger an
184 		 * `authentication-required' event if the session is not authenticated.
185 		 *
186 		 * @private
187 		 * @param {object<string, *>} settings Settings for the ajax request.
188 		 * @throws AUTHENTICATION_FAILED
189 		 */
190 		_authAjax: function (settings) {
191 			var that = this;
192 
193 			if (GCN.isAuthenticating) {
194 				GCN.afterNextAuthentication(function () {
195 					that._authAjax(settings);
196 				});
197 
198 				return;
199 			}
200 
201 			if (!GCN.sid) {
202 				var cancel;
203 
204 				if (settings.error) {
205 					/**
206 					 * @ignore
207 					 */
208 					cancel = function (error) {
209 						GCN.handleError(
210 							error || GCN.createError('AUTHENTICATION_FAILED'),
211 							settings.error
212 						);
213 					};
214 				} else {
215 					/**
216 					 * @ignore
217 					 */
218 					cancel = function (error) {
219 						if (error) {
220 							GCN.error(error.code, error.message, error.data);
221 						} else {
222 							GCN.error('AUTHENTICATION_FAILED');
223 						}
224 					};
225 				}
226 
227 				GCN.afterNextAuthentication(function () {
228 					that._authAjax(settings);
229 				});
230 
231 				if (GCN.usingSSO) {
232 					// First, try to automatically authenticate via
233 					// Single-SignOn
234 					GCN.loginWithSSO(GCN.onAuthenticated, function () {
235 						// ... if SSO fails, then fallback to requesting user
236 						// credentials: broadcast `authentication-required'
237 						// message.
238 						GCN.authenticate(cancel);
239 					});
240 				} else {
241 					// Trigger the `authentication-required' event to request
242 					// user credentials.
243 					GCN.authenticate(cancel);
244 				}
245 
246 				return;
247 			}
248 
249 			// Append "?sid=..." or "&sid=..." if needed.
250 
251 			var urlFragment = settings.url.substr(
252 				GCN.settings.BACKEND_PATH.length
253 			);
254 			var isSidInUrl = /[\?\&]sid=/.test(urlFragment);
255 			if (!isSidInUrl) {
256 				var isFirstParam = (jQuery.inArray('?',
257 					urlFragment.split('')) === -1);
258 
259 				settings.url += (isFirstParam ? '?' : '&') + 'sid='
260 				             + (GCN.sid || '');
261 			}
262 
263 			this._ajax(settings);
264 		},
265 
266 		/**
267 		 * Retrieves a list of all constructs and constructs categories and
268 		 * passes it as the only argument into the `success()' callback.
269 		 *
270 		 * @param {function(Array.<object>)=} success Callback to receive an
271 		 *                                            array of constructs.
272 		 * @param {function(GCNError):boolean=} error Custom error handler.
273 		 * @return undefined
274 		 * @throws INVALID_ARGUMENTS
275 		 */
276 		constructs: function (success, error) {
277 			var that = this;
278 			if (!success) {
279 				GCN.error('INVALID_ARGUMENTS', 'the `constructs()\' method ' +
280 					'requires at least a success callback to be given');
281 			}
282 			if (this._constructs) {
283 				this._invoke(success, [this._constructs]);
284 			} else {
285 				this._authAjax({
286 					url: GCN.settings.BACKEND_PATH + '/rest/construct/list.json',
287 					type: 'GET',
288 					error: function (xhr, status, msg) {
289 						GCN.handleHttpError(xhr, msg, error);
290 					},
291 					success: function (response) {
292 						if (GCN.getResponseCode(response) === 'OK') {
293 							that._constructs = mapConstructs(response.constructs);
294 							that._invoke(success, [that._constructs]);
295 						} else {
296 							GCN.handleResponseError(response, error);
297 						}
298 					}
299 				});
300 			}
301 		},
302 
303 		/**
304 		 * Helper method that will load the constructs of this node.
305 		 *
306 		 * @private
307 		 * @this {AdminAPI}
308 		 * @param {function(Array.<object>)} success callback
309 		 * @param {function(GCNError):boolean=} error callback
310 		 */
311 		constructCategories: function (success, error) {
312 			if (this._constructCategories) {
313 				this._invoke(success, [this._constructCategories]);
314 			} else {
315 				var that = this;
316 				this.constructs(function (constructs) {
317 					that._constructCategories = mapConstructCategories(constructs);
318 					that._invoke(success, [that._constructCategories]);
319 				}, error);
320 			}
321 		},
322 
323 		/**
324 		 * Internal method, to fetch this object's data from the server.
325 		 *
326 		 * @private
327 		 * @override
328 		 * @param {function(ContentObjectAPI)=} success Optional callback that
329 		 *                                              receives this object as
330 		 *                                              its only argument.
331 		 * @param {function(GCNError):boolean=} error Optional customer error
332 		 *                                            handler.
333 		 */
334 		'!_read': function (success, error) {
335 			this._invoke(success, [this]);
336 		}
337 	});
338 
339 	/**
340 	 * Exposes an instance of the AdminAPI via GCN.Admin.
341 	 * 
342 	 * @see AdminAPI
343 	 */
344 	GCN.Admin = new AdminAPI();
345 
346 }(GCN));
347