1 (function (GCN) {
  2 
  3 	'use strict';
  4 
  5 	/**
  6 	 * @private
  7 	 * @const
  8 	 * @type {string}
  9 	 */
 10 	var GCN_REPOSITORY_ID = 'com.gentics.aloha.GCN.Page';
 11 
 12 	/**
 13 	 * @private
 14 	 * @const
 15 	 * @type {object.<string, boolean>} Default page settings.
 16 	 */
 17 	var DEFAULT_SETTINGS = {
 18 		// Load folder information
 19 		folder: true,
 20 
 21 		// Lock page when loading it
 22 		update: true,
 23 
 24 		// Have language variants be included in the page response.
 25 		langvars: true,
 26 
 27 		// Have page variants be included in the page response.
 28 		pagevars: true
 29 	};
 30 
 31 	/**
 32 	 * Searches for the an Aloha editable object of the given id.
 33 	 *
 34 	 * @TODO: Once Aloha.getEditableById() is patched to not cause an
 35 	 *        JavaScript exception if the element for the given ID is not found
 36 	 *        then we can deprecate this function and use Aloha's instead.
 37 	 *
 38 	 * @static
 39 	 * @param {string} id Id of Aloha.Editable object to find.
 40 	 * @return {Aloha.Editable=} The editable object, if wound; otherwise null.
 41 	 */
 42 	function getAlohaEditableById(id) {
 43 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
 44 		if (!Aloha) {
 45 			return null;
 46 		}
 47 
 48 		// If the element is a textarea then route to the editable div.
 49 		var element = jQuery('#' + id);
 50 		if (element.length &&
 51 				element[0].nodeName.toLowerCase() === 'textarea') {
 52 			id += '-aloha';
 53 		}
 54 
 55 		var editables = Aloha.editables;
 56 		var j = editables.length;
 57 		while (j) {
 58 			if (editables[--j].getId() === id) {
 59 				return editables[j];
 60 			}
 61 		}
 62 
 63 		return null;
 64 	}
 65 
 66 	/**
 67 	 * Checks whether the given tag is a magic link block.
 68 	 *
 69 	 * @private
 70 	 * @static
 71 	 * @param {GCN.Tag} tag Must be a tag that has already been fetched.
 72 	 * @param {object} constructs Set of constructs.
 73 	 * @return {boolean} True if the given tag has the magic link constructId.
 74 	 */
 75 	function isMagicLinkTag(tag, constructs) {
 76 		return !!(constructs[GCN.settings.MAGIC_LINK] &&
 77 		          (constructs[GCN.settings.MAGIC_LINK].constructId ===
 78 			       tag.prop('constructId')));
 79 	}
 80 
 81 	/**
 82 	 * Checks whether or not the given block has a corresponding element in the
 83 	 * document DOM.
 84 	 *
 85 	 * @private
 86 	 * @static
 87 	 * @param {object}
 88 	 * @return {boolean} True if an inline element for this block exists.
 89 	 */
 90 	function hasInlineElement(block) {
 91 		return 0 < jQuery('#' + block.element).length;
 92 	}
 93 
 94 	/**
 95 	 * For a given list of editables and a list of blocks, determine in which
 96 	 * editable each block was rendered.  The result is map with a list of
 97 	 * blocks mapped against the id of the rendered editable element.
 98 	 *
 99 	 * @param {string} content The rendered content in which the list of
100 	 *                         editables, and blocks are contained.
101 	 * @param {Array.<object>} editables A list of editables contained in the
102 	 *                                   rendered content.
103 	 * @param {Array.<object>} blocks A list of blocks containd in content
104 	 *                                string string.
105 	 * @return {object<string, <Array>} A object whose keys are editable ids,
106 	 *                                  and whose values are Array of block
107 	 *                                  contained in the corresponding
108 	 *                                  editable.
109 	 */
110 	function getMapOfBlocksToEditables(content, editables, blocks) {
111 		var i;
112 		var $content = jQuery('<div>' + content + '</div>');
113 		var map = {};
114 		var editableElementId;
115 
116 		var selection = [];
117 		for (i = 0; i < editables.length; i++) {
118 			selection.push('#' + editables[i].element);
119 		}
120 
121 		var markerClass = 'aloha-editable-tmp-marker__';
122 		var $editables = $content.find(selection.join(','));
123 		$editables.addClass(markerClass);
124 
125 		var $block;
126 		var $parent;
127 		for (i = 0; i < blocks.length; i++) {
128 			$block = $content.find('#' + blocks[i].element);
129 			if ($block.length) {
130 				$parent = $block.parents('.' + markerClass);
131 				if ($parent.length) {
132 					editableElementId = $parent.attr('id');
133 					if (editableElementId) {
134 						if (!map[editableElementId]) {
135 							map[editableElementId] = [];
136 						}
137 						map[editableElementId].push(blocks[i]);
138 					}
139 				}
140 			}
141 		}
142 
143 		return map;
144 	}
145 
146 	/**
147 	 * @private
148 	 * @const
149 	 * @type {number}
150 	 */
151 	//var TYPE_ID = 10007;
152 
153 	/**
154 	 * @private
155 	 * @const
156 	 * @type {Enum}
157 	 */
158 	var STATUS = {
159 
160 		// page was not found in the database
161 		NOTFOUND: -1,
162 
163 		// page is locally modified and not yet (re-)published
164 		MODIFIED: 0,
165 
166 		// page is marked to be published (dirty)
167 		TOPUBLISH: 1,
168 
169 		// page is published and online
170 		PUBLISHED: 2,
171 
172 		// Page is offline
173 		OFFLINE: 3,
174 
175 		// Page is in the queue (publishing of the page needs to be affirmed)
176 		QUEUE: 4,
177 
178 		// page is in timemanagement and outside of the defined timespan
179 		// (currently offline)
180 		TIMEMANAGEMENT: 5,
181 
182 		// page is to be published at a given time (not yet)
183 		TOPUBLISH_AT: 6
184 	};
185 
186 	/**
187 	 * Given a link, will read the data-gentics-aloha-object-id attribute and
188 	 * from it, will determine the backend object that was selected by the
189 	 * repository browser.
190 	 *
191 	 * @param {jQuery.<HTMLElement>} link A link in an editable.
192 	 * @return {Object}
193 	 *        A map containing the gtxalohapagelink part keyword and
194 	 *        value. The keyword may be either be 'url' or 'fileurl'
195 	 *        depending on the type of object linked to. The value may
196 	 *        be a string url ('http://...') for external links or an
197 	 *        integer for internal links.
198 	 */
199 	function getRepositoryLinkAsPart(link) {
200 		var data = link.attr('data-gentics-aloha-object-id');
201 		var value;
202 		var keyword;
203 		if (data) {
204 			var parts = data.split('.');
205 			if (parts.length === 2) {
206 				if (parts[0] === '10008' || parts[0] === '10011') {
207 					keyword = 'fileurl';
208 				} else { // assumes 10007 for page links
209 					keyword = 'url';
210 				}
211 				value = parts[1];
212 			} else {
213 				keyword = 'url';
214 				value = data;
215 			}
216 			if (null !== value && typeof value !== 'undefined') {
217 				value = parseInt(value, 10);
218 			}
219 		} else {
220 			keyword = 'url';
221 			value = link.attr('data-gentics-gcn-url');
222 		}
223 		return {
224 			keyword: keyword,
225 			value: value
226 		};
227 	}
228 
229 	/**
230 	* @class
231 	* @name PageAPI
232 	* @extends ContentObjectAPI
233 	* @extends TagContainerAPI
234 	*
235 	* Page object information can be extened using the default REST-API.
236 	* options:
237 	*
238 	* - update: true
239 	* Whether the page should be locked in the backend when loading it.
240 	* default: true
241 	*
242 	* - template: true
243 	* Whether the template information should be embedded in the page object.
244 	* default: true
245 	*
246 	* - folder: true,
247 	* Whether the folder information should be embedded in the page object.
248 	* default: true
249 	* WARNING: do not turn this option off - it will leave the API in a broken
250 	*          state.
251 	*
252 	* - langvars: false,
253 	* When the language variants shall be embedded in the page response.
254 	* default: false
255 
256 	* - workflow: false,
257 	* When the workflow information shall be embedded in the page response.
258 	* default: false
259 
260 	* - pagevars: false,
261 	* When the page variants shall be embedded in the page response. Page
262 	* variants will contain folder information.
263 	* default: false
264 	*
265 	* - translationstatus: false
266 	* Will return information on the page's translation status.
267 	* default: false
268 	*/
269 	var PageAPI = GCN.defineChainback({
270 		/** @lends PageAPI */
271 
272 		__chainbacktype__: 'PageAPI',
273 		_extends: [ GCN.ContentObjectAPI, GCN.TagContainerAPI ],
274 		_type: 'page',
275 
276 		/**
277 		 * @private
278 		 * @type {Array.<object>} A hash set of block tags belonging to this
279 		 *                        content object.  This set is added to when
280 		 *                        this page's tags are rendered.
281 		 */
282 		_blocks: {},
283 
284 		/**
285 		 * @private
286 		 * @type {Array.<object>} A hash set of editable tags belonging to this
287 		 *                        content object.  This set is added to when
288 		 *                        this page's tags are rendered.
289 		 */
290 		_editables: {},
291 
292 		/**
293 		 * @type {Array.string} Writable properties for the page object.
294 		 */
295 		WRITEABLE_PROPS: ['cdate',
296 		                  'description',
297 		                  'fileName',
298 		                  'folderId', // @TODO Check if moving a page is
299 		                              //       implemented correctly.
300 		                  'name',
301 		                  'priority',
302 		                  'templateId'],
303 
304 		/**
305 		 * Gets all blocks in this page.  Will return an array of all block
306 		 * objects found in the page AFTER they have been rendered using an
307 		 * `edit()' call for a contenttag.
308 		 * NOTE: If you have just loaded the page and not used the `edit()'
309 		 *       method for any tag the array will be empty.  Only those blocks
310 		 *       that have been initialized using `edit()' will be available.
311 		 *
312 		 * @return {Array.<object>} Array of block objects.
313 		 */
314 		'!blocks': function () {
315 			return this._blocks;
316 		},
317 
318 		/**
319 		 * Looks for a block with the given id in the `_blocks' array.
320 		 *
321 		 * @private
322 		 * @param {string} id The block's id.
323 		 * @return {?object} The block data object.
324 		 */
325 		'!_getBlockById': function (id) {
326 			return this._blocks[id];
327 		},
328 
329 		/**
330 		 * Maps the received editables into this content object's `_editable'
331 		 * hash.
332 		 *
333 		 * @private
334 		 * @param {Array.<object>} editables An set of object representing
335 		 *                                   editable tags that have been
336 		 *                                   rendered.
337 		 */
338 		'!_storeRenderedEditables': function (editables) {
339 			if (!this.hasOwnProperty('_editables')) {
340 				this._editables = {};
341 			}
342 
343 			var j = editables && editables.length;
344 
345 			while (j) {
346 				this._editables[editables[--j].element] = editables[j];
347 			}
348 		},
349 
350 		/**
351 		 * Maps received blocks of this content object into the `_blocks' hash.
352 		 *
353 		 * @private
354 		 * @param {Array.<object>} blocks An set of object representing
355 		 *                                block tags that have been rendered.
356 		 */
357 		'!_storeRenderedBlocks': function (blocks) {
358 			if (!this.hasOwnProperty('_blocks')) {
359 				this._blocks = {};
360 			}
361 
362 			var j = blocks && blocks.length;
363 
364 			while (j) {
365 				this._blocks[blocks[--j].element] = blocks[j];
366 			}
367 		},
368 
369 		/**
370 		 * @override
371 		 */
372 		'!_processRenderedTags': function (data) {
373 			var tags = this._getEditablesAndBlocks(data);
374 			var map = getMapOfBlocksToEditables(data.content, tags.editables,
375 				tags.blocks);
376 
377 			this._storeRenderedEditables(tags.editables);
378 			this._storeRenderedBlocks(tags.blocks);
379 
380 			var editableId;
381 			for (editableId in map) {
382 				if (map.hasOwnProperty(editableId) &&
383 						this._editables[editableId]) {
384 					if (!this._editables[editableId]._gcnContainedBlocks) {
385 						this._editables[editableId]._gcnContainedBlocks =
386 							map[editableId];
387 					} else {
388 						this._editables[editableId]._gcnContainedBlocks =
389 							this._editables[editableId]._gcnContainedBlocks
390 							    .concat(map[editableId]);
391 					}
392 				}
393 			}
394 
395 			return tags;
396 		},
397 
398 		/**
399 		 * Processes rendered tags, and update the `_blocks' and `_editables'
400 		 * array accordingly.  This function is called during pre-saving to
401 		 * update this page's editable tags.
402 		 *
403 		 * @private
404 		 */
405 		'!_prepareTagsForSaving': function (success, error) {
406 			if (!this.hasOwnProperty('_deletedBlocks')) {
407 				this._deletedBlocks = [];
408 			}
409 
410 			var editableId;
411 			var containedBlocks = [];
412 			for (editableId in this._editables) {
413 				if (this._editables.hasOwnProperty(editableId)) {
414 					if (this._editables[editableId]._gcnContainedBlocks) {
415 						containedBlocks = containedBlocks.concat(
416 							this._editables[editableId]._gcnContainedBlocks
417 						);
418 					}
419 				}
420 			}
421 
422 			var blocks = [];
423 			var j = containedBlocks.length;
424 			while (j) {
425 				if (this._blocks[containedBlocks[--j]]) {
426 					blocks.push(containedBlocks[j]);
427 				}
428 			}
429 
430 			var that = this;
431 			this._addNewLinkBlocks(function () {
432 				GCN.Admin.constructs(function (constructs) {
433 					that._removeOldLinkBlocks(blocks, constructs, function () {
434 						that._removeUnusedLinkBlocks(blocks, constructs, function () {
435 							that._updateEditableBlocks();
436 							success();
437 						}, error);
438 					}, error);
439 				}, error);
440 			}, error);
441 		},
442 
443 		/**
444 		 * Removes any link blocks that existed in rendered tags, but have
445 		 * since been removed by the user while editing.
446 		 *
447 		 * @private
448 		 * @param {Array.<object>} blocks An array of blocks belonging to this
449 		 *                                page.
450 		 * @param {object} constrcts A set of constructs.
451 		 * @param {function} success
452 		 * @param {function(GCNError):boolean=} error Optional custom error
453 		 *                                            handler.
454 		 */
455 		'!_removeUnusedLinkBlocks': function (blocks, constructs, success,
456 		                                      error) {
457 			if (0 === blocks.length) {
458 				if (success) {
459 					success();
460 				}
461 				return;
462 			}
463 
464 			var j = blocks.length;
465 			var numToProcess = j;
466 
467 			var onProcess = function () {
468 				if (0 === --numToProcess) {
469 					if (success) {
470 						success();
471 					}
472 				}
473 			};
474 
475 			var onError = function (GCNError) {
476 				if (error) {
477 					error(GCNError);
478 				}
479 				return;
480 			};
481 
482 			var that = this;
483 			var createBlockTagProcessor = function (block) {
484 				return function (tag) {
485 					if (isMagicLinkTag(tag, constructs) &&
486 							!hasInlineElement(block)) {
487 						that._deletedBlocks.push(block.tagname);
488 					}
489 
490 					onProcess();
491 				};
492 			};
493 
494 			while (j) {
495 				this.tag(blocks[--j].tagname,
496 					createBlockTagProcessor(blocks[j]), onError);
497 			}
498 		},
499 
500 		/**
501 		 * Adds any newly created link blocks into this page object.  This is
502 		 * done by looking for all link blocks that do not have corresponding
503 		 * tag in this object's `_blocks' list.  For each anchor tag we find,
504 		 * create a tag for it and, add it in the list of tags.
505 		 *
506 		 * @private
507 		 * @param {function} success Function to invoke if this function
508 		 *                           successeds.
509 		 * @param {function(GCNError):boolean=} error Optional custom error
510 		 *                                            handler.
511 		 */
512 		'!_addNewLinkBlocks': function (success, error) {
513 			// Limit the search for links to be done only withing rendered
514 			// editables.
515 			var id;
516 			var $editables = jQuery();
517 			for (id in this._editables) {
518 				if (this._editables.hasOwnProperty(id)) {
519 					$editables = $editables.add('#' + id);
520 				}
521 			}
522 
523 			var selector = [
524 				'a[data-gentics-aloha-repository="com.gentics.aloha.GCN.Page"]',
525 				'a[data-GENTICS-aloha-repository="com.gentics.aloha.GCN.Page"]',
526 				'a[data-gentics-gcn-url]'
527 			].join(',');
528 
529 			var links = $editables.find(selector);
530 
531 			if (0 === links.length) {
532 				if (success) {
533 					success();
534 				}
535 
536 				return;
537 			}
538 
539 			var link;
540 			var j = links.length;
541 			var numToProcess = j;
542 
543 			var onProcessed = function () {
544 				if (0 === --numToProcess) {
545 					success();
546 				}
547 			};
548 
549 			var onError = function (GCNError) {
550 				if (error) {
551 					error(GCNError);
552 				}
553 				return;
554 			};
555 
556 			var createOnEditHandler = function (link) {
557 				return function (html, tag) {
558 					link.attr('id', jQuery(html).attr('id'));
559 					var part = getRepositoryLinkAsPart(link);
560 					tag.part(part.keyword, part.value);
561 					onProcessed();
562 				};
563 			};
564 
565 			var tag;
566 
567 			while (j) {
568 				link = links.eq(--j);
569 				if (link.attr('data-gcnignore') === true) {
570 					onProcessed();
571 				} else if (this._getBlockById(link.attr('id'))) {
572 					tag = this.tag(this._getBlockById(link.attr('id')).tagname);
573 					tag.part('text', link.html());
574 					var part = getRepositoryLinkAsPart(link);
575 					tag.part(part.keyword, part.value);
576 					onProcessed();
577 				} else {
578 					this.createTag(GCN.settings.MAGIC_LINK, link.html())
579 					    .edit(createOnEditHandler(link), onError);
580 				}
581 			}
582 		},
583 
584 		/**
585 		 * Any links that change from internal GCN links to external links will
586 		 * have their corresponding blocks added to the '_deletedBlocks' list
587 		 * since they these links no longer need to be tracked.  Any tags in
588 		 * this list will be removed during saving.
589 		 *
590 		 * @private
591 		 * @param {Array.<object>} blocks An array of blocks belonging to this
592 		 *                                page.
593 		 * @param {object} constrcts A set of constructs.
594 		 * @param {function} success
595 		 * @param {function(GCNError):boolean=} error Optional custom error
596 		 *                                            handler.
597 		 */
598 		'!_removeOldLinkBlocks': function (blocks, constructs, success, error) {
599 			if (0 === blocks.length) {
600 				if (success) {
601 					success();
602 				}
603 				return;
604 			}
605 
606 			var $links = jQuery('a');
607 			var j = blocks.length;
608 			var numToProcess = j;
609 
610 			var onProcess = function () {
611 				if (0 === --numToProcess) {
612 					if (success) {
613 						success();
614 					}
615 				}
616 			};
617 
618 			var onError = function (GCNError) {
619 				if (error) {
620 					error(GCNError);
621 				}
622 				return;
623 			};
624 
625 			var that = this;
626 			var createBlockTagProcessor = function (block) {
627 				return function (tag) {
628 					if (!isMagicLinkTag(tag, constructs)) {
629 						onProcess();
630 						return;
631 					}
632 
633 					var a = $links.filter('#' + block.element);
634 
635 					if (a.length) {
636 						var isExternal = (GCN_REPOSITORY_ID !==
637 							a.attr('data-gentics-aloha-repository')) &&
638 							!a.attr('data-gentics-gcn-url');
639 
640 						// An external tag was found.  Stop tracking it and
641 						// remove it from the list of blocks.
642 						if (isExternal) {
643 							a.removeAttr('id');
644 							that._deletedBlocks.push(block.tagname);
645 							delete that._blocks[block.element];
646 						}
647 
648 					// No anchor tag was found for this block.  Add it to the
649 					// "delete" list.
650 					} else {
651 						that._deletedBlocks.push(block.tagname);
652 						delete that._blocks[block.element];
653 					}
654 
655 					onProcess();
656 				};
657 			};
658 
659 			while (j) {
660 				this.tag(blocks[--j].tagname,
661 					createBlockTagProcessor(blocks[j]), onError);
662 			}
663 		},
664 
665 		/**
666 		 * Writes the contents of editables back into their corresponding tags.
667 		 * If a corresponding tag cannot be found for an editable, a new one
668 		 * will be created for it.
669 		 *
670 		 * A reference for each editable tag is then added to the `_shadow'
671 		 * object in order that the tag will be sent with the save request.
672 		 *
673 		 * @private
674 		 */
675 		'!_updateEditableBlocks': function () {
676 			var element;
677 			var elementId;
678 			var editable;
679 			var editables = this._editables;
680 			var tags = this._data.tags;
681 			var tagname;
682 			var html;
683 			var alohaEditable;
684 			var cleanElement;
685 			var customSerializer;
686 
687 			for (elementId in editables) {
688 				if (editables.hasOwnProperty(elementId)) {
689 					editable = editables[elementId];
690 					element = jQuery('#' + elementId);
691 
692 					// If this editable has no element that was placed in the
693 					// DOM, then do not attempt to update it.
694 					if (0 === element.length) {
695 						continue;
696 					}
697 
698 					tagname = editable.tagname;
699 
700 					if (!tags[tagname]) {
701 						tags[tagname] = {
702 							name       : tagname,
703 							active     : true,
704 							properties : {}
705 						};
706 					} else {
707 						// make sure, that all tags (which relate to editables)
708 						// are activated
709 						tags[tagname].active = true;
710 					}
711 
712 					// If the editable element has been `aloha()'fied, then we
713 					// need to use `getContents()' from is corresponding
714 					// Aloha.Editable object in order to get clean HTML.
715 
716 					alohaEditable = getAlohaEditableById(elementId);
717 
718 					if (alohaEditable) {
719 						// Avoid the unnecessary overhead of custom editable
720 						// serialization by calling html ourselves.
721 						cleanElement = jQuery('<div>')
722 							.append(alohaEditable.getContents(true));
723 						alohaEditable.setUnmodified();
724 						// Apply the custom editable serialization as the last step.
725 						customSerializer = window.Aloha.Editable.getContentSerializer();
726 						html = this.encode(cleanElement, customSerializer);
727 					} else {
728 						html = this.encode(element);
729 					}
730 
731 					tags[tagname].properties[editable.partname] = {
732 						stringValue: html,
733 						type: 'RICHTEXT'
734 					};
735 
736 					this._update('tags.' + tagname, tags[tagname]);
737 				}
738 			}
739 		},
740 
741 		/**
742 		 * @see ContentObjectAPI.!_loadParams
743 		 */
744 		'!_loadParams': function () {
745 			return jQuery.extend(DEFAULT_SETTINGS, this._settings);
746 		},
747 
748 		/**
749 		 * Get this page's template.
750 		 *
751 		 * @public
752 		 * @function
753 		 * @name template
754 		 * @memberOf PageAPI
755 		 * @param {funtion(TemplateAPI)=} success Optional callback to receive
756 		 *                                        a {@link TemplateAPI} object
757 		 *                                        as the only argument.
758 		 * @param {function(GCNError):boolean=} error Optional custom error
759 		 *                                            handler.
760 		 * @return {TemplateAPI} This page's parent template.
761 		 */
762 		'!template': function (success, error) {
763 			var id = this._fetched ? this.prop('templateId') : null;
764 			return this._continue(GCN.TemplateAPI, id, success, error);
765 		},
766 
767 		/**
768 		 * @override
769 		 * @see ContentObjectAPI._save
770 		 */
771 		'!_save': function (settings, success, error) {
772 			var that = this;
773 			this._continueWith(function () {
774 				var fork = that._fork();
775 				fork._prepareTagsForSaving(function () {
776 					fork._persist(settings, function () {
777 						if (success) {
778 							success(that);
779 						}
780 						fork._merge();
781 					}, error);
782 				}, error);
783 			}, error);
784 		},
785 
786 		//---------------------------------------------------------------------
787 		// Surface the tag container methods that are applicable for GCN page
788 		// objects.
789 		//---------------------------------------------------------------------
790 
791 		/**
792 		 * Creates a tag of a given tagtype in this page.
793 		 *
794 		 * Exmaple:
795 		 * <pre>
796 		 *	createTag('link', 'http://www.gentics.com', onSuccess, onError);
797 		 * </pre>
798 		 * or
799 		 * <pre>
800 		 *	createTag('link', onSuccess, onError);
801 		 * </pre>
802 		 *
803 		 * @public
804 		 * @function
805 		 * @name createTag
806 		 * @memberOf PageAPI
807 		 * @param {string|number} construct The name of the construct on which
808 		 *                                  the tag to be created should be
809 		 *                                  derived from.  Or the id of that
810 		 * @param {string=} magicValue Optional property that will override the
811 		 *                             default values of this tag type.
812 		 * @param {function(TagAPI)=} success Optional callback that will
813 		 *                                    receive the newly created tag as
814 		 *                                    its only argument.
815 		 * @param {function(GCNError):boolean=} error Optional custom error
816 		 *                                            handler.
817 		 * @return {TagAPI} The newly created tag.
818 		 * @throws INVALID_ARGUMENTS
819 		 */
820 		'!createTag': function () {
821 			return this._createTag.apply(this, arguments);
822 		},
823 
824 		/**
825 		 * Deletes the specified tag from this page.
826 		 *
827 		 * @public
828 		 * @function
829 		 * @name removeTag
830 		 * @memberOf PageAPI
831 		 * @param {string} id The id of the tag to be deleted.
832 		 * @param {function(PageAPI)=} success Optional callback that receive
833 		 *                                     this object as its only
834 		 *                                     argument.
835 		 * @param {function(GCNError):boolean=} error Optional custom error
836 		 *                                            handler.
837 		 */
838 		removeTag: function () {
839 			this._removeTag.apply(this, arguments);
840 		},
841 
842 		/**
843 		 * Deletes a set of tags from this page.
844 		 *
845 		 * @public
846 		 * @function
847 		 * @name removeTags
848 		 * @memberOf PageAPI
849 		 * @param {Array.<string>} ids The ids of the set of tags to be
850 		 *                             deleted.
851 		 * @param {function(PageAPI)=} success Optional callback that receive
852 		 *                                     this object as its only
853 		 *                                     argument.
854 		 * @param {function(GCNError):boolean=} error Optional custom error
855 		 *                                            handler.
856 		 */
857 		removeTags: function () {
858 			this._removeTags.apply(this, arguments);
859 		},
860 
861 		/**
862 		 * Marks the page as to be taken offline. This method will change the
863 		 * state of the page object.
864 		 *
865 		 * @public
866 		 * @function
867 		 * @name takeOffline
868 		 * @memberOf PageAPI
869 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
870 		 *                                    page object as the only argument.
871 		 * @param {function(GCNError):boolean=} error Optional custom error
872 		 *                                            handler.
873 		 */
874 		takeOffline: function (success, error) {
875 			var that = this;
876 
877 			this._read(function () {
878 				that._update('status', STATUS.OFFLINE, error);
879 				if (success) {
880 					that._save(null, success, error);
881 				}
882 			}, error);
883 		},
884 
885 		/**
886 		 * Trigger publish process for the page.
887 		 *
888 		 * @public
889 		 * @function
890 		 * @name publish
891 		 * @memberOf PageAPI
892 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
893 		 *                                    page object as the only argument.
894 		 * @param {function(GCNError):boolean=} error Optional custom error
895 		 *                                            handler.
896 		 */
897 		publish: function (success, error) {
898 			var that = this;
899 			this._continueWith(function () {
900 				that._authAjax({
901 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
902 					     '/publish/' + that.id(),
903 					type: 'POST',
904 					json: {}, // There needs to be at least empty content
905 					          // because of a bug in Jersey.
906 					success: function (response) {
907 						that._data.status = STATUS.PUBLISHED;
908 						if (success) {
909 							success(that);
910 						}
911 					},
912 					error: error
913 				});
914 			});
915 		},
916 
917 		/**
918 		 * Renders a preview of the current page.
919 		 *
920 		 * @public
921 		 * @function
922 		 * @name preview
923 		 * @memberOf PageAPI
924 		 * @param {function(string, PageAPI)} success Callback to receive the
925 		 *                                            rendered page preview as
926 		 *                                            the first argument, and
927 		 *                                            this page object as the
928 		 *                                            second.
929 		 * @param {function(GCNError):boolean=} error Optional custom error
930 		 *                                            handler.
931 		 */
932 		preview: function (success, error) {
933 			var that = this;
934 
935 			this._read(function () {
936 				that._authAjax({
937 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
938 					     '/preview/',
939 					json: {
940 						page: that._data // @FIXME Shouldn't this a be merge of
941 						                 //        the `_shadow' object and the
942 										 //        `_data'.
943 					},
944 					type: 'POST',
945 					error: error,
946 					success: function (response) {
947 						if (success) {
948 							GCN._handleContentRendered(response.preview, that,
949 								function (html) {
950 									success(html, that);
951 								});
952 						}
953 					}
954 				});
955 			}, error);
956 		},
957 
958 		/**
959 		 * Unlocks the page when finishing editing
960 		 *
961 		 * @public
962 		 * @function
963 		 * @name unlock
964 		 * @memberOf PageAPI
965 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
966 		 *                                    page object as the only argument.
967 		 * @param {function(GCNError):boolean=} error Optional custom error
968 		 *                                            handler.
969 		 */
970 		unlock: function (success, error) {
971 			var that = this;
972 			this._continueWith(function () {
973 				that._authAjax({
974 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
975 					     '/cancel/' + that.id(),
976 					type: 'POST',
977 					json: {}, // There needs to be at least empty content
978 					          // because of a bug in Jersey.
979 					error: error,
980 					success: function (response) {
981 						if (success) {
982 							success(that);
983 						}
984 					}
985 				});
986 			});
987 		},
988 
989 		/**
990 		 * @see GCN.ContentObjectAPI._processResponse
991 		 */
992 		'!_processResponse': function (data) {
993 			jQuery.extend(this._data, data[this._type]);
994 
995 			// if data contains page variants turn them into page objects
996 			if (this._data.pageVariants) {
997 				var pagevars = [];
998 				var i;
999 				for (i = 0; i < this._data.pageVariants.length; i++) {
1000 					pagevars.push(this._continue(GCN.PageAPI,
1001 							this._data.pageVariants[i]));
1002 				}
1003 				this._data.pageVariants = pagevars;
1004 			}
1005 		},
1006 
1007 		/**
1008 		 * @override
1009 		 */
1010 		'!_removeAssociatedTagData': function (tagid) {
1011 			var block;
1012 			for (block in this._blocks) {
1013 				if (this._blocks.hasOwnProperty(block) &&
1014 						this._blocks[block].tagname === tagid) {
1015 					delete this._blocks[block];
1016 				}
1017 			}
1018 
1019 			var editable;
1020 			for (editable in this._editables) {
1021 				if (this._editables.hasOwnProperty(editable) &&
1022 						this._editables[editable].tagname === tagid) {
1023 					delete this._editables[editable];
1024 				}
1025 			}
1026 		}
1027 
1028 	});
1029 
1030 	GCN.page = GCN.exposeAPI(PageAPI);
1031 	GCN.PageAPI = PageAPI;
1032 
1033 }(GCN));
1034