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