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