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