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