1 /*jslint regexp: false */
  2 (function (GCN) {
  3 
  4 	'use strict';
  5 
  6 	/**
  7 	 * @private
  8 	 * @const
  9 	 * @type {object.<string, boolean>} Default page settings.
 10 	 */
 11 	var DEFAULT_SETTINGS = {
 12 		// Load folder information
 13 		folder: true,
 14 
 15 		// Lock page when loading it
 16 		update: true,
 17 
 18 		// Have language variants be included in the page response.
 19 		langvars: true,
 20 
 21 		// Have page variants be included in the page response.
 22 		pagevars: true
 23 	};
 24 
 25 	/**
 26 	 * Match URL to internal page
 27 	 *
 28 	 * @const
 29 	 * @type {RegExp}
 30 	 */
 31 	var INTERNAL_PAGEURL = /^(?:http.*)?\/CNPortletapp\/alohapage\?.*realid=([0-9]+).*$/;
 32 
 33 	/**
 34 	 * Match URL to internal file
 35 	 *
 36 	 * @const
 37 	 * @type {RegExp}
 38 	 */
 39 	var INTERNAL_FILEURL = /^http.*\?.*&do=16000&id=([0-9]+)&.*$/;
 40 
 41 
 42 	/**
 43 	 * Match URL to anchor
 44 	 *
 45 	 * @const
 46 	 * @type {RegExp}
 47 	 */
 48 	var ANCHOR_LINK = /(.+)#(.+)/;
 49 
 50 	/**
 51 	 * Checks whether the given tag is a magic link block.
 52 	 *
 53 	 * @param {TagAPI} tag A tag that must already have been fetched.
 54 	 * @param {Object} constructs Set of constructs.
 55 	 * @return {boolean} True if the given tag's constructId is equal to the
 56 	 *                   `MAGIC_LINK' value.
 57 	 */
 58 	function isMagicLinkTag(tag, constructs) {
 59 		return !!(constructs[GCN.settings.MAGIC_LINK]
 60 					&& (constructs[GCN.settings.MAGIC_LINK].constructId
 61 						=== tag.prop('constructId')));
 62 	}
 63 
 64 	/**
 65 	 * Given a page object, returns a jQuery set containing DOM elements for
 66 	 * each of the page's editable that is attached to the document.
 67 	 *
 68 	 * @param {PageAPI} page A page object.
 69 	 * @return {jQuery.<HTMLElement>} A jQuery set of editable DOM elements.
 70 	 */
 71 	function getEditablesInDocument(page) {
 72 		var id;
 73 		var $editables = jQuery();
 74 		var editables = page._editables;
 75 		for (id in editables) {
 76 			if (editables.hasOwnProperty(id)) {
 77 				$editables = $editables.add('#' + id);
 78 			}
 79 		}
 80 		return $editables;
 81 	}
 82 
 83 	/**
 84 	 * Returns all editables associated with the given page that have been
 85 	 * rendered in the document for editing.
 86 	 *
 87 	 * @param {PageAPI} page
 88 	 * @return {object} A set of editable objects.
 89 	 */
 90 	function getEditedEditables(page) {
 91 		return page._editables;
 92 	}
 93 
 94 	/**
 95 	 * Derives a list of the blocks that were rendered inside at least one of
 96 	 * the given page's edit()ed editables.
 97 	 *
 98 	 * @param {PageAPI} page Page object.
 99 	 * @return {Array.<object>} The set of blocks contained in any of the
100 	 *                          page's rendered editables.
101 	 */
102 	function getRenderedBlocks(page) {
103 		var editables = getEditedEditables(page);
104 		var id;
105 		var renderedBlocks = [];
106 		for (id in editables) {
107 			if (editables.hasOwnProperty(id)) {
108 				if (editables[id]._gcnContainedBlocks) {
109 					renderedBlocks = renderedBlocks.concat(
110 						editables[id]._gcnContainedBlocks
111 					);
112 				}
113 			}
114 		}
115 		return renderedBlocks;
116 	}
117 
118 	/**
119 	 * Gets the DOM element associated with the given block.
120 	 *
121 	 * @param {object} block
122 	 * @return {?jQuery.<HTMLElement>} A jQuery unit set of the block's
123 	 *                                 corresponding DOM element, or null if no
124 	 *                                 element for the given block exists in
125 	 *                                 the document.
126 	 */
127 	function getElement(block) {
128 		var $element = jQuery('#' + block.element);
129 		return $element.length ? $element : null;
130 	}
131 
132 	/**
133 	 * Retrieves a jQuery set of all link elements that are contained in
134 	 * editables associated with the given page.
135 	 *
136 	 * @param {PageAPI} page
137 	 * @return {jQuery.<HTMLElement>} A jQuery set of link elements.
138 	 */
139 	function getEditableLinks(page) {
140 		return getEditablesInDocument(page).find('a[href]');
141 	}
142 
143 	/**
144 	 * Determines all blocks that no longer need their tags to be kept in the
145 	 * given page's tag list.
146 	 *
147 	 * @param {PageAPI} page
148 	 * @param {function(Array.<object>)} success A callback function that
149 	 *                                           receives a list of obsolete
150 	 *                                           blocks.
151 	 * @param {function(GCNError):boolean=} error Optional custom error
152 	 *                                            handler.
153 	 */
154 	function getObsoleteBlocks(page, success, error) {
155 		var blocks = getRenderedBlocks(page);
156 		if (0 === blocks.length) {
157 			success([]);
158 			return;
159 		}
160 		var $links = getEditableLinks(page);
161 		var numToProcess = blocks.length;
162 		var obsolete = [];
163 		var onSuccess = function () {
164 			if ((0 === --numToProcess) && success) {
165 				success(obsolete);
166 			}
167 		};
168 		var onError = function (GCNError) {
169 			if (error) {
170 				return error(GCNError);
171 			}
172 		};
173 		page.constructs(function (constructs) {
174 			var processTag = function (block) {
175 				page.tag(block.tagname, function (tag) {
176 					if (isMagicLinkTag(tag, constructs) && !getElement(block)) {
177 						obsolete.push(block);
178 					}
179 					onSuccess();
180 				}, onError);
181 			};
182 			var i;
183 			for (i = 0; i < blocks.length; i++) {
184 				processTag(blocks[i]);
185 			}
186 		});
187 	}
188 
189 	/**
190 	 * Checks whether or not the given block has a corresponding element in the
191 	 * document DOM.
192 	 *
193 	 * @private
194 	 * @static
195 	 * @param {object}
196 	 * @return {boolean} True if an inline element for this block exists.
197 	 */
198 	function hasInlineElement(block) {
199 		return !!getElement(block);
200 	}
201 
202 	/**
203 	 * Matches "aloha-*" class names.
204 	 *
205 	 * @const
206 	 * @type {RegExp}
207 	 */
208 	var ALOHA_CLASS_NAMES = /\baloha-[a-z0-9\-\_]*\b/gi;
209 
210 	/**
211 	 * Strips unwanted names from the given className string.
212 	 *
213 	 * All class names beginning with "aloha-block*" will be removed.
214 	 *
215 	 * @param {string} classes Space seperated list of classes.
216 	 * @return {string} Sanitized classes string.
217 	 */
218 	function cleanBlockClasses(classes) {
219 		return classes ? jQuery.trim(classes.replace(ALOHA_CLASS_NAMES, ''))
220 		               : '';
221 	}
222 
223 
224 	/**
225 	 * Fetches the href attribute of a link and checks whether the browser
226 	 * added an absolute URL to an anchor link on the same page. This absolute
227 	 * url to the same page will be stripped before returning the href.
228 	 *
229 	 * @param {jQuery.<HTMLElement>} $link A link in an editable
230 	 * @return {string} href of the supplied link.
231 	 */
232 	function getHrefFromLink($link) {
233 		var href = $link.attr('href') || '';
234 		var anchorUrlMatch = href.match(ANCHOR_LINK);
235 		if (anchorUrlMatch && anchorUrlMatch[1] === window.location.href) {
236 			// check whether the href contains a link to an anchor on the current page
237 			// we only want the anchor link and not the absolute url
238 			href = '#' + anchorUrlMatch[2];
239 			$link.attr('href', href);
240 		}
241 		return href;
242 	}
243 
244 	/**
245 	 * Determines the backend object that was set to the given link.
246 	 *
247 	 * @param {jQuery.<HTMLElement>} $link A link in an editable.
248 	 * @return {Object} An object containing the gtxalohapagelink part keyword
249 	 *                  and value.  The keyword may be either be "url" or
250 	 *                  "fileurl" depending on the type of object linked to.
251 	 *                  The value may be a string url ("http://...") for
252 	 *                  external links or an integer for internal links.
253 	 */
254 	function getTagPartsFromLink($link) {
255 		var linkData = $link.attr('data-gentics-aloha-object-id');
256 		var tagparts = {
257 			text: $link.html(),
258 			title: $link.attr('title'),
259 			target: $link.attr('target'),
260 			language: $link.attr('hreflang'),
261 			'class': cleanBlockClasses($link.attr('class'))
262 		};
263 
264 		if (!linkData) {
265 			// check whether the href contains links to internal pages or files
266 			var href = getHrefFromLink($link),
267 				pageUrlMatch = href.match(INTERNAL_PAGEURL),
268 				fileUrlMatch = href.match(INTERNAL_FILEURL);
269 			if (pageUrlMatch) {
270 				tagparts.url = parseInt(pageUrlMatch[1], 10);
271 				tagparts.fileurl = 0;
272 				return tagparts;
273 			} else if (fileUrlMatch) {
274 				tagparts.url = 0;
275 				tagparts.fileurl = parseInt(fileUrlMatch[1], 10);
276 				return tagparts;
277 			} else {
278 				tagparts.url = $link.attr('href');
279 				return tagparts;
280 			}
281 		}
282 
283 		var idParts = linkData.split('.');
284 
285 		if (2 !== idParts.length) {
286 			tagparts.url = linkData;
287 		} else if ('10007' === idParts[0]) {
288 			tagparts.url = parseInt(idParts[1], 10);
289 			tagparts.fileurl = 0;
290 		} else if ('10008' === idParts[0] || '10011' === idParts[0]) {
291 			tagparts.url = 0;
292 			tagparts.fileurl = parseInt(idParts[1], 10);
293 		} else {
294 			tagparts.url = $link.attr('href');
295 		}
296 
297 		return tagparts;
298 	}
299 
300 	/**
301 	 * Checks whether a page object has a corresponding tag for a given link
302 	 * DOM element.
303 	 *
304 	 * @param {PageAPI} page The page object in which to look for the link tag.
305 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
306 	 *                                     DOM element.
307 	 * @return {boolean} True if there is a tag on the page that corresponds with
308 	 *                   the givn link.
309 	 */
310 	function hasTagForLink(page, $link) {
311 		var id = $link.attr('id');
312 		return !!(id && page._getBlockById(id));
313 	}
314 
315 	/**
316 	 * Checks whether or not the given part has a part type of the given
317 	 * name
318 	 *
319 	 * @param {TagAPI} tag
320 	 * @param {string} part Part name
321 	 * @return {boolean} True of part exists in the given tag.
322 	 */
323 	function hasTagPart(tag, part) {
324 		return !!tag._data.properties[part];
325 	}
326 
327 	/**
328 	 * Updates the parts of a tag in the page object that corresponds to the
329 	 * given link DOM element.
330 	 *
331 	 * @param {PageAPI} page
332 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
333 	 *                                     DOM element.
334 	 */
335 	function updateTagForLink(page, $link) {
336 		var block = page._getBlockById($link.attr('id'));
337 		// ASSERT(block)
338 		var tag = page.tag(block.tagname);
339 		var parts = getTagPartsFromLink($link);
340 		var part;
341 		for (part in parts) {
342 			if (parts.hasOwnProperty(part) && hasTagPart(tag, part)) {
343 				tag.part(part, parts[part]);
344 			}
345 		}
346 	}
347 
348 	/**
349 	 * Creates a new tag for the given link in the page.
350 	 *
351 	 * @param {PageAPI} page
352 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
353 	 *                                     element.
354 	 * @param {function} success Callback function that whill be called when
355 	 *                           the new tag is created.
356 	 * @param {function(GCNError):boolean=} error Optional custom error
357 	 *                                            handler.
358 	 */
359 	function createTagForLink(page, $link, success, error) {
360 		page.createTag({
361 			keyword: GCN.settings.MAGIC_LINK,
362 			magicValue: $link.html()
363 		}).edit(function (html, tag) {
364 			// Copy over the rendered id value for the link so that we can bind
365 			// the link in the DOM with the newly created block.
366 			$link.attr('id', jQuery(html).attr('id'));
367 			updateTagForLink(page, $link);
368 			success();
369 		}, error);
370 	}
371 
372 	/**
373 	 * Create tags for all new links in the page
374 	 * 
375 	 * @param {PageApi} page
376 	 * @param {jQuery.<HTMLElement>} $gcnlinks jQuery unit set containing links
377 	 * @param {function} success Callback function that will be called when the tags are created
378 	 * @param {function(GCNError)} error Optional custom error handler
379 	 */
380 	function createMissingLinkTags(page, $gcnlinks, success, error) {
381 		var $newGcnLinks = $gcnlinks.filter(function () {
382 			return !hasTagForLink(page, jQuery(this));
383 		}), linkData = {create:{}}, i = 0, id;
384 
385 		if ($newGcnLinks.length > 0) {
386 			$newGcnLinks.each(function (index) {
387 				id = 'link' + (i++);
388 				linkData.create[id] = {
389 					data: {
390 						keyword: GCN.settings.MAGIC_LINK,
391 						magicValue: jQuery(this).html()
392 					},
393 					obj: jQuery(this)
394 				};
395 			});
396 			page._createTags(linkData, function () {
397 				var id;
398 				for (id in linkData.create) {
399 					if (linkData.create.hasOwnProperty(id)) {
400 						linkData.create[id].obj.attr('id', jQuery(linkData.create[id].response.html).attr('id'));
401 					}
402 				}
403 				page._processRenderedTags(linkData.response);
404 				success();
405 			}, error);
406 		} else {
407 			success();
408 		}
409 
410 	}
411 
412 	/**
413 	 * Identifies internal GCN links in the given page's rendered editables,
414 	 * and updates their corresponding content tags, or create new tags for the
415 	 * if they are new links.
416 	 *
417 	 *  @param {PageAPI} page
418 	 *  @param {function} success
419 	 *  @param {function} error
420 	 */
421 	function processGCNLinks(page, success, error) {
422 		var $links = getEditableLinks(page);
423 		var $gcnlinks = $links.filter(function () {
424 			return this.isContentEditable;
425 		}).filter(':not(.aloha-editable)');
426 		if (0 === $gcnlinks.length) {
427 			if (success) {
428 				success();
429 			}
430 			return;
431 		}
432 		var numToProcess = $gcnlinks.length;
433 		var onSuccess = function () {
434 			if ((0 === --numToProcess) && success) {
435 				success();
436 			}
437 		};
438 		var onError = function (GCNError) {
439 			if (error) {
440 				return error(GCNError);
441 			}
442 		};
443 
444 		// When a link was copied it would result in two magic link tags
445 		// with the same ID. We remove the id attribute from such duplicates
446 		// so that hasTagForLink() will return false and create a new tag
447 		// for the copied link.
448 		var alreadyExists = {};
449 
450 		$links.each(function () {
451 			var $link = jQuery(this);
452 			var id = $link.attr('id');
453 
454 			if (id) {
455 				if (alreadyExists[id]) {
456 					$link.removeAttr('id');
457 				} else {
458 					alreadyExists[id] = true;
459 				}
460 			}
461 		});
462 
463 		createMissingLinkTags(page, $gcnlinks, function () {
464 			var i;
465 			for (i = 0; i < $gcnlinks.length; i++) {
466 				if (hasTagForLink(page, $gcnlinks.eq(i))) {
467 					updateTagForLink(page, $gcnlinks.eq(i));
468 					onSuccess();
469 				} else {
470 					onError(GCN.error('500', 'Missing Tag for Link', $gcnlinks.get(i)));
471 				}
472 			}
473 		}, onError);
474 	}
475 
476 	/**
477 	 * Adds the given blocks into the page's list of blocks that are to be
478 	 * deleted when the page is saved.
479 	 *
480 	 * @param {PageAPI} page
481 	 * @param {Array.<object>} blocks Blocks that are to be marked for deletion.
482 	 */
483 	function deleteBlocks(page, blocks) {
484 		blocks = jQuery.isArray(blocks) ? blocks : [blocks];
485 		var i;
486 		var success = function(tag) {
487 			tag.remove();
488 		};
489 		for (i = 0; i < blocks.length; i++) {
490 			if (-1 ===
491 					jQuery.inArray(blocks[i].tagname, page._deletedBlocks)) {
492 				page._deletedBlocks.push(blocks[i].tagname);
493 			}
494 			delete page._blocks[blocks[i].element];
495 
496 			page.tag(blocks[i].tagname, success);
497 		}
498 	}
499 
500 	/**
501 	 * Removes all tags on the given page which belong to links that are no
502 	 * longer present in any of the page's rendered editables.
503 	 *
504 	 * @param {PageAPI} page
505 	 * @param {function} success Callback function that will be invoked when
506 	 *                           all obsolete tags have been successfully
507 	 *                           marked for deletion.
508 	 * @param {function(GCNError):boolean=} error Optional custom error
509 	 *                                            handler.
510 	 */
511 	function deleteObsoleteLinkTags(page, success, error) {
512 		getObsoleteBlocks(page, function (obsolete) {
513 			deleteBlocks(page, obsolete);
514 			if (success) {
515 				success();
516 			}
517 		}, error);
518 	}
519 
520 	/**
521 	 * Searches for the an Aloha editable object of the given id.
522 	 *
523 	 * @TODO: Once Aloha.getEditableById() is patched to not cause an
524 	 *        JavaScript exception if the element for the given ID is not found
525 	 *        then we can deprecate this function and use Aloha's instead.
526 	 *
527 	 * @static
528 	 * @param {string} id Id of Aloha.Editable object to find.
529 	 * @return {Aloha.Editable=} The editable object, if wound; otherwise null.
530 	 */
531 	function getAlohaEditableById(id) {
532 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
533 		if (!Aloha) {
534 			return null;
535 		}
536 
537 		// If the element is a textarea then route to the editable div.
538 		var element = jQuery('#' + id);
539 		if (element.length &&
540 				element[0].nodeName.toLowerCase() === 'textarea') {
541 			id += '-aloha';
542 		}
543 
544 		var editables = Aloha.editables;
545 		var j = editables.length;
546 		while (j) {
547 			if (editables[--j].getId() === id) {
548 				return editables[j];
549 			}
550 		}
551 
552 		return null;
553 	}
554 
555 	/**
556 	 * For a given list of editables and a list of blocks, determines in which
557 	 * editable each block is contained.  The result is a map of block sets.
558 	 * Each of these sets of blocks are mapped against the id of the editable
559 	 * in which they are rendered.
560 	 *
561 	 * @param {string} content The rendered content in which both the list of
562 	 *                         editables, and blocks are contained.
563 	 * @param {Array.<object>} editables A list of editables contained in the
564 	 *                                   content.
565 	 * @param {Array.<object>} blocks A list of blocks containd in the content.
566 	 * @return {object<string, Array>} A object whose keys are editable ids,
567 	 *                                  and whose values are arrays of blocks
568 	 *                                  contained in the corresponding
569 	 *                                  editable.
570 	 */
571 	function categorizeBlocksAgainstEditables(content, editables, blocks) {
572 		var i;
573 		var $content = jQuery('<div>' + content + '</div>');
574 		var sets = {};
575 		var editableId;
576 
577 		var editablesSelectors = [];
578 		for (i = 0; i < editables.length; i++) {
579 			editablesSelectors.push('#' + editables[i].element);
580 		}
581 
582 		var markerClass = 'aloha-editable-tmp-marker__';
583 		var $editables = $content.find(editablesSelectors.join(','));
584 		$editables.addClass(markerClass);
585 
586 		var $block;
587 		var $parent;
588 		for (i = 0; i < blocks.length; i++) {
589 			$block = $content.find('#' + blocks[i].element);
590 			if ($block.length) {
591 				$parent = $block.closest('.' + markerClass);
592 				if ($parent.length) {
593 					editableId = $parent.attr('id');
594 					if (editableId) {
595 						if (!sets[editableId]) {
596 							sets[editableId] = [];
597 						}
598 						sets[editableId].push(blocks[i]);
599 					}
600 				}
601 			}
602 		}
603 
604 		return sets;
605 	}
606 
607 	/**
608 	 * Causes the given editables to be tracked, so that when the content
609 	 * object is saved, these editables will be processed.
610 	 *
611 	 * @private
612 	 * @param {PageAPI} page
613 	 * @param {Array.<object>} editables A set of object representing
614 	 *                                   editable tags that have been
615 	 *                                   rendered.
616 	 */
617 	function trackEditables(page, editables) {
618 		if (!page.hasOwnProperty('_editables')) {
619 			page._editables = {};
620 		}
621 		var i;
622 		for (i = 0; i < editables.length; i++) {
623 			page._editables[editables[i].element] = editables[i];
624 		}
625 	}
626 
627 	/**
628 	 * Causes the given blocks to be tracked so that when the content object is
629 	 * saved, these editables will be processed.
630 	 *
631 	 * @private
632 	 * @param {PageAPI} page
633 	 * @param {Array.<object>} blocks An set of object representing block
634 	 *                                tags that have been rendered.
635 	 */
636 	function trackBlocks(page, blocks) {
637 		if (!page.hasOwnProperty('_blocks')) {
638 			page._blocks = {};
639 		}
640 		var i;
641 		for (i = 0; i < blocks.length; i++) {
642 			page._blocks[blocks[i].element] = blocks[i];
643 		}
644 	}
645 
646 	/**
647 	 * Associates a list of blocks with an editable so that it can later be
648 	 * determined which blocks are contained inside which editable.
649 	 *
650 	 * @param object
651 	 */
652 	function associateBlocksWithEditable(editable, blocks) {
653 		if (!jQuery.isArray(editable._gcnContainedBlocks)) {
654 			editable._gcnContainedBlocks = [];
655 		}
656 
657 		var i, j;
658 		for (i = 0; i < blocks.length; i++) {
659 			for (j = 0; j < editable._gcnContainedBlocks.length; j++) {
660 				if (blocks[i].element === editable._gcnContainedBlocks[j].element
661 						&& blocks[i].tagname === editable._gcnContainedBlocks[j].tagname) {
662 					// Prevent duplicates
663 					continue;
664 				}
665 
666 				editable._gcnContainedBlocks.push(blocks[i]);
667 			}
668 		}
669 	}
670 
671 	/**
672 	 * Extracts the editables and blocks that have been rendered from the
673 	 * REST API render call's response data, and stores them in the page.
674 	 *
675 	 * @param {PageAPI} page The page inwhich to track the incoming tags.
676 	 * @param {object} data Raw data containing editable and block tags
677 	 * information.
678 	 * @return {object} A object containing to lists: one list of blocks, and
679 	 *                  another of editables.
680 	 */
681 	function trackRenderedTags(page, data) {
682 		var tags = GCN.TagContainerAPI.getEditablesAndBlocks(data);
683 
684 		var containment = categorizeBlocksAgainstEditables(
685 			data.content,
686 			tags.editables,
687 			tags.blocks
688 		);
689 
690 		trackEditables(page, tags.editables);
691 		trackBlocks(page, tags.blocks);
692 
693 		jQuery.each(containment, function (editable, blocks) {
694 			if (page._editables[editable]) {
695 				associateBlocksWithEditable(page._editables[editable], blocks);
696 			}
697 		});
698 
699 		return tags;
700 	}
701 
702 	/**
703 	 * @private
704 	 * @const
705 	 * @type {number}
706 	 */
707 	//var TYPE_ID = 10007;
708 
709 	/**
710 	 * @private
711 	 * @const
712 	 * @type {Enum}
713 	 */
714 	var STATUS = {
715 
716 		// page was not found in the database
717 		NOTFOUND: -1,
718 
719 		// page is locally modified and not yet (re-)published
720 		MODIFIED: 0,
721 
722 		// page is marked to be published (dirty)
723 		TOPUBLISH: 1,
724 
725 		// page is published and online
726 		PUBLISHED: 2,
727 
728 		// Page is offline
729 		OFFLINE: 3,
730 
731 		// Page is in the queue (publishing of the page needs to be affirmed)
732 		QUEUE: 4,
733 
734 		// page is in timemanagement and outside of the defined timespan
735 		// (currently offline)
736 		TIMEMANAGEMENT: 5,
737 
738 		// page is to be published at a given time (not yet)
739 		TOPUBLISH_AT: 6
740 	};
741 
742 	/**
743 	 * @class
744 	 * @name PageAPI
745 	 * @extends ContentObjectAPI
746 	 * @extends TagContainerAPI
747 	 * 
748 	 * @param {number|string}
749 	 *            id of the page to be loaded
750 	 * @param {function(ContentObjectAPI))=}
751 	 *            success Optional success callback that will receive this
752 	 *            object as its only argument.
753 	 * @param {function(GCNError):boolean=}
754 	 *            error Optional custom error handler.
755 	 * @param {object}
756 	 *            settings additional settings for object creation. These
757 	 *            correspond to options available from the REST-API and will
758 	 *            extend or modify the PageAPI object.
759 	 *            <dl>
760 	 *            <dt>update: true</dt>
761 	 *            <dd>Whether the page should be locked in the backend when
762 	 *            loading it. default: true</dd>
763 	 *            <dt>template: true</dt>
764 	 *            <dd>Whether the template information should be embedded in
765 	 *            the page object. default: true</dd>
766 	 *            <dt>folder: true</dt>
767 	 *            <dd>Whether the folder information should be embedded in the
768 	 *            page object. default: true <b>WARNING</b>: do not turn this
769 	 *            option off - it will leave the API in a broken state.</dd>
770 	 *            <dt>langvars: true</dt>
771 	 *            <dd>When the language variants shall be embedded in the page
772 	 *            response. default: true</dd>
773 	 *            <dt>workflow: false</dt>
774 	 *            <dd>When the workflow information shall be embedded in the
775 	 *            page response. default: false</dd>
776 	 *            <dt>pagevars: true</dt>
777 	 *            <dd>When the page variants shall be embedded in the page
778 	 *            response. Page variants will contain folder information.
779 	 *            default: true</dd>
780 	 *            <dt>translationstatus: false</dt>
781 	 *            <dd>Will return information on the page's translation status.
782 	 *            default: false</dd>
783 	 *            </dl>
784 	 */
785 	var PageAPI = GCN.defineChainback({
786 		/** @lends PageAPI */
787 
788 		__chainbacktype__: 'PageAPI',
789 		_extends: [ GCN.TagContainerAPI, GCN.ContentObjectAPI ],
790 		_type: 'page',
791 
792 		/**
793 		 * A hash set of block tags belonging to this page.  This set grows as
794 		 * this page's tags are rendered.
795 		 *
796 		 * @private
797 		 * @type {Array.<object>}
798 		 */
799 		_blocks: {},
800 
801 		/**
802 		 * A hash set of editable tags belonging to this page.  This set grows
803 		 * as this page's tags are rendered.
804 		 *
805 		 * @private
806 		 * @type {Array.<object>}
807 		 */
808 		_editables: {},
809 
810 		/**
811 		 * Writable properties for the page object. Currently the following
812 		 * properties are writeable: cdate, description, fileName, folderId,
813 		 * name, priority, templateId. WARNING: changing the folderId might not
814 		 * work as expected.
815 		 * 
816 		 * @type {Array.string}
817 		 * @const
818 		 */
819 		WRITEABLE_PROPS: [
820 		                  'cdate',
821 		                  'description',
822 		                  'fileName',
823 		                  'folderId', // @TODO Check if moving a page is
824 		                              //       implemented correctly.
825 		                  'name',
826 		                  'priority',
827 		                  'templateId',
828 		                  'timeManagement'
829 		                  ],
830 
831 		/**
832 		 * @type {object} Constraints for writeable props
833 		 * @const
834 		 *
835 		 */
836 		WRITEABLE_PROPS_CONSTRAINTS: {
837 			'name': {
838 				maxLength: 255
839 			}
840 		},
841 
842 		/**
843 		 * Gets all blocks that are associated with this page.
844 		 *
845 		 * It is important to note that the set of blocks in the returned array
846 		 * will only include those that are the returned by the server when
847 		 * calling  edit() on a tag that belongs to this page.
848 		 *
849 		 * @return {Array.<object>} The set of blocks that have been
850 		 *                          initialized by calling edit() on one of
851 		 *                          this page's tags.
852 		 */
853 		'!blocks': function () {
854 			return this._blocks;
855 		},
856 
857 		/**
858 		 * Retrieves a block with the given id among the blocks that are
859 		 * tracked by this page content object.
860 		 *
861 		 * @private
862 		 * @param {string} id The block's id.
863 		 * @return {?object} The block data object.
864 		 */
865 		'!_getBlockById': function (id) {
866 			return this._blocks[id];
867 		},
868 
869 		/**
870 		 * Extracts the editables and blocks that have been rendered from the
871 		 * REST API render call's response data, and stores them in the page.
872 		 *
873 		 * @override
874 		 */
875 		'!_processRenderedTags': function (data) {
876 			return trackRenderedTags(this, data);
877 		},
878 
879 		/**
880 		 * Processes this page's tags in preparation for saving.
881 		 *
882 		 * The preparation process:
883 		 *
884 		 * 1. For all editables associated with this page, determine which of
885 		 *    their blocks have been rendered into the DOM for editing so that
886 		 *    changes to the DOM can be reflected in the corresponding data
887 		 *    structures before pushing the tags to the server.
888 		 *
889 		 * 2.
890 		 *
891 		 * Processes rendered tags, and updates the `_blocks' and `_editables'
892 		 * arrays accordingly.  This function is called during pre-saving to
893 		 * update this page's editable tags.
894 		 *
895 		 * @private
896 		 */
897 		'!_prepareTagsForSaving': function (success, error) {
898 			if (!this.hasOwnProperty('_deletedBlocks')) {
899 				this._deletedBlocks = [];
900 			}
901 			var page = this;
902 			processGCNLinks(page, function () {
903 				deleteObsoleteLinkTags(page, function () {
904 					page._updateEditableBlocks();
905 					if (success) {
906 						success();
907 					}
908 				}, error);
909 			}, error);
910 		},
911 
912 		/**
913 		 * Writes the contents of editables back into their corresponding tags.
914 		 * If a corresponding tag cannot be found for an editable, a new one
915 		 * will be created for it.
916 		 *
917 		 * A reference for each editable tag is then added to the `_shadow'
918 		 * object in order that the tag will be sent with the save request.
919 		 *
920 		 * @private
921 		 */
922 		'!_updateEditableBlocks': function () {
923 			var $element;
924 			var id;
925 			var editables = this._editables;
926 			var tags = this._data.tags;
927 			var tagname;
928 			var html;
929 			var alohaEditable;
930 			var $cleanElement;
931 			var customSerializer;
932 
933 			for (id in editables) {
934 				if (editables.hasOwnProperty(id)) {
935 					$element = jQuery('#' + id);
936 
937 					// Because the editable may not have have been rendered into
938 					// the document DOM.
939 					if (0 === $element.length) {
940 						continue;
941 					}
942 
943 					tagname = editables[id].tagname;
944 
945 					if (!tags[tagname]) {
946 						tags[tagname] = {
947 							name       : tagname,
948 							active     : true,
949 							properties : {}
950 						};
951 					} else {
952 						// Because it is sensible to assume that every editable
953 						// that was rendered for editing is intended to be an
954 						// activate tag.
955 						tags[tagname].active = true;
956 					}
957 
958 					// Because editables that have been aloha()fied, must have
959 					// their contents retrieved by getContents() in order to get
960 					// clean HTML.
961 
962 					alohaEditable = getAlohaEditableById(id);
963 
964 					if (alohaEditable) {
965 						// Avoid the unnecessary overhead of custom editable
966 						// serialization by calling html ourselves.
967 						$cleanElement = jQuery('<div>').append(
968 							alohaEditable.getContents(true)
969 						);
970 						alohaEditable.setUnmodified();
971 						// Apply the custom editable serialization as the last step.
972 						customSerializer = window.Aloha.Editable.getContentSerializer();
973 						html = this.encode($cleanElement, customSerializer);
974 					} else {
975 						html = this.encode($element);
976 					}
977 					// If the editable is backed by a parttype, that
978 					// would replace newlines by br tags while
979 					// rendering, remove all newlines before saving back
980 					if ($element.hasClass('GENTICS_parttype_text') ||
981 						$element.hasClass('GENTICS_parttype_texthtml') ||
982 						$element.hasClass('GENTICS_parttype_java_editor') ||
983 						$element.hasClass('GENTICS_parttype_texthtml_long')) {
984 						html = html.replace(/(\r\n|\n|\r)/gm,"");
985 					}
986 
987 					tags[tagname].properties[editables[id].partname] = {
988 						stringValue: html,
989 						type: 'RICHTEXT'
990 					};
991 
992 					this._update('tags.' + GCN.escapePropertyName(tagname),
993 						tags[tagname]);
994 				}
995 			}
996 		},
997 
998 		/**
999 		 * @see ContentObjectAPI.!_loadParams
1000 		 */
1001 		'!_loadParams': function () {
1002 			return jQuery.extend(DEFAULT_SETTINGS, this._settings);
1003 		},
1004 
1005 		/**
1006 		 * Get this page's template.
1007 		 *
1008 		 * @public
1009 		 * @function
1010 		 * @name template
1011 		 * @memberOf PageAPI
1012 		 * @param {funtion(TemplateAPI)=} success Optional callback to receive
1013 		 *                                        a {@link TemplateAPI} object
1014 		 *                                        as the only argument.
1015 		 * @param {function(GCNError):boolean=} error Optional custom error
1016 		 *                                            handler.
1017 		 * @return {TemplateAPI} This page's parent template.
1018 		 */
1019 		'!template': function (success, error) {
1020 			var id = this._fetched ? this.prop('templateId') : null;
1021 			return this._continue(GCN.TemplateAPI, id, success, error);
1022 		},
1023 
1024 		/**
1025 		 * Cache of constructs for this page.
1026 		 * Should be cleared when page is saved.
1027 		 */
1028 		_constructs: null,
1029 
1030 		/**
1031 		 * List of success and error callbacks that need to be called
1032 		 * once the constructs are loaded
1033 		 * @private
1034 		 * @type {array.<object>}
1035 		 */
1036 		_constructLoadHandlers: null,
1037 
1038 		/**
1039 		 * Retrieve the list of constructs of the tag that are used in this
1040 		 * page.
1041 		 *
1042 		 * Note that tags that have been created on this page locally, but have
1043 		 * yet to be persisted to the server (unsaved tags), will not have their
1044 		 * constructs included in the list unless their constructs are used by
1045 		 * other saved tags.
1046 		 */
1047 		'!constructs': function (success, error) {
1048 			var page = this;
1049 			if (page._constructs) {
1050 				return success(page._constructs);
1051 			}
1052 
1053 			// if someone else is already loading the constructs, just add the callbacks
1054 			page._constructLoadHandlers = page._constructLoadHandlers || [];
1055 			if (page._constructLoadHandlers.length > 0) {
1056 				page._constructLoadHandlers.push({success: success, error: error});
1057 				return;
1058 			}
1059 
1060 			// we are the first to load the constructs, register the callbacks and
1061 			// trigger the ajax call
1062 			page._constructLoadHandlers.push({success: success, error: error});
1063 			page._authAjax({
1064 				url: GCN.settings.BACKEND_PATH
1065 				     + '/rest/construct/list.json?pageId=' + page.id(),
1066 				type: 'GET',
1067 				error: function (xhr, status, msg) {
1068 					var i;
1069 					for (i = 0; i < page._constructLoadHandlers.length; i++) {
1070 						GCN.handleHttpError(xhr, msg, page._constructLoadHandlers[i].error);
1071 					}
1072 				},
1073 				success: function (response) {
1074 					var i;
1075 					if (GCN.getResponseCode(response) === 'OK') {
1076 						page._constructs = GCN.mapConstructs(response.constructs);
1077 						for (i = 0; i < page._constructLoadHandlers.length; i++) {
1078 							page._invoke(page._constructLoadHandlers[i].success, [page._constructs]);
1079 						}
1080 					} else {
1081 						for (i = 0; i < page._constructLoadHandlers.length; i++) {
1082 							GCN.handleResponseError(response, page._constructLoadHandlers[i].error);
1083 						}
1084 					}
1085 				},
1086 
1087 				complete: function () {
1088 					page._constructLoadHandlers = [];
1089 				}
1090 			});
1091 		},
1092 
1093 		/**
1094 		 * @override
1095 		 * @see ContentObjectAPI._save
1096 		 */
1097 		'!_save': function (settings, success, error) {
1098 			var page = this;
1099 			this._fulfill(function () {
1100 				page._read(function () {
1101 					var fork = page._fork();
1102 					fork._prepareTagsForSaving(function () {
1103 						GCN.pub('page.before-saved', fork);
1104 						fork._persist(settings, function () {
1105 							if (success) {
1106 								page._constructs = null;
1107 								fork._merge(false);
1108 								page._invoke(success, [page]);
1109 								page._vacate();
1110 							} else {
1111 								fork._merge();
1112 							}
1113 						}, error);
1114 					}, error);
1115 				}, error);
1116 			}, error);
1117 		},
1118 
1119 		//---------------------------------------------------------------------
1120 		// Surface the tag container methods that are applicable for GCN page
1121 		// objects.
1122 		//---------------------------------------------------------------------
1123 
1124 		/**
1125 		 * Creates a tag of a given tagtype in this page.
1126 		 * The first parameter should either be the construct keyword or ID,
1127 		 * or an object containing exactly one of the following property sets:<br/>
1128 		 * <ol>
1129 		 *   <li><i>keyword</i> to create a tag based on the construct with given keyword</li>
1130 		 *   <li><i>constructId</i> to create a tag based on the construct with given ID</li>
1131 		 *   <li><i>sourcePageId</i> and <i>sourceTagname</i> to create a tag as copy of the given tag from the page</li>
1132 		 * </ol>
1133 		 *
1134 		 * Exmaple:
1135 		 * <pre>
1136 		 *  createTag('link', onSuccess, onError);
1137 		 * </pre>
1138 		 * or
1139 		 * <pre>
1140 		 *  createTag({keyword: 'link', magicValue: 'http://www.gentics.com'}, onSuccess, onError);
1141 		 * </pre>
1142 		 * or
1143 		 * <pre>
1144 		 *  createTag({sourcePageId: 4711, sourceTagname: 'link'}, onSuccess, onError);
1145 		 * </pre>
1146 		 *
1147 		 * @public
1148 		 * @function
1149 		 * @name createTag
1150 		 * @memberOf PageAPI
1151 		 * @param {string|number|object} construct either the keyword of the
1152 		 *                               construct, or the ID of the construct
1153 		 *                               or an object with the following
1154 		 *                               properties
1155 		 *                               <ul>
1156 		 *                                <li><i>keyword</i> keyword of the construct</li>
1157 		 *                                <li><i>constructId</i> ID of the construct</li>
1158 		 *                                <li><i>magicValue</i> magic value to be filled into the tag</li>
1159 		 *                                <li><i>sourcePageId</i> source page id</li>
1160 		 *                                <li><i>sourceTagname</i> source tag name</li>
1161 		 *                               </ul>
1162 		 * @param {function(TagAPI)=} success Optional callback that will
1163 		 *                                    receive the newly created tag as
1164 		 *                                    its only argument.
1165 		 * @param {function(GCNError):boolean=} error Optional custom error
1166 		 *                                            handler.
1167 		 * @return {TagAPI} The newly created tag.
1168 		 * @throws INVALID_ARGUMENTS
1169 		 */
1170 		'!createTag': function () {
1171 			return this._createTag.apply(this, arguments);
1172 		},
1173 
1174 		/**
1175 		 * Deletes the specified tag from this page.
1176 		 * You should pass a keyword here not an Id.
1177 		 * 
1178 		 * Note: Due to how the underlying RestAPI layer works,
1179 		 * the success callback will also be called if the specified tag
1180 		 * does not exist.
1181 		 * 
1182 		 * @public
1183 		 * @function
1184 		 * @memberOf PageAPI
1185 		 * @param {string}
1186 		 *            keyword The keyword of the tag to be deleted.
1187 		 * @param {function(PageAPI)=}
1188 		 *            success Optional callback that receive this object as its
1189 		 *            only argument.
1190 		 * @param {function(GCNError):boolean=}
1191 		 *            error Optional custom error handler.
1192 		 */
1193 		removeTag: function () {
1194 			this._removeTag.apply(this, arguments);
1195 		},
1196 
1197 		/**
1198 		 * Deletes a set of tags from this page.
1199 		 * 
1200 		 * @public
1201 		 * @function
1202 		 * @memberOf PageAPI
1203 		 * @param {Array.
1204 		 *            <string>} keywords The keywords of the tags to be deleted.
1205 		 * @param {function(PageAPI)=}
1206 		 *            success Optional callback that receive this object as its
1207 		 *            only argument.
1208 		 * @param {function(GCNError):boolean=}
1209 		 *            error Optional custom error handler.
1210 		 */
1211 		removeTags: function () {
1212 			this._removeTags.apply(this, arguments);
1213 		},
1214 
1215 		/**
1216 		 * Takes the page offline.
1217 		 * If instant publishing is enabled, this will take the page offline
1218 		 * immediately. Otherwise it will be taken offline during the next
1219 		 * publish run.
1220 		 *
1221 		 * @public
1222 		 * @function
1223 		 * @memberOf PageAPI
1224 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
1225 		 *                                    page object as the only argument.
1226 		 * @param {function(GCNError):boolean=} error Optional custom error
1227 		 *                                            handler.
1228 		 */
1229 		takeOffline: function (success, error) {
1230 			var page = this;
1231 			page._fulfill(function () {
1232 				page._authAjax({
1233 					url: GCN.settings.BACKEND_PATH + '/rest/' + page._type +
1234 					     '/takeOffline/' + page.id(),
1235 					type: 'POST',
1236 					json: {}, // There needs to be at least empty content
1237 					          // because of a bug in Jersey.
1238 					error: error,
1239 					success: function (response) {
1240 						if (success) {
1241 							page._invoke(success, [page]);
1242 						}
1243 					}
1244 				});
1245 			});
1246 		},
1247 
1248 		/**
1249 		 * Trigger publish process for the page.
1250 		 *
1251 		 * @public
1252 		 * @function
1253 		 * @memberOf PageAPI
1254 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
1255 		 *                                    page object as the only argument.
1256 		 * @param {function(GCNError):boolean=} error Optional custom error
1257 		 *                                            handler.
1258 		 */
1259 		publish: function (success, error) {
1260 			var page = this;
1261 			GCN.pub('page.before-publish', page);
1262 			this._fulfill(function () {
1263 				page._authAjax({
1264 					url: GCN.settings.BACKEND_PATH + '/rest/' + page._type +
1265 					     '/publish/' + page.id() + GCN._getChannelParameter(page),
1266 					type: 'POST',
1267 					json: {}, // There needs to be at least empty content
1268 					          // because of a bug in Jersey.
1269 					success: function (response) {
1270 						page._data.status = STATUS.PUBLISHED;
1271 						if (success) {
1272 							page._invoke(success, [page]);
1273 						}
1274 					},
1275 					error: error
1276 				});
1277 			});
1278 		},
1279 
1280 		/**
1281 		 * Renders a preview of the current page.
1282 		 * 
1283 		 * @public
1284 		 * @function
1285 		 * @memberOf PageAPI
1286 		 * @param {function(string,
1287 		 *            PageAPI)} success Callback to receive the rendered page
1288 		 *            preview as the first argument, and this page object as the
1289 		 *            second.
1290 		 * @param {function(GCNError):boolean=}
1291 		 *            error Optional custom error handler.
1292 		 */
1293 		preview: function (success, error) {
1294 			var that = this;
1295 
1296 			this._read(function () {
1297 				that._authAjax({
1298 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
1299 					     '/preview/' + GCN._getChannelParameter(that),
1300 					json: {
1301 						page: that._data, // @FIXME Shouldn't this a be merge of
1302 						                 //        the `_shadow' object and the
1303 										 //        `_data'.
1304 						nodeId: that.nodeId()
1305 					},
1306 					type: 'POST',
1307 					error: error,
1308 					success: function (response) {
1309 						if (success) {
1310 							GCN._handleContentRendered(response.preview, that,
1311 								function (html) {
1312 									that._invoke(success, [html, that]);
1313 								});
1314 						}
1315 					}
1316 				});
1317 			}, error);
1318 		},
1319 
1320 		/**
1321 		 * Unlocks the page when finishing editing
1322 		 * 
1323 		 * @public
1324 		 * @function
1325 		 * @memberOf PageAPI
1326 		 * @param {funtion(PageAPI)=}
1327 		 *            success Optional callback to receive this page object as
1328 		 *            the only argument.
1329 		 * @param {function(GCNError):boolean=}
1330 		 *            error Optional custom error handler.
1331 		 */
1332 		unlock: function (success, error) {
1333 			var that = this;
1334 			this._fulfill(function () {
1335 				that._authAjax({
1336 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
1337 					     '/cancel/' + that.id() + GCN._getChannelParameter(that),
1338 					type: 'POST',
1339 					json: {}, // There needs to be at least empty content
1340 					          // because of a bug in Jersey.
1341 					error: error,
1342 					success: function (response) {
1343 						if (success) {
1344 							that._invoke(success, [that]);
1345 						}
1346 					}
1347 				});
1348 			});
1349 		},
1350 
1351 		/**
1352 		 * @see GCN.ContentObjectAPI._processResponse
1353 		 */
1354 		'!_processResponse': function (data) {
1355 			this._data = jQuery.extend(true, {}, data[this._type], this._data);
1356 
1357 			// if data contains page variants turn them into page objects
1358 			if (this._data.pageVariants) {
1359 				var pagevars = [];
1360 				var i;
1361 				for (i = 0; i < this._data.pageVariants.length; i++) {
1362 					pagevars.push(this._continue(GCN.PageAPI,
1363 						this._data.pageVariants[i]));
1364 				}
1365 				this._data.pageVariants = pagevars;
1366 			}
1367 		},
1368 
1369 		/**
1370 		 * @override
1371 		 */
1372 		'!_removeAssociatedTagData': function (tagid) {
1373 			var block;
1374 			for (block in this._blocks) {
1375 				if (this._blocks.hasOwnProperty(block) &&
1376 						this._blocks[block].tagname === tagid) {
1377 					delete this._blocks[block];
1378 				}
1379 			}
1380 
1381 			var editable, containedBlocks, i;
1382 			for (editable in this._editables) {
1383 				if (this._editables.hasOwnProperty(editable)) {
1384 					if (this._editables[editable].tagname === tagid) {
1385 						delete this._editables[editable];
1386 					} else {
1387 						containedBlocks = this._editables[editable]._gcnContainedBlocks;
1388 						if (jQuery.isArray(containedBlocks)) {
1389 							for (i = containedBlocks.length -1; i >= 0; i--) {
1390 								if (containedBlocks[i].tagname === tagid) {
1391 									containedBlocks.splice(i, 1);
1392 								}
1393 							}
1394 						}
1395 					}
1396 				}
1397 			}
1398 		}
1399 
1400 	});
1401 
1402 	/**
1403 	 * Creates a new instance of PageAPI.
1404 	 * See the {@link PageAPI} constructor for detailed information.
1405 	 * 
1406 	 * @function
1407 	 * @name page
1408 	 * @memberOf GCN
1409 	 * @see PageAPI
1410 	 */
1411 	GCN.page = GCN.exposeAPI(PageAPI);
1412 	GCN.PageAPI = PageAPI;
1413 
1414 	GCN.PageAPI.trackRenderedTags = trackRenderedTags;
1415 
1416 }(GCN));
1417