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