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