1 (function (GCN) {
  2 
  3 	'use strict';
  4 
  5 	/**
  6 	 * Updates the internal data of the given content object.
  7 	 *
  8 	 * This function extends and overwrites properties of the instances's
  9 	 * internal data structure.  No property is deleted on account of being
 10 	 * absent from the given `props' object.
 11 	 *
 12 	 * @param {ContentObjectAPI} obj An instance whose internal data is to be
 13 	 *                               reset.
 14 	 * @param {object} props The properties with which to replace the internal
 15 	 *                       data of the given chainback instance.
 16 	 */
 17 	function update(obj, props) {
 18 		jQuery.extend(obj._data, props);
 19 	}
 20 
 21 	/**
 22 	 * The prefix that will be temporarily applied to block tags during an
 23 	 * encode() process.
 24 	 *
 25 	 * @type {string}
 26 	 * @const
 27 	 */
 28 	var BLOCK_ENCODING_PREFIX = 'GCN_BLOCK_TMP__';
 29 
 30 	/**
 31 	 * Will match <span id="GENTICS_block_123"></span>" but not "<node abc123>"
 32 	 * tags.  The first backreference contains the tagname of the tag
 33 	 * corresponding to this block.
 34 	 *
 35 	 * Limitation: Will not work with unicode characters.
 36 	 *
 37 	 * @type {RexExp}
 38 	 * @const
 39 	 */
 40     var CONTENT_BLOCK = new RegExp(
 41 			// "<span" or "<div" but not "<node"
 42 			'<(?!node)[a-z]+'            +
 43 				// "class=... data-*..."
 44 				'(?:\\s+[^/<>\\s=]+(?:=(?:"[^"]*"|\'[^\']*\'|[^>/\\s]+))?)*?' +
 45 				// " id = "
 46 				'\\s+id\\s*=\\s*["\']?'  +
 47 				// "GCN_BLOCK_TMP__"
 48 				BLOCK_ENCODING_PREFIX    +
 49 				// "_abc-123"
 50 				'([^"\'/<>\\s=]*)["\']?' +
 51 				// class=... data-*...
 52 				'(?:\\s+[^/<>\\s=]+(?:=(?:"[^"]*"|\'[^\']*\'|[^>/\\s]+))?)*' +
 53 				// "' ...></span>" or "</div>"
 54 				'\\s*></[a-z]+>',
 55 			'gi'
 56 		);
 57 
 58 	/**
 59 	 * Will match <node foo> or <node bar_123> or <node foo-bar> but not
 60 	 * <node "blah">.
 61 	 *
 62 	 * @type {RegExp}
 63 	 * @const
 64 	 */
 65 	var NODE_NOTATION = /<node ([a-z0-9_\-]+?)>/gim;
 66 
 67 	/**
 68 	 * Examines a string for "<node>" tags, and for each occurance of this
 69 	 * notation, the given callback will be invoked to manipulate the string.
 70 	 *
 71 	 * @private
 72 	 * @static
 73 	 * @param {string} str The string that will be examined for "<node>" tags.
 74 	 * @param {function} onMatchFound Callback function that should receive the
 75 	 *                                following three parameters:
 76 	 *
 77 	 *                    name:string The name of the tag being notated by the
 78 	 *                                node substring.  If the `str' arguments
 79 	 *                                is "<node myTag>", then the `name' value
 80 	 *                                will be "myTag".
 81 	 *                  offset:number The offset where the node substring was
 82 	 *                                found within the examined string.
 83 	 *                     str:string The string in which the "<node *>"
 84 	 *                                substring occured.
 85 	 *
 86 	 *                                The return value of the function will
 87 	 *                                replace the entire "<node>" substring
 88 	 *                                that was passed to it within the examined
 89 	 *                                string.
 90 	 */
 91 	function replaceNodeTags(str, onMatchFound) {
 92 		var parsed = str.replace(NODE_NOTATION, function (substr, tagname,
 93 		                                                  offset, examined) {
 94 				return onMatchFound(tagname, offset, examined);
 95 			});
 96 		return parsed;
 97 	}
 98 
 99 	/*
100 	 * have a look at _init 
101 	 */
102 	GCN.ContentObjectAPI = GCN.defineChainback({
103 		/** @lends ContentObjectAPI */
104 
105 		/**
106 		 * @private
107 		 * @type {string} A string denoting a content node type.  This value is
108 		 *                used to compose the correct REST API ajax urls.  The
109 		 *                following are valid values: "node", "folder",
110 		 *                "template", "page", "file", "image".
111 		 */
112 		_type: null,
113 
114 		/**
115 		 * @private
116 		 * @type {object<string,*>} An internal object to store data that we
117 		 *                          get from the server.
118 		 */
119 		_data: {},
120 
121 		/**
122 		 * @private
123 		 * @type {object<string,*>} An internal object to store updates to
124 		 *                          the content object.  Should reflect the
125 		 *                          structural typography of the `_data'
126 		 *                          object.
127 		 */
128 		_shadow: {},
129 
130 		/**
131 		 * @type {boolean} Flags whether or not data for this content object have
132 		 *                 been fetched from the server.
133 		 */
134 		_fetched: false,
135 
136 		/**
137 		 * @private
138 		 * @type {object} will contain an objects internal settings
139 		 */
140 		_settings: null,
141 
142 		/**
143 		 * An array of all properties of an object that can be changed by the
144 		 * user. Writeable properties for all content objects.
145 		 * 
146 		 * @public
147 		 * @type {Array.string}
148 		 */
149 		WRITEABLE_PROPS: [],
150 
151 		/**
152 		 * <p>This object can contain various contrains for writeable props. 
153 		 * Those contrains will be checked when the user tries to set/save a
154 		 * property. Currently only maxLength is beeing handled.</p>
155 		 *
156 		 * <p>Example:</p>
157 		 * <pre>WRITEABLE_PROPS_CONSTRAINTS: {
158 		 *    'name': {
159 		 *        maxLength: 255
160 		 *     } 
161 		 * }</pre>
162 		 * @type {object}
163 		 * @const
164 		 *
165 		 */
166 		WRITEABLE_PROPS_CONSTRAINTS: {},
167 
168 		/**
169 		 * Fetches this content object's data from the backend.
170 		 *
171 		 * @ignore
172 		 * @param {function(object)} success A function to receive the server
173 		 *                                   response.
174 		 * @param {function(GCNError):boolean} error Optional custrom error
175 		 *                                           handler.
176 		 */
177 		'!fetch': function (success, error, stack) {
178 			var obj = this;
179 			var ajax = function () {
180 				obj._authAjax({
181 					url: GCN.settings.BACKEND_PATH + '/rest/' + obj._type +
182 					     '/load/' + obj.id() + GCN._getChannelParameter(obj),
183 					data: obj._loadParams(),
184 					error: error,
185 					success: success
186 				});
187 			};
188 
189 			// If this chainback object has an ancestor, then invoke that
190 			// parent's `_read()' method before fetching the data for this
191 			// chainback object.
192 			if (obj._chain) {
193 				var circularReference =
194 						stack && -1 < jQuery.inArray(obj._chain, stack);
195 				if (!circularReference) {
196 					stack = stack || [];
197 					stack.push(obj._chain);
198 					obj._chain._read(ajax, error, stack);
199 					return;
200 				}
201 			}
202 
203 			ajax();
204 		},
205 
206 		/**
207 		 * Internal method, to fetch this object's data from the server.
208 		 *
209 		 * @ignore
210 		 * @private
211 		 * @param {function(ContentObjectAPI)=} success Optional callback that
212 		 *                                              receives this object as
213 		 *                                              its only argument.
214 		 * @param {function(GCNError):boolean=} error Optional customer error
215 		 *                                            handler.
216 		 */
217 		'!_read': function (success, error, stack) {
218 			var obj = this;
219 			if (obj._fetched) {
220 				if (success) {
221 					obj._invoke(success, [obj]);
222 				}
223 				return;
224 			}
225 
226 			if (obj.multichannelling) {
227 				obj.multichannelling.read(obj, success, error);
228 				return;
229 			}
230 
231 			var id = obj.id();
232 
233 			if (null === id || undefined === id) {
234 				obj._getIdFromParent(function () {
235 					obj._read(success, error, stack);
236 				}, error, stack);
237 				return;
238 			}
239 
240 			obj.fetch(function (response) {
241 				obj._processResponse(response);
242 				obj._fetched = true;
243 				if (success) {
244 					obj._invoke(success, [obj]);
245 				}
246 			}, error, stack);
247 		},
248 
249 		/**
250 		 * Retrieves this object's id from its parent.  This function is used
251 		 * in order for this object to be able to fetch its data from the
252 		 * backend.
253 		 *
254 		 * @ignore
255 		 * @private
256 		 * @param {function(ContentObjectAPI)=} success Optional callback that
257 		 *                                              receives this object as
258 		 *                                              its only argument.
259 		 * @param {function(GCNError):boolean=} error Optional customer error
260 		 *                                            handler.
261 		 * @throws CANNOT_GET_OBJECT_ID
262 		 */
263 		'!_getIdFromParent': function (success, error, stack) {
264 			var parent = this._ancestor();
265 
266 			if (!parent) {
267 				var err = GCN.createError('CANNOT_GET_OBJECT_ID',
268 					'Cannot get an id for object', this);
269 				GCN.handleError(err, error);
270 				return;
271 			}
272 
273 			var that = this;
274 
275 			parent._read(function () {
276 				if ('folder' === that._type) {
277 					// There are 3 possible property names that an object can
278 					// use to hold the id of the folder that it is related to:
279 					//
280 					// "folderId": for pages, templates, files, and images.
281 					// "motherId": for folders
282 					// "nodeId":   for nodes
283 					//
284 					// We need to see which of this properties is set, the
285 					// first one we find will be our folder's id.
286 					var props = ['folderId', 'motherId', 'nodeId'];
287 					var prop = props.pop();
288 					var id;
289 
290 					while (prop) {
291 						id = parent.prop(prop);
292 						if (typeof id !== 'undefined') {
293 							break;
294 						}
295 						prop = props.pop();
296 					}
297 
298 					that._data.id = id;
299 				} else {
300 					that._data.id = parent.prop(that._type + 'Id');
301 				}
302 
303 				if (that._data.id === null || typeof that._data.id === 'undefined') {
304 					var err = GCN.createError('CANNOT_GET_OBJECT_ID',
305 						'Cannot get an id for object', this);
306 					GCN.handleError(err, error);
307 					return;
308 				}
309 
310 				that._setHash(that._data.id)._addToCache();
311 
312 				if (success) {
313 					success();
314 				}
315 			}, error, stack);
316 		},
317 
318 		/**
319 		 * Gets this object's node id. If used in a multichannelling is enabled
320 		 * it will return the channel id or 0 if no channel was set.
321 		 * 
322 		 * @public
323 		 * @function
324 		 * @name nodeId
325 		 * @memberOf ContentObjectAPI
326 		 * @return {number} The channel to which this object is set. 0 if no
327 		 *         channel is set.
328 		 */
329 		'!nodeId': function () {
330 			return this._channel || 0;
331 		},
332 
333 		/**
334 		 * Gets this object's id. We'll return the id of the object when it has
335 		 * been loaded - this can only be a localid. Otherwise we'll return the
336 		 * id which was provided by the user. This can either be a localid or a
337 		 * globalid.
338 		 *
339 		 * @name id
340 		 * @function
341 		 * @memberOf ContentObjectAPI
342 		 * @public
343 		 * @return {number}
344 		 */
345 		'!id': function () {
346 			return this._data.id;
347 		},
348 
349 		/**
350 		 * Alias for {@link ContentObjectAPI#id}
351 		 *
352 		 * @name localId
353 		 * @function
354 		 * @memberOf ContentObjectAPI
355 		 * @private
356 		 * @return {number}
357 		 * @decprecated
358 		 */
359 		'!localId': function () {
360 			return this.id();
361 		},
362 
363 		/**
364 		 * Update the `_shadow' object that maintains changes to properties
365 		 * that reflected the internal `_data' object.  This shadow object is
366 		 * used to persist differential changes to a REST API object.
367 		 *
368 		 * @ignore
369 		 * @private
370 		 * @param {string} path The path through the object to the property we
371 		 *                      want to modify if a node in the path contains
372 		 *                      dots, then these dots should be escaped.  This
373 		 *                      can be done using the GCN.escapePropertyName()
374 		 *                      convenience function.
375 		 * @param {*} value The value we wish to set the property to.
376 		 * @param {function=} error Custom error handler.
377 		 * @param {boolean=} force If true, no error will be thrown if `path'
378 		 *                         cannot be fully resolved against the
379 		 *                         internal `_data' object, instead, the path
380 		 *                         will be created on the shadow object.
381 		 */
382 		'!_update': function (pathStr, value, error, force) {
383 			var boundary = Math.random().toString(8).substring(2);
384 			var path = pathStr.replace(/\./g, boundary)
385 			                  .replace(new RegExp('\\\\' + boundary, 'g'), '.')
386 			                  .split(boundary);
387 			var shadow = this._shadow;
388 			var actual = this._data;
389 			var i = 0;
390 			var numPathNodes = path.length;
391 			var pathNode;
392 			// Whether or not the traversal path in `_data' and `_shadow' are
393 			// at the same position in the respective objects.
394 			var areMirrored = true;
395 
396 			while (true) {
397 				pathNode = path[i++];
398 
399 				if (areMirrored) {
400 					actual = actual[pathNode];
401 					areMirrored = jQuery.type(actual) !== 'undefined';
402 				}
403 
404 				if (i === numPathNodes) {
405 					break;
406 				}
407 
408 				if (shadow[pathNode]) {
409 					shadow = shadow[pathNode];
410 				} else if (areMirrored || force) {
411 					shadow = (shadow[pathNode] = {});
412 				} else {
413 					break; // goto error
414 				}
415 			}
416 
417 			if (i === numPathNodes && (areMirrored || force)) {
418 				shadow[pathNode] = value;
419 			} else {
420 				var err = GCN.createError('TYPE_ERROR', 'Object "' +
421 					path.slice(0, i).join('.') + '" does not exist',
422 					actual);
423 				GCN.handleError(err, error);
424 			}
425 		},
426 
427 		/**
428 		 * Receives the response from a REST API request, and stores it in the
429 		 * internal `_data' object.
430 		 *
431 		 * @private
432 		 * @param {object} data Parsed JSON response data.
433 		 */
434 		'!_processResponse': function (data) {
435 			jQuery.extend(this._data, data[this._type]);
436 		},
437 
438 		/**
439 		 * Specifies a list of parameters that will be added to the url when
440 		 * loading the content object from the server.
441 		 *
442 		 * @private
443 		 * @return {object} object With parameters to be appended to the load
444 		 *                         request
445 		 */
446 		'!_loadParams': function () {},
447 
448 		/**
449 		 * Reads the property `property' of this content object if this
450 		 * property is among those in the WRITEABLE_PROPS array. If a second
451 		 * argument is provided, them the property is updated with that value.
452 		 *
453 		 * @name prop
454 		 * @function
455 		 * @memberOf ContentObjectAPI
456 		 * @param {String} property Name of the property to be read or updated.
457 		 * @param {String} value Optional value to set property to. If omitted the property will just be read.
458 		 * @param {function(GCNError):boolean=} error Custom error handler to 
459 		 *                                      stop error propagation for this
460 		 *                                      synchronous call. 
461 		 * @return {?*} Meta attribute.
462 		 * @throws UNFETCHED_OBJECT_ACCESS if the object has not been fetched from the server yet
463 		 * @throws READONLY_ATTRIBUTE whenever trying to write to an attribute that's readonly
464 		 */
465 		'!prop': function (property, value, error) {
466 			if (!this._fetched) {
467 				GCN.handleError(GCN.createError(
468 					'UNFETCHED_OBJECT_ACCESS',
469 					'Object not fetched yet.'
470 				), error);
471 				return;
472 			}
473 
474 			if (typeof value !== 'undefined') {
475 				// Check whether the property is writable
476 				if (jQuery.inArray(property, this.WRITEABLE_PROPS) >= 0) {
477 					// Check wether the property has a constraint and verify it
478 					var constraint = this.WRITEABLE_PROPS_CONSTRAINTS[property];
479 					if (constraint) {
480 						// verify maxLength
481 						if (constraint.maxLength && value.length >= constraint.maxLength) {
482 							var data = { name: property, value: value, maxLength: constraint.maxLength };
483 							var constraintError = GCN.createError('ATTRIBUTE_CONSTRAINT_VIOLATION',
484 								'Attribute "' + property + '" of ' + this._type +
485 								' is too long. The \'maxLength\' was set to {' + constraint.maxLength + '} ', data);
486 							GCN.handleError(constraintError, error);
487 							return;
488 						}
489 					}
490 					this._update(GCN.escapePropertyName(property), value);
491 				} else {
492 					GCN.handleError(GCN.createError('READONLY_ATTRIBUTE',
493 						'Attribute "' + property + '" of ' + this._type +
494 						' is read-only. Writeable properties are: ' +
495 						this.WRITEABLE_PROPS, this.WRITEABLE_PROPS), error);
496 					return;
497 				}
498 			}
499 
500 			return (
501 				(jQuery.type(this._shadow[property]) !== 'undefined'
502 					? this._shadow
503 					: this._data)[property]
504 			);
505 		},
506 
507 		/**
508 		 * Sends the a template string to the Aloha Servlet for rendering.
509 		 *
510 		 * @ignore
511 		 * @TODO: Consider making this function public.  At least one developer
512 		 *        has had need to render a custom template for a content
513 		 *        object.
514 		 *
515 		 * @private
516 		 * @param {string} template Template which will be rendered.
517 		 * @param {string} mode The rendering mode.  Valid values are "view",
518 		 *                      "edit", "pub."
519 		 * @param {function(object)} success A callback the receives the render
520 		 *                                   response.
521 		 * @param {function(GCNError):boolean} error Error handler.
522 		 */
523 		'!_renderTemplate' : function (template, mode, success, error) {
524 			var channelParam = GCN._getChannelParameter(this);
525 			var url = GCN.settings.BACKEND_PATH
526 			        + '/rest/' + this._type
527 			        + '/render/' + this.id()
528 			        + channelParam
529 			        + (channelParam ? '&' : '?')
530 			        + 'edit=' + ('edit' === mode)
531 			        + '&template=' + encodeURIComponent(template);
532 			this._authAjax({
533 				url: url,
534 				error: error,
535 				success: success
536 			});
537 		},
538 
539 		/**
540 		 * Wrapper for internal chainback _ajax method.
541 		 * 
542 		 * @ignore
543 		 * @private
544 		 * @param {object<string, *>} settings Settings for the ajax request.
545 		 *                                     The settings object is identical
546 		 *                                     to that of the `GCN.ajax'
547 		 *                                     method, which handles the actual
548 		 *                                     ajax transportation.
549 		 * @throws AJAX_ERROR
550 		 */
551 		'!_ajax': function (settings) {
552 			var that = this;
553 
554 			// force no cache for all API calls
555 			settings.cache = false;
556 			settings.success = (function (onSuccess, onError) {
557 				return function (data) {
558 					// Ajax calls that do not target the REST API servlet do
559 					// not response data with a `responseInfo' object.
560 					// "/CNPortletapp/alohatag" is an example.  So we cannot
561 					// just assume that it exists.
562 					if (data.responseInfo) {
563 						switch (data.responseInfo.responseCode) {
564 						case 'OK':
565 							break;
566 						case 'AUTHREQUIRED':
567 							GCN.clearSession();
568 							that._authAjax(settings);
569 							return;
570 						default:
571 							GCN.handleResponseError(data, onError);
572 							return;
573 						}
574 					}
575 
576 					if (onSuccess) {
577 						onSuccess(data);
578 					}
579 				};
580 			}(settings.success, settings.error, settings.url));
581 
582 			this._queueAjax(settings);
583 		},
584 
585 		/**
586 		 * Concrete implementatation of _fulfill().
587 		 *
588 		 * Resolves all promises made by this content object while ensuring
589 		 * that circularReferences, (which are completely possible, and valid)
590 		 * do not result in infinit recursion.
591 		 *
592 		 * @override
593 		 */
594 		'!_fulfill': function (success, error, stack) {
595 			var obj = this;
596 			if (obj._chain) {
597 				var circularReference =
598 						stack && -1 < jQuery.inArray(obj._chain, stack);
599 				if (!circularReference) {
600 					stack = stack || [];
601 					stack.push(obj._chain);
602 					obj._fulfill(function () {
603 						obj._read(success, error);
604 					}, error, stack);
605 					return;
606 				}
607 			}
608 			obj._read(success, error);
609 		},
610 
611 		/**
612 		 * Similar to `_ajax', except that it prefixes the ajax url with the
613 		 * current session's `sid', and will trigger an
614 		 * `authentication-required' event if the session is not authenticated.
615 		 *
616 		 * @ignore
617 		 * @TODO(petro): Consider simplifiying this function signature to read:
618 		 *               `_auth( url, success, error )'
619 		 *
620 		 * @private
621 		 * @param {object<string, *>} settings Settings for the ajax request.
622 		 * @throws AUTHENTICATION_FAILED
623 		 */
624 		_authAjax: function (settings) {
625 			var that = this;
626 
627 			if (GCN.isAuthenticating) {
628 				GCN.afterNextAuthentication(function () {
629 					that._authAjax(settings);
630 				});
631 				return;
632 			}
633 
634 			if (!GCN.sid) {
635 				var cancel;
636 
637 				if (settings.error) {
638 					/**
639 					 * @ignore
640 					 */
641 					cancel = function (error) {
642 						GCN.handleError(
643 							error || GCN.createError('AUTHENTICATION_FAILED'),
644 							settings.error
645 						);
646 					};
647 				} else {
648 					/**
649 					 * @ignore
650 					 */
651 					cancel = function (error) {
652 						if (error) {
653 							GCN.error(error.code, error.message, error.data);
654 						} else {
655 							GCN.error('AUTHENTICATION_FAILED');
656 						}
657 					};
658 				}
659 
660 				GCN.afterNextAuthentication(function () {
661 					that._authAjax(settings);
662 				});
663 
664 				if (GCN.usingSSO) {
665 					// First, try to automatically authenticate via
666 					// Single-SignOn
667 					GCN.loginWithSSO(GCN.onAuthenticated, function () {
668 						// ... if SSO fails, then fallback to requesting user
669 						// credentials: broadcast `authentication-required'
670 						// message.
671 						GCN.authenticate(cancel);
672 					});
673 				} else {
674 					// Trigger the `authentication-required' event to request
675 					// user credentials.
676 					GCN.authenticate(cancel);
677 				}
678 
679 				return;
680 			}
681 
682 			// Append "?sid=..." or "&sid=..." if needed.
683 
684 			var urlFragment = settings.url.substr(
685 				GCN.settings.BACKEND_PATH.length
686 			);
687 			var isSidInUrl = /[\?\&]sid=/.test(urlFragment);
688 			if (!isSidInUrl) {
689 				var isFirstParam = (jQuery.inArray('?',
690 					urlFragment.split('')) === -1);
691 				settings.url += (isFirstParam ? '?' : '&') + 'sid='
692 				             +  (GCN.sid || '');
693 			}
694 
695 			this._ajax(settings);
696 		},
697 
698 		/**
699 		 * Recursively call `_continueWith()'.
700 		 *
701 		 * @ignore
702 		 * @private
703 		 * @override
704 		 */
705 		'!_onContinue': function (success, error) {
706 			var that = this;
707 			this._continueWith(function () {
708 				that._read(success, error);
709 			}, error);
710 		},
711 
712 		/**
713 		 * Initializes this content object.  If a `success' callback is
714 		 * provided, it will cause this object's data to be fetched and passed
715 		 * to the callback.  This object's data will be fetched from the cache
716 		 * if is available, otherwise it will be fetched from the server.  If
717 		 * this content object API contains parent chainbacks, it will get its
718 		 * parent to fetch its own data first.
719 		 *
720 		 * <p>
721 		 * Basic content object implementation which all other content objects
722 		 * will inherit from.
723 		 * </p>
724 		 * 
725 		 * <p>
726 		 * If a `success' callback is provided,
727 		 * it will cause this object's data to be fetched and passed to the
728 		 * callback. This object's data will be fetched from the cache if is
729 		 * available, otherwise it will be fetched from the server. If this
730 		 * content object API contains parent chainbacks, it will get its parent
731 		 * to fetch its own data first.
732 		 * </p>
733 		 * 
734 		 * <p>
735 		 * You might also provide an object for initialization, to directly
736 		 * instantiate the object's data without loading it from the server. To
737 		 * do so just pass in a data object as received from the server instead
738 		 * of an id--just make sure this object has an `id' property.
739 		 * </p>
740 		 * 
741 		 * <p>
742 		 * If an `error' handler is provided, as the third parameter, it will
743 		 * catch any errors that have occured since the invocation of this call.
744 		 * It allows the global error handler to be intercepted before stopping
745 		 * the error or allowing it to propagate on to the global handler.
746 		 * </p>
747 		 * 
748 		 * @class
749 		 * @name ContentObjectAPI
750 		 * @param {number|string|object}
751 		 *            id
752 		 * @param {function(ContentObjectAPI))=}
753 		 *            success Optional success callback that will receive this
754 		 *            object as its only argument.
755 		 * @param {function(GCNError):boolean=}
756 		 *            error Optional custom error handler.
757 		 * @param {object}
758 		 *            settings Basic settings for this object - depends on the
759 		 *            ContentObjetAPI Object used.
760 		 * @throws INVALID_DATA
761 		 *             If no id is found when providing an object for
762 		 *             initialization.
763 		 */
764 		_init: function (data, success, error, settings) {
765 			this._settings = settings;
766 			var id;
767 
768 			if (jQuery.type(data) === 'object') {
769 				if (data.multichannelling) {
770 					this.multichannelling = data;
771 					// Remove the inherited object from the chain.
772 					if (this._chain) {
773 						this._chain = this._chain._chain;
774 					}
775 					id = this.multichannelling.derivedFrom.id();
776 				} else {
777 					if (!data.id) {
778 						var err = GCN.createError(
779 							'INVALID_DATA',
780 							'Data not sufficient for initalization: id is missing',
781 							data
782 						);
783 						GCN.handleError(err, error);
784 						return;
785 					}
786 					this._data = data;
787 					this._fetched = true;
788 					if (success) {
789 						this._invoke(success, [this]);
790 					}
791 					return;
792 				}
793 			} else {
794 				id = data;
795 			}
796 
797 			// Ensure that each object has its very own `_data' and `_shadow'
798 			// objects.
799 			if (!this._fetched) {
800 				this._data = {};
801 				this._shadow = {};
802 				this._data.id = id;
803 			}
804 			if (success) {
805 				this._read(success, error);
806 			}
807 		},
808 
809 		/**
810 		 * <p>Replaces tag blocks with appropriate "<node *>" notation in a given
811 		 * string. Given an element whose innerHTML is:<p>
812 		 * <pre>
813 		 *		<span id="GENTICS_BLOCK_123">My Tag</span>
814 		 * </pre>
815 		 * <p>encode() will return:</p>
816 		 * <pre>
817 		 *		<node 123>
818 		 * </pre>
819 		 *
820 		 * @name encode
821 		 * @function
822 		 * @memberOf ContentObjectAPI
823 		 * @param {!jQuery} $element
824 		 *       An element whose contents are to be encoded.
825 		 * @param {?function(!Element): string} serializeFn
826 		 *       A function that returns the serialized contents of the
827 		 *       given element as a HTML string, excluding the start and end
828 		 *       tag of the element. If not provided, jQuery.html() will
829 		 *       be used.
830 		 * @return {string} The encoded HTML string.
831 		 */
832 		'!encode': function ($element, serializeFn) {
833 			var $clone = $element.clone();
834 			var id;
835 			var $block;
836 			for (id in this._blocks) {
837 				if (this._blocks.hasOwnProperty(id)) {
838 					$block = $clone.find('#' + this._blocks[id].element);
839 					if ($block.length) {
840 						// Empty all content blocks of their innerHTML.
841 						$block.html('').attr('id', BLOCK_ENCODING_PREFIX +
842 							this._blocks[id].tagname);
843 					}
844 				}
845 			}
846 			serializeFn = serializeFn || function ($element) {
847 				return jQuery($element).html();
848 			};
849 			var html = serializeFn($clone[0]);
850 			return html.replace(CONTENT_BLOCK, function (substr, match) {
851 				return '<node ' + match + '>';
852 			});
853 		},
854 
855 		/**
856 		 * For a given string, replace all occurances of "<node>" with
857 		 * appropriate HTML markup, allowing notated tags to be rendered within
858 		 * the surrounding HTML content.
859 		 *
860 		 * The success() handler will receives a string containing the contents
861 		 * of the `str' string with references to "<node>" having been inflated
862 		 * into their appropriate tag rendering.
863 		 *
864 		 * @name decode
865 		 * @function
866 		 * @memberOf ContentObjectAPI
867 		 * @param {string} str The content string, in which  "<node *>" tags
868 		 *                     will be inflated with their HTML rendering.
869 		 * @param {function(ContentObjectAPI))} success Success callback that
870 		 *                                              will receive the
871 		 *                                              decoded string.
872 		 * @param {function(GCNError):boolean=} error Optional custom error
873 		 *                                            handler.
874 		 */
875 		'!decode': function (str, success, error) {
876 			if (!success) {
877 				return;
878 			}
879 
880 			var prefix = 'gcn-tag-placeholder-';
881 			var toRender = [];
882 			var html = replaceNodeTags(str, function (name, offset, str) {
883 				toRender.push('<node ', name, '>');
884 				return '<div id="' + prefix + name + '"></div>';
885 			});
886 
887 			if (!toRender.length) {
888 				success(html);
889 				return;
890 			}
891 
892 			// Instead of rendering each tag individually, we render them
893 			// together in one string, and map the results back into our
894 			// original html string.  This allows us to perform one request to
895 			// the server for any number of node tags found.
896 
897 			var parsed = jQuery('<div>' + html + '</div>');
898 			var template = toRender.join('');
899 			var that = this;
900 
901 			this._renderTemplate(template, 'edit', function (data) {
902 				var content = data.content;
903 				var tag;
904 				var tags = data.tags;
905 				var j = tags.length;
906 				var rendered = jQuery('<div>' + content + '</div>');
907 
908 				var replaceTag = (function (numTags) {
909 					return function (tag) {
910 						parsed.find('#' + prefix + tag.prop('name'))
911 							.replaceWith(
912 								rendered.find('#' + tag.prop('id'))
913 							);
914 
915 						if (0 === --numTags) {
916 							success(parsed.html());
917 						}
918 					};
919 				}(j));
920 
921 				while (j) {
922 					that.tag(tags[--j], replaceTag);
923 				}
924 			}, error);
925 		},
926 
927 		/**
928 		 * Clears this object from its constructor's cache so that the next
929 		 * attempt to access this object will result in a brand new instance
930 		 * being initialized and placed in the cache.
931 		 *
932 		 * @name clear
933 		 * @function
934 		 * @memberOf ContentObjectAPI
935 		 */
936 		'!clear': function () {
937 			// Do not clear the id from the _data.
938 			var id = this._data.id;
939 			this._data = {};
940 			this._data.id = id;
941 			this._shadow = {};
942 			this._fetched = false;
943 			this._clearCache();
944 		},
945 
946 		/**
947 		 * Retrieves this objects parent folder.
948 		 * 
949 		 * @name folder
950 		 * @function
951 		 * @memberOf ContentObjectAPI
952 		 * @param {function(FolderAPI)=}
953 		 *            success Callback that will receive the requested object.
954 		 * @param {function(GCNError):boolean=}
955 		 *            error Custom error handler.
956 		 * @return {FolderAPI} API object for the retrieved GCN folder.
957 		 */
958 		'!folder': function (success, error) {
959 			return this._continue(GCN.FolderAPI, this._data.folderId, success,
960 				error);
961 		},
962 
963 		/**
964 		 * Saves changes made to this content object to the backend.
965 		 * 
966 		 * @param {object=}
967 		 *            settings Optional settings to pass on to the ajax
968 		 *            function.
969 		 * @param {function(ContentObjectAPI)=}
970 		 *            success Optional callback that receives this object as its
971 		 *            only argument.
972 		 * @param {function(GCNError):boolean=}
973 		 *            error Optional customer error handler.
974 		 */
975 		save: function () {
976 			var settings;
977 			var success;
978 			var error;
979 			var args = Array.prototype.slice.call(arguments);
980 			var len = args.length;
981 			var i;
982 
983 			for (i = 0; i < len; ++i) {
984 				switch (jQuery.type(args[i])) {
985 				case 'object':
986 					if (!settings) {
987 						settings = args[i];
988 					}
989 					break;
990 				case 'function':
991 					if (!success) {
992 						success = args[i];
993 					} else {
994 						error = args[i];
995 					}
996 					break;
997 				case 'undefined':
998 					break;
999 				default:
1000 					var err = GCN.createError('UNKNOWN_ARGUMENT',
1001 						'Don\'t know what to do with arguments[' + i + '] ' +
1002 						'value: "' + args[i] + '"', args);
1003 					GCN.handleError(err, error);
1004 					return;
1005 				}
1006 			}
1007 
1008 			this._save(settings, success, error);
1009 		},
1010 
1011 		/**
1012 		 * Persists this object's local data onto the server.  If the object
1013 		 * has not yet been fetched we need to get it first so we can update
1014 		 * its internals properly...
1015 		 *
1016 		 * @private
1017 		 * @param {object} settings Object which will extend the basic
1018 		 *                          settings of the ajax call
1019 		 * @param {function(ContentObjectAPI)=} success Optional callback that
1020 		 *                                              receives this object as
1021 		 *                                              its only argument.
1022 		 * @param {function(GCNError):boolean=} error Optional customer error
1023 		 *                                            handler.
1024 		 */
1025 		'!_save': function (settings, success, error) {
1026 			var that = this;
1027 			this._fulfill(function () {
1028 				that._persist(settings, success, error);
1029 			}, error);
1030 		},
1031 
1032 		/**
1033 		 * Returns the bare data structure of this content object.
1034 		 * To be used for creating the save POST body data.
1035 		 *
1036 		 * @param {object<string, *>} Plain old object representation of this
1037 		 *                            content object.
1038 		 */
1039 		'!json': function () {
1040 			var json = {};
1041 
1042 			if (this._deletedTags.length) {
1043 				json['delete'] = this._deletedTags;
1044 			}
1045 
1046 			if (this._deletedBlocks.length) {
1047 				json['delete'] = json['delete']
1048 				               ? json['delete'].concat(this._deletedBlocks)
1049 				               : this._deletedBlocks;
1050 			}
1051 
1052 			json[this._type] = this._shadow;
1053 			json[this._type].id = this._data.id;
1054 			return json;
1055 		},
1056 
1057 		/**
1058 		 * Sends the current state of this content object to be stored on the
1059 		 * server.
1060 		 *
1061 		 * @private
1062 		 * @param {function(ContentObjectAPI)=} success Optional callback that
1063 		 *                                              receives this object as
1064 		 *                                              its only argument.
1065 		 * @param {function(GCNError):boolean=} error Optional customer error
1066 		 *                                            handler.
1067 		 * @throws HTTP_ERROR
1068 		 */
1069 		'!_persist': function (settings, success, error) {
1070 			var that = this;
1071 
1072 			if (!this._fetched) {
1073 				this._read(function () {
1074 					that._persist(settings, success, error);
1075 				}, error);
1076 				return;
1077 			}
1078 
1079 			var json = this.json();
1080 			jQuery.extend(json, settings);
1081 			var tags = json[this._type].tags;
1082 			var tagname;
1083 			for (tagname in tags) {
1084 				if (tags.hasOwnProperty(tagname)) {
1085 					tags[tagname].active = true;
1086 				}
1087 			}
1088 
1089 			this._authAjax({
1090 				url   : GCN.settings.BACKEND_PATH + '/rest/'
1091 				        + this._type + '/save/' + this.id()
1092 				        + GCN._getChannelParameter(this),
1093 				type  : 'POST',
1094 				error : error,
1095 				json  : json,
1096 				success : function (response) {
1097 					// We must not overwrite the `_data.tags' object with this
1098 					// one.
1099 					delete that._shadow.tags;
1100 
1101 					// Everything else in `_shadow' should be written over to
1102 					// `_data' before resetting the `_shadow' object.
1103 					jQuery.extend(that._data, that._shadow);
1104 					that._shadow = {};
1105 					that._deletedTags = [];
1106 					that._deletedBlocks = [];
1107 
1108 					if (success) {
1109 						that._invoke(success, [that]);
1110 					}
1111 				}
1112 			});
1113 		},
1114 
1115 		/**
1116 		 * Deletes this content object from its containing parent.
1117 		 * 
1118 		 * @param {function(ContentObjectAPI)=}
1119 		 *            success Optional callback that receives this object as its
1120 		 *            only argument.
1121 		 * @param {function(GCNError):boolean=}
1122 		 *            error Optional customer error handler.
1123 		 */
1124 		remove: function (success, error) {
1125 			this._remove(success, error);
1126 		},
1127 
1128 		/**
1129 		 * Get a channel-local copy of this content object.
1130 		 *
1131 		 * @public
1132 		 * @function
1133 		 * @name localize
1134 		 * @memberOf ContentObjectAPI
1135 		 * @param {funtion(ContentObjectAPI)=} success Optional callback to
1136 		 *                                             receive this content
1137 		 *                                             object as the only
1138 		 *                                             argument.
1139 		 * @param {function(GCNError):boolean=} error Optional custom error
1140 		 *                                            handler.
1141 		 */
1142 		'!localize': function (success, error) {
1143 			if (!this._channel && !GCN.channel()) {
1144 				var err = GCN.createError(
1145 					'NO_CHANNEL_ID_SET',
1146 					'No channel is set in which to get the localized object',
1147 					GCN
1148 				);
1149 				GCN.handleError(err, error);
1150 				return false;
1151 			}
1152 			var local = this._continue(
1153 				this._constructor,
1154 				{
1155 					derivedFrom: this,
1156 					multichannelling: true,
1157 					read: GCN.multichannelling.localize
1158 				},
1159 				success,
1160 				error
1161 			);
1162 			return local;
1163 		},
1164 
1165 		/**
1166 		 * Remove this channel-local object, and delete its local copy in the
1167 		 * backend.
1168 		 *
1169 		 * @public
1170 		 * @function
1171 		 * @name unlocalize
1172 		 * @memberOf ContentObjectAPI
1173 		 * @param {funtion(ContentObjectAPI)=} success Optional callback to
1174 		 *                                             receive this content
1175 		 *                                             object as the only
1176 		 *                                             argument.
1177 		 * @param {function(GCNError):boolean=} error Optional custom error
1178 		 *                                            handler.
1179 		 */
1180 		'!unlocalize': function (success, error) {
1181 			if (!this._channel && !GCN.channel()) {
1182 				var err = GCN.createError(
1183 					'NO_CHANNEL_ID_SET',
1184 					'No channel is set in which to get the unlocalized object',
1185 					GCN
1186 				);
1187 				GCN.handleError(err, error);
1188 				return false;
1189 			}
1190 			var placeholder = {
1191 				multichannelling: {
1192 					derivedFrom: this
1193 				}
1194 			};
1195 			var that = this;
1196 			GCN.multichannelling.unlocalize(placeholder, function () {
1197 				// Clean cache & reset object to make sure it can't be used
1198 				// properly any more.
1199 				that._clearCache();
1200 				that._data = {};
1201 				that._shadow = {};
1202 				if (success) {
1203 					success();
1204 				}
1205 			}, error);
1206 		},
1207 
1208 		/**
1209 		 * Performs a REST API request to delete this object from the server.
1210 		 *
1211 		 * @private
1212 		 * @param {function()=} success Optional callback that
1213 		 *                                              will be invoked once
1214 		 *                                              this object has been
1215 		 *                                              removed.
1216 		 * @param {function(GCNError):boolean=} error Optional customer error
1217 		 *                                            handler.
1218 		 */
1219 		'!_remove': function (success, error) {
1220 			var that = this;
1221 			this._authAjax({
1222 				url     : GCN.settings.BACKEND_PATH + '/rest/'
1223 				          + this._type + '/delete/' + this.id()
1224 				          + GCN._getChannelParameter(that),
1225 				type    : 'POST',
1226 				error   : error,
1227 				success : function (response) {
1228 					// Clean cache & reset object to make sure it can't be used
1229 					// properly any more.
1230 					that._clearCache();
1231 					that._data = {};
1232 					that._shadow = {};
1233 
1234 					// Don't forward the object to the success handler since
1235 					// it's been deleted.
1236 					if (success) {
1237 						that._invoke(success);
1238 					}
1239 				}
1240 			});
1241 		},
1242 
1243 		/**
1244 		 * Removes any additionaly data stored on this objec which pertains to
1245 		 * a tag matching the given tagname.  This function will be called when
1246 		 * a tag is being removed in order to bring the content object to a
1247 		 * consistant state.
1248 		 * Should be overriden by subclasses.
1249 		 *
1250 		 * @param {string} tagid The Id of the tag whose associated data we
1251 		 *                       want we want to remove.
1252 		 */
1253 		'!_removeAssociatedTagData': function (tagname) {}
1254 
1255 	});
1256 
1257 	GCN.ContentObjectAPI.update = update;
1258 
1259 	/**
1260 	 * Generates a factory method for chainback classes.  The method signature
1261 	 * used with this factory function will match that of the target class'
1262 	 * constructor.  Therefore this function is expected to be invoked with the
1263 	 * follow combination of arguments ...
1264 	 *
1265 	 * Examples for GCN.pages api:
1266 	 *
1267 	 * To get an array containing 1 page:
1268 	 * pages(1)
1269 	 * pages(1, function () {})
1270 	 *
1271 	 * To get an array containing 2 pages:
1272 	 * pages([1, 2])
1273 	 * pages([1, 2], function () {})
1274 	 *
1275 	 * To get an array containing any and all pages:
1276 	 * pages()
1277 	 * pages(function () {})
1278 	 *
1279 	 * To get an array containing no pages:
1280 	 * pages([])
1281 	 * pages([], function () {});
1282 	 *
1283 	 * @param {Chainback} ctor The Chainback constructor we want to expose.
1284 	 * @throws UNKNOWN_ARGUMENT
1285 	 */
1286 	GCN.exposeAPI = function (ctor) {
1287 		return function () {
1288 			// Convert arguments into an array
1289 			// https://developer.mozilla.org/en/JavaScript/Reference/...
1290 			// ...Functions_and_function_scope/arguments
1291 			var args = Array.prototype.slice.call(arguments);
1292 			var id;
1293 			var ids;
1294 			var success;
1295 			var error;
1296 			var settings;
1297 
1298 			// iterate over arguments to find id || ids, succes, error and
1299 			// settings
1300 			jQuery.each(args, function (i, arg) {
1301 				switch (jQuery.type(arg)) {
1302 				// set id
1303 				case 'string':
1304 				case 'number':
1305 					if (!id && !ids) {
1306 						id = arg;
1307 					} else {
1308 						GCN.error('UNKNOWN_ARGUMENT',
1309 							'id is already set. Don\'t know what to do with ' +
1310 							'arguments[' + i + '] value: "' + arg + '"');
1311 					}
1312 					break;
1313 				// set ids
1314 				case 'array':
1315 					if (!id && !ids) {
1316 						ids = args[0];
1317 					} else {
1318 						GCN.error('UNKNOWN_ARGUMENT',
1319 							'ids is already set. Don\'t know what to do with' +
1320 							' arguments[' + i + '] value: "' + arg + '"');
1321 					}
1322 					break;
1323 				// success and error handlers
1324 				case 'function':
1325 					if (!success) {
1326 						success = arg;
1327 					} else if (success && !error) {
1328 						error = arg;
1329 					} else {
1330 						GCN.error('UNKNOWN_ARGUMENT',
1331 							'success and error handler already set. Don\'t ' +
1332 							'know what to do with arguments[' + i + ']');
1333 					}
1334 					break;
1335 				// settings
1336 				case 'object':
1337 					if (!id && !ids) {
1338 						id = arg;
1339 					} else if (!settings) {
1340 						settings = arg;
1341 					} else {
1342 						GCN.error('UNKNOWN_ARGUMENT',
1343 							'settings are already present. Don\'t know what ' +
1344 							'to do with arguments[' + i + '] value:' + ' "' +
1345 							arg + '"');
1346 					}
1347 					break;
1348 				default:
1349 					GCN.error('UNKNOWN_ARGUMENT',
1350 						'Don\'t know what to do with arguments[' + i +
1351 						'] value: "' + arg + '"');
1352 				}
1353 			});
1354 
1355 			// Prepare a new set of arguments to pass on during initialzation
1356 			// of callee object.
1357 			args = [];
1358 
1359 			// settings should always be an object, even if it's just empty
1360 			if (!settings) {
1361 				settings = {};
1362 			}
1363 
1364 			args[0] = (typeof id !== 'undefined') ? id : ids;
1365 			args[1] = success || settings.success || null;
1366 			args[2] = error || settings.error || null;
1367 			args[3] = settings;
1368 
1369 			// We either add 0 (no channel) or the channelid to the hash
1370 			var channel = GCN.settings.channel;
1371 
1372 			// Check if the value is false, and set it to 0 in this case
1373 			if (!channel) {
1374 				channel = 0;
1375 			}
1376 
1377 			var hash = (id || ids)
1378 			         ? ctor._makeHash(channel + '/' + (ids ? ids.sort().join(',') : id))
1379 			         : null;
1380 
1381 			return GCN.getChainback(ctor, hash, null, args);
1382 		};
1383 
1384 	};
1385 
1386 }(GCN));
1387