text: /*!
   * Copyright 2013 Apereo Foundation (AF) Licensed under the
   * Educational Community License, Version 2.0 (the "License"); you may
   * not use this file except in compliance with the License. You may
   * obtain a copy of the License at
   *
   *     http://opensource.org/licenses/ECL-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an "AS IS"
   * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
   * or implied. See the License for the specific language governing
   * permissions and limitations under the License.
   */
  
  var _ = require('underscore');
  var events = require('events');
  var fs = require('fs');
  var mime = require('mime');
  var Path = require('path');
  var ShortId = require('shortid');
  var util = require('util');
  
  var AuthzAPI = require('oae-authz');
  var AuthzUtil = require('oae-authz/lib/util');
  var Config = require('oae-config').config('oae-content');
  var Context = require('oae-context').Context;
  var LibraryAPI = require('oae-library');
  var log = require('oae-logger').logger('oae-content');
  var MessageBoxAPI = require('oae-messagebox');
  var MessageBoxConstants = require('oae-messagebox/lib/constants').MessageBoxConstants;
  var OaeUtil = require('oae-util/lib/util');
  var PrincipalsDAO = require('oae-principals/lib/internal/dao');
  var PrincipalsUtil = require('oae-principals/lib/util');
  var Signature = require('oae-util/lib/signature');
  var TenantsUtil = require('oae-tenants/lib/util');
  var Validator = require('oae-util/lib/validator').Validator;
  
  var Content = require('./model').Content;
  var ContentConstants = require('./constants').ContentConstants;
  var ContentDAO = require('./internal/dao');
  var ContentUtil = require('./internal/util');
  var Etherpad = require('./internal/etherpad');
  var Revision = require('./model').Revision;
  
  // Library index for the content module, for "content" libraries
  var LIBRARY_INDEX_NAME = 'content:content';
  
  var _backends = {};
  
  /**
   * ### Events
   *
   * The `ContentAPI`, as enumerated in `ContentConstants.events`, emits the following events:
   *
   * * `createdContent(ctx, content)`: A new content item was created. The `ctx` and the `content` object that was created are both provided.
   * * `updatedContent(ctx, newContentObj, oldContentObj)`: A content item was updated. The `ctx`, the updated content object and the content before was updated are provided.
   * * `updatedContentBody(ctx, content, fileInfo)`: A content item's file body was updated. The `ctx` of the request, the `content` object that was updated and the `fileInfo` of the new file location are all provided.
   * * `restoredContent(ctx, newContentObj, oldContentObj, restoredRevision)`: An older revision for a content item has been restored.
   * * `deletedContent(ctx, contentId)`: A content item was deleted. The `ctx` and the `contentId` of the deleted item are provided.
   * * `updatedContentMembers(ctx, content, memberUpdates)`: A content's members list was updated. The `ctx`, full `content` object of the updated content, and the hash of principalId -> role that outlines the changes that were made are provided.
   * * `createdComment(ctx, comment, content)`: A new comment was posted for a content item. The `ctx`, `comment` and commented `content` object are provided.
   */
  var ContentAPI = module.exports = new events.EventEmitter();
  
  ///////////////////////////////////
  // Retrieving a piece of content //
  ///////////////////////////////////
  
  /**
   * Get a content's basic profile information based on a pooled content id.
   * An access-check will be run to determine if the user can view this piece of content.
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId           The id of the content object we want to retrieve
   * @param  {Function}       callback            Standard callback function
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Content}        callback.contentObj Retrieved content object
   */
  var getContent = module.exports.getContent = function(ctx, contentId, callback) {
      // Parameter validation
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      ContentDAO.Content.getContent(contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          _hasAccess(ctx, contentObj, function(err, hasAccess) {
              if (err) {
                  return callback(err);
              } else if (!hasAccess) {
                  return callback({'code': 401, 'msg': 'You don\'t have access to this piece of content'});
              }
  
              _augmentContent(ctx, contentObj);
              callback(null, contentObj);
          });
      });
  };
  
  /**
   * Get a content item's full content profile. Next to the basic content profile, this will include the created date,
   * the profile of the user who originally created the content, and a isManager property specifying whether or not
   * the current user can manage the content.
   *
   * @param  {Context}        ctx                         Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId                   The id of the content profile we want to retrieve
   * @param  {Function}       callback                    Standard callback function
   * @param  {Object}         callback.err                Error object containing the error message
   * @param  {Content}        callback.contentProfile     Retrieved content profile
   */
  var getFullContentProfile = module.exports.getFullContentProfile = function(ctx, contentId, callback) {
      // Parameter validation
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          // Check whether the user is a manager.
          _canManage(ctx, contentObj, function(err, isManager) {
              if (err) {
                  return callback(err);
              }
  
              _getFullContentProfile(ctx, contentObj, isManager, callback);
          });
      });
  };
  
  /**
   * Adds the `isManager` flag, `createdBy` user object and the `latestRevision` in case it's a collaborative document.
   *
   * @param  {Context}    ctx                         Standard context object, representing the currently logged in user and its tenant
   * @param  {Content}    contentObj                  The content object to add the extra profile information on.
   * @param  {Boolean}    isManager                   Whether or not the current user is a manager of the piece of content.
   * @param  {Function}   callback                    Standard callback function
   * @param  {Object}     callback.err                Error object containing the error message
   * @param  {Content}    callback.contentProfile     Retrieved content profile
   */
  var _getFullContentProfile = function(ctx, contentObj, isManager, callback) {
      // Store the isManager property.
      contentObj.isManager = isManager;
  
      // Get the user object for the createdBy property
      PrincipalsUtil.getPrincipal(ctx, contentObj.createdBy, function(err, createdBy) {
          if (err) {
              return callback(err);
          }
          contentObj.createdBy = createdBy;
  
          // If the content item is a collaborative document, add the revision data.
          if (contentObj.resourceSubType === 'collabdoc') {
              _getRevision(ctx, contentObj, contentObj.latestRevisionId, function(err, revision) {
                  if (err) {
                      return callback(err);
                  }
                  contentObj.latestRevision = revision;
                  callback(null, contentObj);
              });
          } else {
              callback(null, contentObj);
          }
      });
  };
  
  /////////////////////////////////////
  // Creating a new piece of content //
  /////////////////////////////////////
  
  /**
   * Create a link as a pooled content item
   *
   * @param  {Context}        ctx                Standard context object containing the current user and the current tenant
   * @param  {String}         displayName        The display name for the link
   * @param  {String}         description        The description of the link [optional]
   * @param  {String}         visibility         The visibility setting for the piece of content. Possible values are "public", "loggedin" and "private" [optional]
   * @param  {String}         link               The URL for the link
   * @param  {Object}         additionalMembers  Object where the keys represent principal ids that need to be added to the content upon creation and the values represent the role that principal will have. Possible values are "viewer" and "manager"
   * @param  {Function}       callback           Standard callback function takes arguments `err` and `content`
   * @param  {Object}         callback.err       Error object containing the error message
   * @param  {Object}         callback.content   JSON object containing the pool id of the created content
   */
  var createLink = module.exports.createLink = function(ctx, displayName, description, visibility, link, additionalMembers, callback) {
      callback = callback || function() {};
  
      // Setting content to default if no visibility setting is provided
      visibility = visibility || Config.getValue(ctx.tenant().alias, 'visibility', 'links');
  
      // Check if the link property is present. All other validation will be done in the _createContent function
      var validator = new Validator();
      validator.check(link, {'code': 400, 'msg': 'A valid link must be provided'}).isUrl();
      validator.check(null, {'code': 401, 'msg': 'You have to be logged in to be able to create a content item'}).isLoggedInUser(ctx);
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      var contentId = _generateContentId(ctx.tenant().alias);
      var revisionId = _generateRevisionId(contentId);
      _createContent(ctx, contentId, revisionId, 'link', displayName, description, visibility, additionalMembers, {'link': link}, {}, function(err, content, revision) {
          if (err) {
              return callback(err);
          }
  
          ContentAPI.emit(ContentConstants.events.CREATED_CONTENT, ctx, content, revision, additionalMembers);
          callback(err, content);
      });
  };
  
  /**
   * Create a file as a pooled content item
   *
   * @param  {Context}   ctx                Standard context object containing the current user and the current tenant
   * @param  {String}    displayName        The display name for the file
   * @param  {String}    description        The description of the file [optional]
   * @param  {String}    visibility         The visibility setting for the piece of content. Possible values are "public", "loggedin" and "private" [optional]
   * @param  {Object}    additionalMembers  Object where the keys represent principal ids that need to be added to the content upon creation and the values represent the role that principal will have. Possible values are "viewer" and "manager"
   * @param  {Object}    file               A file object as returned by express
   * @param  {Function}  callback           Standard callback function takes arguments `err` and `content`
   * @param  {Object}    callback.err       Error object containing the error message
   * @param  {Content}   callback.content   JSON object containing the pool id of the created content
   */
  var createFile = module.exports.createFile = function(ctx, displayName, description, visibility, additionalMembers, file, callback) {
      // Wrap the callback function into a function that cleans up the file in case something went wrong
      var cleanUpCallback = _getCleanUpCallback({'file': file}, callback);
  
      // Try to create the file.
      _createFile(ctx, displayName, description, visibility, additionalMembers, file, cleanUpCallback);
  };
  
  /**
   * Returns a function that will call the provided callback function.
   * In case the returned function gets called with an error object, the passed in file object will
   * be removed from the file system.
   *
   * @param  {Object}    files                The ExpressJS files object.
   * @param  {Function}  callback             Standard callback function takes arguments `err` and `content`
   * @param  {Object}    callback.err         Error object containing the error message
   * @param  {Content}   callback.content     JSON object containing the pool id of the created content
   * @return {Function}                       A function that removes the file on disk in case something went wrong.
   * @api private
   */
  var _getCleanUpCallback = function(files, callback) {
      return function() {
          // Remember the arguments so we can pass them to the callback later.
          var callbackArguments = arguments;
  
          // The first argument is always the error object.
          var err = callbackArguments[0];
  
          if (err && files) {
              // Something went wrong with a request that has uploaded files associated to it.
              // In that case we try to remove the files.
              var fileObjects = _.values(files);
              _cleanupUploadedFiles(fileObjects, function() {
                  callback.apply(this, callbackArguments);
              });
          } else {
              // If we get here, the request might have failed, but it didn't contain an uploaded file.
              callback.apply(this, callbackArguments);
          }
      };
  };
  
  /**
   * Recursively iterates trough an array of uploaded files and removes them.
   *
   * @param  {Object[]}   files       An array of ExpressJS file objects
   * @param  {Function}   callback    Standard callback method
   * @api private
   */
  var _cleanupUploadedFiles = function(files, callback) {
      if (files.length === 0) {
          return callback();
      }
  
      var file = files.pop();
      if (file && file.path) {
          fs.exists(file.path, function(exists) {
              if (exists) {
                  fs.unlink(file.path, function(unlinkErr) {
                      if (unlinkErr) {
                          log().warn({'err': unlinkErr, 'file': file}, 'Could not remove the uploaded file.');
                      }
                      _cleanupUploadedFiles(files, callback);
                  });
              } else {
                  _cleanupUploadedFiles(files, callback);
              }
          });
      } else {
          _cleanupUploadedFiles(files, callback);
      }
  };
  
  /**
   * Create a file as a pooled content item
   *
   * @param  {Context}   ctx                Standard context object containing the current user and the current tenant
   * @param  {String}    displayName        The display name for the file
   * @param  {String}    [description]      The description of the file
   * @param  {String}    visibility         The visibility setting for the piece of content. Possible values are "public", "loggedin" and "private" [optional]
   * @param  {Object}    additionalMembers  Object where the keys represent principal ids that need to be added to the content upon creation and the values represent the role that principal will have. Possible values are "viewer" and "manager"
   * @param  {Object}    file               A file object as returned by express.
   * @param  {Function}  callback           Standard callback function takes arguments `err` and `content`
   * @param  {Object}    callback.err       Error object containing the error message
   * @param  {Content}   callback.content   JSON object containing the pool id of the created content
   * @api private
   */
  var _createFile = function(ctx, displayName, description, visibility, additionalMembers, file, callback) {
      callback = callback || function() {};
  
      // Setting content to default if no visibility setting is provided
      visibility = visibility || Config.getValue(ctx.tenant().alias, 'visibility', 'files');
  
      var validator = new Validator();
      validator.check(null, {'code': 401, 'msg': 'Anonymous users are not allowed to upload files'}).isLoggedInUser(ctx);
      validator.check(file, {'code': 400, 'msg': 'Missing file parameter'}).notNull();
      validator.check(displayName, {'code': 400, 'msg': 'A displayName must be provided'}).notEmpty();
      validator.check(displayName, {'code': 400, 'msg': 'A displayName can only be 1000 characters long'}).isShortString();
      if (description) {
          validator.check(description, {'code': 400, 'msg': 'A content description can only be 10000 characters long'}).isMediumString();
      }
      if (file) {
          validator.check(file.size, {'code': 400, 'msg': 'Missing size on the file object'}).notEmpty();
          validator.check(file.size, {'code': 400, 'msg': 'Invalid size on the file object'}).isInt();
          validator.check(file.size, {'code': 400, 'msg': 'Invalid size on the file object'}).min(0);
          validator.check(file.name, {'code': 400, 'msg': 'Missing name on the file object'}).notEmpty();
      }
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Generate a content ID that can be used when storing the file.
      var contentId = _generateContentId(ctx.tenant().alias);
      var revisionId = _generateRevisionId(contentId);
  
      // Detect the mimetype of the file using the file extension, as the one that Express gives us is pulled
      // from the HTTP request. This makes it an untrustworthy piece of information as some browsers are
      // notoriously bad at providing the correct mimetype and it can be spoofed. If the mimetype cannot
      // be determined, the mime utility falls back to application/octet-stream.
      file.type = mime.lookup(file.name);
  
      // We store the uploaded file in a location identified by the content id, then further identified by the revision id
      var options = {'resourceId': contentId, 'prefix': revisionId};
      getStorageBackend(ctx).store(ctx, file, options, function(err, uri) {
          if (err) {
              return callback(err);
          }
  
          // Create the content and revision object.
          var otherValues = {
              'mime': file.type,
              'size': file.size,
              'filename': file.name
          };
          var revisionData = _.extend({}, otherValues, {'uri': uri});
          _createContent(ctx, contentId, revisionId, 'file', displayName, description, visibility, additionalMembers, otherValues, revisionData, function(err, contentObj, revision) {
              if (err) {
                  return callback(err);
              }
  
              contentObj.filename = file.name;
              contentObj.size = file.size;
              contentObj.mime = file.type;
  
              ContentAPI.emit(ContentConstants.events.CREATED_CONTENT, ctx, contentObj, revision, additionalMembers);
              callback(null, contentObj);
          });
      });
  };
  
  /////////////////////////////
  // Collaborative documents //
  /////////////////////////////
  
  /**
   * Create a collaborative document as a pooled content item
   *
   * @param  {Context}   ctx                Standard context object containing the current user and the current tenant
   * @param  {String}    displayName        The display name for the collaborative document
   * @param  {String}    [description]      The description for the collaborative document
   * @param  {String}    visibility         The visibility setting for the piece of content. Possible values are "public", "loggedin" and "private" [optional]
   * @param  {Object}    additionalMembers  Object where the keys represent principal ids that need to be added to the content upon creation and the values represent the role that principal will have. Possible values are "viewer" and "manager"
   * @param  {Function}  callback           Standard callback function takes arguments `err` and `content`
   * @param  {Object}    callback.err       Error object containing the error message
   * @param  {Content}   callback.content   JSON object containing the pool id of the created content
   */
  var createCollabDoc = module.exports.createCollabDoc = function(ctx, displayName, description, visibility, additionalMembers, callback) {
      callback = callback || function() {};
  
      // Setting content to default if no visibility setting is provided
      visibility = visibility || Config.getValue(ctx.tenant().alias, 'visibility', 'collabdocs');
  
      var contentId = _generateContentId(ctx.tenant().alias);
      var revisionId = _generateRevisionId(contentId);
      Etherpad.createPad(contentId, function(err, ids) {
          if (err) {
              return callback(err);
          }
          _createContent(ctx, contentId, revisionId, 'collabdoc', displayName, description, visibility, additionalMembers, ids, {}, function(err, content, revision) {
              if (err) {
                  return callback(err);
              }
  
              content.etherpadPadId = ids.etherpadPadId;
              content.etherpadGroupId = ids.etherpadGroupId;
  
              ContentAPI.emit(ContentConstants.events.CREATED_CONTENT, ctx, content, revision, additionalMembers);
              callback(null, content);
          });
      });
  };
  
  /**
   * Publishes a collaborative document.
   * When a user publishes a document the following happens:
   *     -  The HTML for this pad is retrieved.
   *     -  A new revision is created with this HTML.
   *     -  The content object is updated with this HTML.
   *     -  The content item gets bumped to the top of all the libraries it resides in.
   *     -  An `updatedContent` event is fired so activities and PP images can be generated.
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId           The id of the collaborative document that needs to be published.
   * @param  {Function}       callback            Standard callback method.
   * @param  {Object}         callback.err        Standard error object (if any.)
   * @param  {Content}        callback.content    The new content object.
   */
  var publishCollabDoc = module.exports.publishCollabDoc = function(ctx, contentId, callback) {
      // Check if we have access to this piece of content.
      canManage(ctx, contentId, function(err, canManage, contentObj) {
          if (err) {
              return callback(err);
          } else if (!canManage) {
              return callback({'code': 401, 'msg': 'Only managers can publish collaborative documents'});
          } else if (contentObj.resourceSubType !== 'collabdoc') {
              return callback({'code': 400, 'msg': 'This is not a collaborative document'});
          }
  
          // Get the latest html from etherpad.
          Etherpad.getHTML(contentObj.id, contentObj.etherpadPadId, function(err, html) {
              if (err) {
                  return callback(err);
              }
  
              // Get our latest revision and compare the html on there to the one in Etherpad.
              // We only need to create a new revision if there is an actual update.
              _getRevision(ctx, contentObj, contentObj.latestRevisionId, function(err, revision) {
                  if (err) {
                      return callback(err);
                  } else if (revision.etherpadHtml === html || (!revision.etherpadHtml && html === '<br>')) {
                      // If there is no real update or this is a brand new doc (no etherpadHtml on the revision)
                      // and the user didn't enter anything, we can return early.
                      // No need to create a revision/activity for a non-update.
                      contentObj.latestRevision = revision;
                      return callback(null, contentObj);
                  }
  
                  // Otherwise we create a new revision.
                  var newRevisionId = _generateRevisionId(contentId);
                  ContentDAO.Revisions.createRevision(newRevisionId, contentId, ctx.user().id, {'etherpadHtml': html}, function(err, revision) {
                      if (err) {
                          log().error({'err': err, 'contentId': contentId}, 'Could not create a revision for this collaborative document.');
                          return callback({'code': 500, 'msg': 'Could not create a revision for this collaborative document.'});
                      }
  
                      // Update the content so we can easily retrieve it.
                      // This will also bump the collab doc to the top of the library lists.
                      ContentDAO.Content.updateContent(contentObj, {'latestRevisionId': revision.revisionId}, true, function(err, newContentObj) {
                          if (err) {
                              log().error({'err': err, 'contentId': contentId}, 'Could not update the main Content CF this collaborative document.');
                              return callback({'code': 500, 'msg': 'Could not update this collaborative document.'});
                          }
  
                          // Add the revision on the content object so the UI doesn't have to
                          // do another request to get the HTML.
                          newContentObj.latestRevision = revision;
  
                          // Emit an event for activities and preview processing
                          ContentAPI.emit(ContentConstants.events.UPDATED_CONTENT_BODY, ctx, newContentObj, revision);
                          callback(null, newContentObj);
                      });
                  });
              });
          });
      });
  };
  
  /**
   * Lets the user join a collaborative etherpad pad.
   * Only users who have manager permissions on the collaborative document can join the pad.
   *
   * @param  {Context}    ctx             Standard context object containing the current user and the current tenant
   * @param  {String}     contentId       The ID of the collaborative document that should be joined.
   * @param  {Function}   callback        Standard callback function takes arguments `err` and `content`
   * @param  {Object}     callback.err    Error object containing the error message
   * @param  {Content}    callback.url    JSON object containing the url where the pad is accessible.
   */
  var joinCollabDoc = module.exports.joinCollabDoc = function(ctx, contentId, callback) {
      // Check if we have access to this piece of content.
      canManage(ctx, contentId, function(err, canManage, contentObj) {
          if (err) {
              return callback(err);
          } else if (!canManage) {
              return callback({'code': 401, 'msg': 'You need to be a manager of this piece of content to be able to join it'});
          } else if (contentObj.resourceSubType !== 'collabdoc') {
              return callback({'code': 400, 'msg': 'This is not a collaborative document'});
          }
  
          // Join the pad
          Etherpad.joinPad(ctx, contentObj.id, contentObj.etherpadPadId, contentObj.etherpadGroupId, callback);
      });
  };
  
  /**
   * Create a new piece of pooled content
   *
   * @param  {Context}        ctx                Standard context object containing the current user and the current tenant
   * @param  {String}         contentId          The ID of the content that should be created
   * @param  {Strign}         revisionId         The ID of the revision that should be created for the content
   * @param  {String}         resourceSubType    The content type. Possible values are "file", "collabdoc" and "link"
   * @param  {String}         displayName        The display name for the piece of content
   * @param  {String}         description        The description of the piece of content [optional]
   * @param  {String}         visibility         The visibility setting for the piece of content. Possible values are "public", "loggedin" and "private" [optional]
   * @param  {Object}         additionalMembers  Object where the keys represent principal ids that need to be added to the content upon creation and the values represent the role that principal will have. Possible values are "viewer" and "manager"
   * @param  {Object}         otherValues        JSON object where the keys represent other metadata values that need to be stored, and the values represent the metadata values
   * @param  {Object}         revisionData        JSON object where the keys represent revision columns that need to be stored, and the values represent the revision values
   * @param  {Function}       callback           Standard callback function takes arguments `err` and `content`
   * @param  {Object}         callback.err       Error object containing the error message
   * @param  {Content}        callback.content   JSON object containing the pool id of the created content
   * @param  {Revision}       callback.revision  The created revision object
   * @api private
   */
  var _createContent = function(ctx, contentId, revisionId, resourceSubType, displayName, description, visibility, additionalMembers, otherValues, revisionData, callback) {
      callback = callback || function() {};
  
      // Use an empty description if no description has been provided
      description = description || '';
      // Make sure the otherValues and additionalMembers are valid objects
      additionalMembers = additionalMembers || {};
      otherValues = otherValues || {};
  
      // Parameter validation
      var validator = new Validator();
      validator.check(displayName, {'code': 400, 'msg': 'A displayName must be provided'}).notEmpty();
      validator.check(displayName, {'code': 400, 'msg': 'A displayName can only be 1000 characters long'}).isShortString();
      if (description) {
          validator.check(description, {'code': 400, 'msg': 'A description can only be 10000 characters long'}).isMediumString();
      }
      validator.check(contentId, {'code': 400, 'msg': 'A content ID must be provided'}).isResourceId();
      validator.check(visibility, {'code': 400, 'msg': 'An invalid content visibility option has been provided. This can be "private", "loggedin" or "public"'}).isIn(_.values(ContentConstants.visibility));
      validator.check(resourceSubType, {'code': 400, 'msg': 'A valid resourceSubType must be provided. This can be "file", "collabdoc" or "link"'}).isIn(ContentConstants.resourceSubTypes);
      validator.check(null, {'code': 401, 'msg': 'You have to be logged in to be able to create a content item'}).isLoggedInUser(ctx);
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      ContentDAO.Content.createContent(contentId, revisionId, ctx.user().id, resourceSubType, displayName, description, visibility, additionalMembers, otherValues, revisionData, function(err, contentObj, revision) {
          if (err) {
              return callback(err);
          }
  
          // Now make the current user a manager of the content
          additionalMembers[ctx.user().id] = ContentConstants.roles.MANAGER;
          _setContentPermissions(ctx, contentObj, additionalMembers, function(err, contentObj) {
              if (err) {
                  return callback(err);
              }
  
              callback(null, contentObj, revision);
          });
      });
  };
  
  /////////////////////////////////
  // Removing a piece of content //
  /////////////////////////////////
  
  /**
   * Delete a piece of content from the system
   *
   * @param  {Context}   ctx               Standard context object containing the current user and the current tenant
   * @param  {String}    contentId         The content id of the content that is being deleted
   * @param  {Function}  callback          Standard callback function takes argument `err`
   * @param  {Object}    callback.err      Error object containing the error message
   */
  var deleteContent = module.exports.deleteContent = function(ctx, contentId, callback) {
      callback = callback || function() {};
  
      // Parameter validation
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      validator.check(null, {'code': 401, 'msg': 'You have to be logged in to be able to delete a content item'}).isLoggedInUser(ctx);
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Fist check whether or not the current user is a manager of the piece of content
      canManage(ctx, contentId, function(err, canManage, contentObj) {
          if (err) {
              return callback(err);
          } else if (!canManage) {
              return callback({'code': 401, 'msg': 'You are not allowed to manage this piece of content'});
          }
  
          // Delete the content
          ContentDAO.Content.deleteContent(contentObj, function(err) {
              if (err) {
                  return callback(err);
              }
              ContentAPI.emit(ContentConstants.events.DELETED_CONTENT, ctx, contentId);
              callback();
          });
      });
  };
  
  ////////////////////////////
  // Content access control //
  ////////////////////////////
  
  /**
   * Share a piece of content with a set of principals. This only be possible when the current user is a manager of the content, or if the current
   * user is logged in and the content item is public or visible to logged in users only. In case that the content is shared with principals that
   * are already content members, no updates to the existing role of those principals will be made
   *
   * @param  {Context}   ctx               Standard context object containing the current user and the current tenant
   * @param  {String}    contentId         The content id of the content that is being shared
   * @param  {String[]}  principalIds      Array of principal ids with whom the content will be shared. By default, they will all be made members.
   * @param  {Function}  callback          Standard callback function takes arguments `err` and `content`
   * @param  {Object}    callback.err      Error object containing the error message
   */
  var shareContent = module.exports.shareContent = function(ctx, contentId, principalIds, callback) {
      callback = callback || function() {};
  
      // Parameter validation
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      validator.check(principalIds.length, {'code': 400, 'msg': 'The content must at least be shared with 1 user or group'}).min(1);
      validator.check(null, {'code': 401, 'msg': 'You have to be logged in to be able to share content'}).isLoggedInUser(ctx);
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
  
      // Check if it exists.
      ContentDAO.Content.getContent(contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          // Take out the principals who already have a role on this content item
          AuthzAPI.getDirectRoles(principalIds, contentObj.id, function(err, roles) {
              if (err) {
                  return callback(err);
              }
  
              principalIds = _.difference(principalIds, _.keys(roles));
  
              // Check if the user can share this piece of content.
              _canShareContent(ctx, contentObj, principalIds, function(err, canShare, illegalPrincipalIds) {
                  if (err) {
                      return callback(err);
                  } else if (illegalPrincipalIds) {
                      return callback({'code': 400, 'msg': 'One or more target members are not authorized to become viewers on this piece of content'});
                  } else if (!canShare) {
                      return callback({'code': 401, 'msg': 'You are not allowed to share this content'});
                  }
  
                  _shareContent(ctx, contentObj, principalIds, callback);
              });
          });
      });
  };
  
  /**
   * Checks if a piece of content can be shared.
   *
   * @param  {Context}    ctx                             The context of the current request
   * @param  {Content}    contentObj                      The content to test for access
   * @param  {String[]}   principalIds                    The principalIds with which the user wishes to share the discussion
   * @param  {Function}   callback                        Invoked when the process completes
   * @param  {Object}     callback.err                    An error that occurred, if any
   * @param  {Boolean}    callback.canShare               `true` if the u ser in context is allowed to perform this share operation. `false` otherwise
   * @param  {String[]}   [callback.illegalPrincipalIds]  If the check failed because of a tenant boundary violation, this will be an array of principalIds that were in violation
   * @api private
   */
  var _canShareContent = function(ctx, contentObj, principalIds, callback) {
      var user = ctx.user();
  
      // Anonymous users can never share
      if (!user) {
          return callback(null, false);
      }
  
      // Verify tenant boundaries
      AuthzAPI.canInteract(ctx, contentObj.tenant.alias, principalIds, function(err, canInteract, illegalPrincipalIds) {
          if (err) {
              return callback(err);
          } else if (!canInteract) {
              return callback(null, false, illegalPrincipalIds);
          }
  
          // Check if we have access on the content file.
          AuthzAPI.resolveImplicitRole(ctx, contentObj.tenant.alias, contentObj.visibility, ContentConstants.roles.ALL_PRIORITY, function(err, implicitRole, canInteract) {
              if (implicitRole === ContentConstants.roles.MANAGER) {
                  // Managers can always share
                  return callback(null, true);
              } else if (canInteract) {
                  // If we can interact with the item, we can always share it
                  return callback(null, true);
              }
  
              // If the content is private, only managers can share it
              if (contentObj.visibility === ContentConstants.visibility.PRIVATE) {
                  return AuthzAPI.hasRole(user.id, contentObj.id, ContentConstants.roles.MANAGER, function(err, hasRole) {
                      return callback(err, hasRole);
                  });
              }
  
              // At this point, we have to see if the user has any explicit role on the resource to see if they can share it
              AuthzAPI.hasAnyRole(user.id, contentObj.id, function(err, hasAnyRole) {
                  return callback(err, hasAnyRole);
              });
          });
      });
  };
  
  /**
   * Internal function used to share a piece of content with a set of principals. This function assume that the current user is allowed
   * to see and share the content. If some of the principals passed into this function are already content members, no updates to the
   * existing role of those principals will be made.
   *
   * @param  {Context}   ctx               The current execution context.
   * @param  {Content}   contentObj        Content object representing the content that is being shared
   * @param  {String[]}  principalIds      Array of principal ids with whom the content will be shared. By default, they will all be made members.
   * @param  {Function}  callback          Standard callback function takes arguments `err` and `content`
   * @param  {Object}    callback.err      Error object containing the error message
   * @api private
   */
  var _shareContent = function(ctx, contentObj, principalIds, callback) {
      var roleChanges = {};
      _.each(principalIds, function(principalId) {
          roleChanges[principalId] = ContentConstants.roles.VIEWER;
      });
  
      _setContentPermissions(ctx, contentObj, roleChanges, function(err) {
          if (err) {
              return callback(err);
          }
  
          ContentAPI.emit(ContentConstants.events.UPDATED_CONTENT_MEMBERS, ctx, contentObj, roleChanges);
          return callback();
      });
  };
  
  /**
   * Check whether or not the current user can manage a piece of content
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId           The id of the content object we want to check
   * @param  {Function}       callback            Standard callback function takes arguments `err`, `canManage`, and `content`
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Boolean}        callback.canManage  Whether or not the user can manage the content
   * @param  {Content}        callback.content    The retrieved content object containing its basic profile
   */
  var canManage = module.exports.canManage = function(ctx, contentId, callback) {
      // Parameter validation
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      validator.check(null, {'code': 401, 'msg': 'You have to be logged in to be able to manage content'}).isLoggedInUser(ctx);
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              if (err.code === 401) {
                  return callback(null, false);
              } else {
                  return callback(err);
              }
          }
          _canManage(ctx, contentObj, function(err, canManage) {
              if (err) {
                  return callback(err);
              }
              return callback(null, canManage, contentObj);
          });
      });
  };
  
  /**
   * Internal function that checks whether or not the current user can manage a piece of content. This function assumes that
   * the piece of content passed in actually exists
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {Content}        contentObj          The content object we want to check
   * @param  {Function}       callback            Standard callback function takes arguments `err` and `canManage`
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Boolean}        callback.canManage  Whether or not the user can manage the content
   * @api private
   */
  var _canManage = function(ctx, contentObj, callback) {
      if (!ctx.user()) {
          return callback(null, false);
      } else if (ctx.user().isAdmin(contentObj.tenant.alias)) {
          return callback(null, true);
      }
  
      return AuthzAPI.hasRole(ctx.user().id, contentObj.id, ContentConstants.roles.MANAGER, callback);
  };
  
  /**
   * Check whether or not the current user has access to a piece of content
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId           The id of the content object we want to check
   * @param  {Function}       callback            Standard callback function takes arguments `err` and `hasAccess`
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Boolean}        callback.hasAccess  Whether or not the user has access to the content
   */
  var hasAccess = module.exports.hasAccess = function(ctx, contentId, callback) {
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              // If the user doesn't have access to the content
              if (err.code ===  401) {
                  return callback(null, false);
              } else {
                  return callback(err);
              }
          }
  
          return callback(null, true);
      });
  };
  
  /**
   * Internal function that checks whether or not the current user has access to a piece of content. This function assumes that
   * the piece of content passed in actually exists
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {Content}        contentObj          The content object representing the content item we're checking access for
   * @param  {Function}       callback            Standard callback function takes arguments `err` and `canManage`
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Boolean}        callback.canManage  Whether or not the user has access to the content
   * @api private
   */
  var _hasAccess = function(ctx, contentObj, callback) {
      if (contentObj.visibility === ContentConstants.visibility.PUBLIC) {
          return callback(null, contentObj);
      // If the content is visible to logged in users, I get access if I'm logged in
      } else if (contentObj.visibility === ContentConstants.visibility.LOGGEDIN && TenantsUtil.isLoggedIn(ctx, contentObj.tenant.alias)) {
          return callback(null, contentObj);
      // If the user is admin of the content's tenant, they have access
      } else if (ctx.user() && ctx.user().isAdmin(contentObj.tenant.alias)) {
          return callback(null, contentObj);
      // If the content is private
      } else if (ctx.user()) {
          return AuthzAPI.hasAnyRole(ctx.user().id, contentObj.id, callback);
      }
  
      return callback({'code': 401, 'msg': 'You don\'t have access to this piece of content.'});
  };
  
  /**
   * Update, add or remove the role of a set of principals on a piece of content
   *
   * @param  {Context}         ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}          contentId           The id of the content object we want to set permissions on
   * @param  {Object}          newPermissions      Object where the keys represent the principal ids for which the content permissions should be updated/added/removed. The value is a string representing the new role. If false is passed in, the permissions for that principal will be removed e.g. {'user1': 'manager', 'user2': 'viewer', 'user3': false}
   * @param  {Function}        callback            Standard callback function takes argument `err`
   * @param  {Object}          callback.err        Error object containing the error message
   */
  var setContentPermissions = module.exports.setContentPermissions = function(ctx, contentId, newPermissions, callback) {
      // Parameter validation
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      validator.check(_.keys(newPermissions).length, {'code': 400, 'msg': 'You should specify at least 1 user/group to set content permissions on'}).min(1);
      _.each(newPermissions, function(role, principalId) {
          validator.check(principalId, {'code': 400, 'msg': 'A principalId needs to be specified for a role.'}).isPrincipalId();
          // A role can be set to false if we're removing it.
          if (role !== false) {
              validator.check(role, {'code': 400, 'msg': 'An invalid role has been specified.'}).isIn(ContentConstants.roles.ALL_PRIORITY);
          }
      });
      validator.check(null, {'code': 401, 'msg': 'You have to be logged in to be able to create a content item'}).isLoggedInUser(ctx);
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Check if the content exists
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          // Check if the current user can manage the content
          _canManage(ctx, contentObj, function(err, canManage) {
              if (err) {
                  return callback(err);
              } else if (!canManage) {
                  return callback({'code': 401, 'msg': 'You are not allowed to manage this piece of content'});
              }
  
              // Check that these new content permissions don't result in an entity without managers.
              _checkNewContentPermissions(ctx, contentObj, newPermissions, function(err) {
                  if (err) {
                      return callback(err);
                  }
  
                  // Add the new permissions to the content. This will then update the libraries as well
                  _setContentPermissions(ctx, contentObj, newPermissions, function(err) {
                      if (err) {
                          return callback(err);
                      }
  
                      ContentAPI.emit(ContentConstants.events.UPDATED_CONTENT_MEMBERS, ctx, contentObj, newPermissions);
                      return callback();
                  });
              });
          });
      });
  };
  
  /**
   * Internal method that checks if the new permission set for a piece of content doesn't leave the content item without any managers.
   *
   * @param  {Context}    ctx                     The current execution context
   * @param  {Content}    contentObj              The content object we want to set permissions on
   * @param  {Object}     newPermissions          Object where the keys represent the principal ids for which the content permissions should be updated/added/removed. The value is a string representing the new role. If false is passed in, the permissions for that principal will be removed e.g. {'user1': 'manager', 'user2': 'viewer', 'user3': false}
   * @param  {Function}   callback                Standard callback function takes argument `err`
   * @param  {Object}     callback.err            Error object containing the error message
   * @api private
   */
  var _checkNewContentPermissions = function(ctx, contentObj, newPermissions, callback) {
      // Check that we don't remove all members.
      AuthzAPI.getAuthzMembers(contentObj.id, null, 10000, function(err, members) {
          if (err) {
              return callback(err);
          }
  
          // Get the managers that aren't removed by this update.
          var remainingManagers = _.filter(members, function(member) {
              var isManager = member.role === ContentConstants.roles.MANAGER;
              var isRemoved = newPermissions[member.id] === false;
              var hasRoleChange = (newPermissions[member.id] && newPermissions[member.id] !== ContentConstants.roles.MANAGER);
  
              return (isManager && !isRemoved && !hasRoleChange);
          });
  
          // Get the extra set of managers.
          var newManagers = _.filter(newPermissions, function(role, id) { return role === ContentConstants.roles.MANAGER; });
  
          // Combine them and count them. If the total is 0 that means an attempt has been made to remove all direct managers.
          var managers = remainingManagers.concat(newManagers);
          if (managers.length === 0) {
              return callback({'code': 400, 'msg': 'At least one principal should be a manager.'});
          } else {
              callback();
          }
      });
  };
  
  /**
   * Internal function used to update, add or remove the role of a set of principals on a piece of content. This function will assume that
   * the content object exists and that the current user is a manager of the content. It will update the lastModified date of the content
   * item as well since the content item should appear newly updated in user libraries. It's assumed that the new content permissions
   * don't cause any inconsistencies such as no managers or members left on the piece of content.
   *
   * @param  {Context}         ctx                    The current execution context
   * @param  {Content}         contentObj             The content object we want to set permissions on
   * @param  {Object}          newPermissions         Object where the keys represent the principal ids for which the content permissions should be updated/added/removed. The value is a string representing the new role. If false is passed in, the permissions for that principal will be removed e.g. {'user1': 'manager', 'user2': 'viewer', 'user3': false}
   * @param  {Function}        callback               Standard callback function takes argument `err`
   * @param  {Object}          callback.err           Error object containing the error message
   * @param  {Content}         callback.newContentObj The content object with the updated `lastModified` timestamp
   * @api private
   */
  var _setContentPermissions = function(ctx, contentObj, newPermissions, callback) {
      // Check if all of the provided principals exist
      var principalIds = _.keys(newPermissions);
      PrincipalsUtil.getPrincipals(ctx, principalIds, function(err, principals) {
          if (err) {
              return callback(err);
          }
  
          // Extract all of the users that are removed from the content, so the content can be removed from their library
          var removedMembers = [];
          for (var i = 0; i < principalIds.length; i++) {
              var principalId = principalIds[i];
              if (newPermissions[principalId] === false) {
                  removedMembers.push(principalId);
              } else if (!TenantsUtil.canInteract(contentObj.tenant.alias, principals[principalId].tenant.alias)) {
                  // We shouldn't be sharing this with someone outside of its tenant
                  return callback({'code': 401, 'msg': 'This content item cannot be shared with external user "' + principals[principalId].id + '".'});
              }
          }
  
          // Update the roles CF
          AuthzAPI.updateRoles(contentObj.id, newPermissions, function(err) {
              if (err) {
                  return callback(err);
              }
  
              // Now update all of the libraries this affects
              ContentDAO.Content.updateLibraries(contentObj, removedMembers, function(err, newContentObj) {
                  if (err) {
                      // If there was an error updating libraries here, the permissions were still changed, so we should not return an error. Just log it.
                      log().warn({
                          'err': err,
                          'contentObj': contentObj,
                          'removedMembers': removedMembers
                      }, 'Failed to update user libraries after updating content permissions.');
  
                      if (newContentObj) {
                          // We successfully updated the timestamp of the content object, so we return that one
                          return callback(null, newContentObj);
                      } else {
                          // We did not successfully update the timestamp, return the old content object
                          return callback(null, contentObj);
                      }
                  }
  
                  return callback(null, newContentObj);
              });
          });
      });
  };
  
  
  /**
   * Remove a piece of content from a content library. This is its own API method due to special permission handling required, as the user
   * is effectively updating a content permissions (removing themselves, or removing it from a group they manage), and they might not
   * necessarily have access to update the permissions of the private content (e.g., they are only a member). Also, tenant privacy
   * rules do not come into play in this case.
   *
   * @param  {Context}    ctx             The context of the current request
   * @param  {String}     libraryOwnerId  The owner of the library, should be a principal id (either user or group id)
   * @param  {String}     contentId       The id of the content to remove from the library
   * @param  {Function}   callback        Invoked when the method is complete
   * @param  {Object}     callback.err    An error that occurred, if any
   */
  var removeContentFromLibrary = module.exports.removeContentFromLibrary = function(ctx, libraryOwnerId, contentId, callback) {
      var validator = new Validator();
      validator.check(null, {'code': 401, 'msg': 'You must be authenticated to remove a piece of content from a library'}).isLoggedInUser(ctx);
      validator.check(libraryOwnerId, {'code': 400, 'msg': 'A user or group id must be provided'}).isPrincipalId();
      validator.check(contentId, {'code': 400, 'msg': 'A valid content id must be provided'}).isResourceId();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Make sure the content exists
      ContentDAO.Content.getContent(contentId, function(err, content) {
          if (err) {
              return callback(err);
          }
  
          // Verify the current user has access to remove discussions from the target library
          LibraryAPI.Authz.canRemoveFromLibrary(ctx, libraryOwnerId, function(err, canRemove) {
              if (err) {
                  return callback(err);
              } else if (!canRemove) {
                  return callback({'code': 401, 'msg': 'You are not authorized to delete a piece of content from this library'});
              }
  
              var permissionChanges = {};
              permissionChanges[libraryOwnerId] = false;
  
              // Ensure we are actually removing something, and that we'll be left with at least one manager afterward
              AuthzAPI.computeMembershipAfterChanges(contentId, permissionChanges, function(err, membershipAfterChanges, newMembers, updatedMembers, removedMembers) {
                  if (err) {
                      return callback(err);
                  } else if (!_.some(membershipAfterChanges, function(role, principalId) { return role === ContentConstants.roles.MANAGER; })) {
                      // If the anticipated membership after these changes has no manager, bail out
                      return callback({'code': 400, 'msg': 'The requested change results in a piece of content with no managers'});
                  } else if (removedMembers.length === 0) {
                      return callback({'code': 400, 'msg': 'The specified piece of content is not in this library'});
                  }
  
                  // All validation checks have passed, finally persist the role change and update the user library
                  AuthzAPI.updateRoles(contentId, permissionChanges, function(err) {
                      if (err) {
                          return callback(err);
                      }
  
                      ContentAPI.emit(ContentConstants.events.UPDATED_DISCUSSION_MEMBERS, ctx, content);
  
                      LibraryAPI.Index.remove(LIBRARY_INDEX_NAME, [ libraryOwnerId ], content.id, content.lastModified, function(err) {
                          if (err) {
                              log().error({
                                      'err': err,
                                      'principalIds': principalIds,
                                      'contentId': content.id
                                  }, 'Error removing content item from principal libraries');
                              return callback(err);
                          }
  
                          return callback();
                      });
                  });
              });
          });
      });
  };
  
  
  /**
   * Get all of the principals that are directly associated to a piece of content.
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId           The id of the content object for which we want to get the members
   * @param  {String}         start               Determines the point at which content members are returned for paging purposes.  If not provided, the first x elements will be returned
   * @param  {Integer}        limit               Number of items to return. Will default to 10 if not provided
   * @param  {Function}       callback            Standard callback function takes arguments `err` and `members`
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Object[]}       callback.members    Array that contains an object for each member. Each object has a role property that contains the role of the member and a profile property that contains the principal profile of the member
   * @param  {String}         callback.nextToken  The value to provide in the `start` parameter to get the next set of results
   */
  var getContentMembers = module.exports.getContentMembers = function(ctx, contentId, start, limit, callback) {
      limit = OaeUtil.getNumberParam(limit, 10, 1);
  
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      validator.check(limit, {'code': 400, 'msg': 'A valid limit should be passed in'}).isInt();
      if (start) {
          validator.check(start, {'code': 400, 'msg': 'A start value must be either null or a principal ID'}).isPrincipalId();
      }
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          // Get the content members
          AuthzAPI.getAuthzMembers(contentObj.id, start, limit, function(err, members, nextToken) {
              if (err) {
                  return callback(err);
              }
  
              // Get the basic profiles for all of these principals
              var memberIds = _.map(members, function(member){ return member.id; });
              PrincipalsUtil.getPrincipals(ctx, memberIds, function(err, memberProfiles) {
                  if (err) {
                      return callback(err);
                  }
  
                  // Merge the member profiles and roles into a single object
                  var memberList = [];
                  for (var m = 0; m < members.length; m++) {
                      if (memberProfiles[members[m].id]) {
                          memberList.push({
                              'profile': memberProfiles[members[m].id],
                              'role': members[m].role
                          });
                      }
                  }
  
                  return callback(null, memberList, nextToken);
              });
          });
      });
  };
  
  /////////////////////////////
  // Update content metadata //
  /////////////////////////////
  
  /**
   * Update the file body that is associated with a file content item.
   *
   * @param  {Context}    ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}     contentId           The ID of the content object that should be updated.
   * @param  {File}       file                An expressjs File object that holds the data for the file that needs updating.
   * @param  {Function}   [callback]          Standard callback function takes arguments `err` and `revision`
   * @param  {Object}     [callback.err]      Error object containing the error message
   */
  var updateFileBody = module.exports.updateFileBody = function(ctx, contentId, file, callback) {
      callback = callback || function(err) {
          if (err) {
              log().error({'err': err}, 'Error updating the filebody for %s', contentId);
          }
      };
  
      // Wrap the callback function into a function that cleans up the file in case something went wrong.
      var cleanUpCallback = _getCleanUpCallback({'file': file}, callback);
  
      // Perform the update.
      _updateFileBody(ctx, contentId, file, cleanUpCallback);
  };
  
  /**
   * Update the file body that is associated with an uploaded file.
   *
   * @param  {Context}    ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}     contentId           The ID of the content object that should be updated.
   * @param  {File}       file                An expressjs File object that holds the data for the file that needs updating.
   * @param  {Function}   [callback]          Standard callback function takes arguments `err` and `revision`
   * @param  {Object}     [callback.err]      Error object containing the error message
   * @api private
   */
  var _updateFileBody = function(ctx, contentId, file, callback) {
      var validator = new Validator();
      validator.check(null, {'code': 401, 'msg': 'You have to be logged in to be able to update a content item'}).isLoggedInUser(ctx);
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      validator.check(file, {'code': 400, 'msg': 'Missing file parameter.'}).notNull();
      if (file) {
          validator.check(file.size, {'code': 400, 'msg': 'Missing size on the file object.'}).notEmpty();
          validator.check(file.size, {'code': 400, 'msg': 'Invalid size on the file object.'}).isInt();
          validator.check(file.size, {'code': 400, 'msg': 'Invalid size on the file object.'}).min(0);
          validator.check(file.name, {'code': 400, 'msg': 'Missing name on the file object.'}).notEmpty();
      }
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      canManage(ctx, contentId, function(err, isManager, contentObj) {
          if (err) {
              return callback(err);
          } else if (!isManager) {
              return callback({'code': 401, 'msg': 'You are not allowed to manage this piece of content'});
          } else if (contentObj.resourceSubType !== 'file') {
              return callback({'code': 400, 'msg': 'This content object is not a file.'});
          }
  
          // Create a revision id ahead of time for the new revision, so we know where to store the file body for it
          var revisionId = _generateRevisionId(contentId);
  
          // Detect the mimetype of the file using the file extension, as the one that Express gives us is pulled
          // from the HTTP request. This makes it an untrustworthy piece of information as some browsers are
          // notoriously bad at providing the correct mimetype and it can be spoofed. If the mimetype cannot
          // be determined, the mime utility falls back to application/octet-stream.
          file.type = mime.lookup(file.name);
  
          // Store the file, using the current time as the folder name
          var options = {'resourceId': contentObj.id, 'prefix': revisionId};
          getStorageBackend(ctx).store(ctx, file, options, function(err, uri) {
              if (err) {
                  return callback(err);
              }
  
              // Create the revision.
              var opts = {
                  'mime': file.type,
                  'size': file.size,
                  'filename': file.name,
                  'uri': uri
              };
  
              ContentDAO.Revisions.createRevision(revisionId, contentObj.id, ctx.user().id, opts, function(err, revision) {
                  if (err) {
                      return callback(err);
                  }
  
                  // Set the new filesize, filename and mimetype on the Content object so the UI
                  // can retrieve all the relevant metadata in 1 Cassandra query.
                  opts.latestRevisionId = revision.revisionId;
  
                  // We have to set the previews status back to pending
                  opts.previews = {'status': ContentConstants.previews.PENDING};
                  ContentDAO.Content.updateContent(contentObj, opts, true, function(err, updatedContentObj) {
                      if (err) {
                          return callback(err);
                      }
  
                      ContentAPI.emit(ContentConstants.events.UPDATED_CONTENT_BODY, ctx, contentObj, revision, opts);
  
                      // Output a full content profile.
                      _getFullContentProfile(ctx, updatedContentObj, true, callback);
                  });
              });
          });
      });
  };
  
  /**
   * Attaches preview items to, or set the status of, a revision of a piece of content.
   * This can only be used by a global admin and should technically only be executed by the preview processor.
   *
   * @param  {Context}     ctx                 Current execution context.
   * @param  {String}      contentId           The ID of the content item.
   * @param  {String}      revisionId          The revision ID of the content item.
   * @param  {String}      status              The result of the preview processing operation. It should be one of the values of ContentConstants.previews.
   * @param  {Object}      [files]             An expressjs files object. The keys are the POST parameter under which the file is submitted, the value is an expressjs File object.
   * @param  {Object}      [sizes]             Each key maps a filename to a preview size.
   * @param  {Object}      [contentMetadata]   An object that holds optional content metadata, data like displayName for example can be passed in here.
   * @param  {Object}      [previewMetadata]   An object that holds optional preview metadata, data like pageCount for example can be passed in here.
   * @param  {Function}    callback            Standard callback method
   * @param  {Object}      callback.err        Standard error object (if any.)
   */
  var setPreviewItems = module.exports.setPreviewItems = function(ctx, contentId, revisionId, status, files, sizes, contentMetadata, previewMetadata, callback) {
      files = files || {};
      sizes = sizes || {};
      contentMetadata = contentMetadata || {};
      previewMetadata = previewMetadata || {};
  
      // Wrap the callback method, which takes care of cleaning up the files if something goes wrong.
      var cleanUpCallback = _getCleanUpCallback(files, callback);
  
      // Check for authentication before we do any expensive checks.
      if (!ctx.user() || !ctx.user().isGlobalAdmin()) {
          return cleanUpCallback({'code': 401, 'msg': 'Only administrators can attach preview items to a content item.'});
      }
  
      var validStatuses = _.values(ContentConstants.previews);
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'Missing or invalid contentId'}).isResourceId();
      validator.check(revisionId, {'code': 400, 'msg': 'Missing or invalid revisionId'}).isResourceId();
      validator.check(status, {'code': 400, 'msg': 'The status parameter must be one of: ' + validStatuses.join(', ')}).isIn(validStatuses);
      if (validator.hasErrors()) {
          return cleanUpCallback(validator.getFirstError());
      }
  
      ContentDAO.Content.getContent(contentId, function(err, contentObj) {
          if (err) {
              return cleanUpCallback(err);
          }
  
          ContentDAO.Revisions.getRevision(revisionId, function(err, revision) {
              if (err) {
                  return cleanUpCallback(err);
              }
  
              // Ensure that the revision supplied is a revision of the specified content item
              validator.check(revision.contentId, {'code': 400, 'msg': 'Specified revisionId does not belong to the specifed content item'}).equals(contentId);
              if (validator.hasErrors()) {
                  return cleanUpCallback(validator.getFirstError());
              }
  
              var fileData = {};
              var thumbnailUri = null;
              var fileKeys = Object.keys(files);
              var todo = fileKeys.length;
  
              if (status === ContentConstants.previews.ERROR || todo === 0) {
                  // Preview generation failed or no files were uploaded, store that information in the database
                  return ContentDAO.Previews.storeMetadata(contentObj, revisionId, status, thumbnailUri, contentMetadata, previewMetadata, fileData, callback);
              }
  
              // Preview generation was successful, store the files
  
              // We store the previews in a location within the content item, in the preview directory specified for the revision
              var storePrefix = 'previews/' + revision.previewsId;
  
              var called = false;
              fileKeys.forEach(function(key) {
                  var size = sizes[key];
                  if (!size) {
                      todo--;
                      log().warn('Ignoring file %s as it has no size associated to it', key);
                      return;
                  }
  
                  // Store the file with a regular storage backend.
                  var options = {'resourceId': contentObj.id, 'prefix': storePrefix};
                  getStorageBackend(ctx).store(ctx, files[key], options, function(err, uri) {
                      todo--;
                      if (err && !called) {
                          called = true;
                          return cleanUpCallback(err);
                      }
  
                      // We use the filename as the column name as there is a requirement that the UI can have
                      // sequential URLs. It's up to the preview processor to provide these sequential URLs.
                      // ie: ../download/page1.png, ../download/page2.png, ...
                      // Rather than having an extra columnfamily to store a single piece of metadata (the size),
                      // we store it next to the URI.
                      fileData[files[key].name] = size + '#' + uri;
  
                      // Remember the thumbnail uri separately so we can stick it on the main content object.
                      if (size === 'thumbnail') {
                          thumbnailUri = uri;
  
                      // Remember the small, medium and large URIs so we can stick it on the previews object.
                      } else if (size === 'small') {
                          previewMetadata['smallUri'] = uri;
                      } else if (size === 'medium') {
                          previewMetadata['mediumUri'] = uri;
                      } else if (size === 'large') {
                          previewMetadata['largeUri'] = uri;
                      }
  
                      if (todo === 0 && !called) {
                          // All files have been stored.
                          called = true;
                          log().trace({'data': fileData}, 'Storing %d content preview files.', fileKeys.length);
                          ContentDAO.Previews.storeMetadata(contentObj, revisionId, status, thumbnailUri, contentMetadata, previewMetadata, fileData, function(err) {
                              if (err) {
                                  return callback(err);
                              }
  
                              // Fire an event so we can update the search index.
                              ContentAPI.emit(ContentConstants.events.UPDATED_CONTENT_PREVIEW, ctx, contentObj.id, contentObj);
  
                              return callback();
                          });
                      }
                  });
              });
          });
      });
  };
  
  /**
   * Verifies the signature for a preview item that is associated to a piece of content.
   * If the signature is valid, an object will be returned that the REST handlers can use
   * to redirect the user to the actual file.
   *
   * @param  {Context}    ctx                     The current context.
   * @param  {String}     contentId               The content ID that the preview item is associated with.
   * @param  {String}     revisionId              The revision ID that the preview item is associated with.
   * @param  {String}     previewItem             The preview item that needs to be retrieved.
   * @param  {Object}     signature               A signature that validates this call.
   * @param  {String}     signature.signature     A signature that validates this call.
   * @param  {Number}     signature.expires       When the signature expires (in millis since epoch.)
   * @param  {Number}     signature.lastModified  When the signature expires (in millis since epoch.)
   * @param  {Function}   callback                Standard callback method.
   * @param  {Object}     callback.err            Standard error object (if any)
   * @param  {Object}     callback.downloadLink   Object that holds a `code` and `link`.
   */
  var verifySignedPreviewItem = module.exports.verifySignedPreviewItem = function(ctx, contentId, revisionId, previewItem, signature, callback) {
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'Missing content ID'}).isResourceId();
      validator.check(revisionId, {'code': 400, 'msg': 'Missing revision ID'}).isResourceId();
      validator.check(previewItem, {'code': 400, 'msg': 'Missing preview item'}).notEmpty();
      validator.check(null, {'code': 400, 'msg': 'Missing signature object'}).isObject(signature);
      if (_.isObject(signature)) {
          validator.check(signature.signature, {'code': 400, 'msg': 'Missing signature'}).notEmpty();
          validator.check(signature.expires, {'code': 400, 'msg': 'Missing expires'}).notEmpty();
          validator.check(signature.expires, {'code': 400, 'msg': 'Invalid expires parameter'}).isInt();
          validator.check(signature.lastModified, {'code': 400, 'msg': 'Missing lastModified parameter'}).notEmpty();
          validator.check(signature.lastModified, {'code': 400, 'msg': 'Invalid lastModified parameter'}).isInt();
          var tenantAlias = (contentId) ? AuthzUtil.getResourceFromId(contentId).tenantAlias : '';
          var principalId = (ctx.user()) ? ctx.user().id : '';
          var expires = (signature.expires) ? signature.expires : 0;
          var lastModified = (signature.lastModified) ? signature.lastModified : 0;
          var body = principalId + ':' + contentId + ':' + lastModified;
          validator.check(signature.signature, {'code': 401, 'msg': 'Invalid signature.'}).verifyExpiringSignature(tenantAlias, expires, body);
      }
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      ContentDAO.Previews.getContentPreview(revisionId, previewItem, function(err, preview) {
          if (err) {
              return callback(err);
          }
  
          var downloadLink = getStorageBackend(ctx, preview.uri).getDownloadLink(ctx, preview.uri);
          var result = {
              'filename': previewItem,
              'downloadLink': downloadLink
          };
          callback(null, result);
      });
  };
  
  /**
   * Get the entire list of preview items that are associated to a content item.
   *
   * @param  {Context}     ctx                 The current context.
   * @param  {String}      contentId           The ID of the content you wish to retrieve the preview items for
   * @param  {String}      revisionId          The ID of the revision you wish to retrieve the preview items for
   * @param  {Function}    callback            Standard callback method.
   * @param  {Object}      callback.err        Standard error object.
   * @param  {Object}      callback.results    Object with a key `files` that holds an array of strings that are the filenames for previews and a key `signature` which holds the signature and expires parameters that should be sent when retrieving the preview bodies.
   */
  var getPreviewItems = module.exports.getPreviewItems = function(ctx, contentId, revisionId, callback) {
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          ContentDAO.Previews.getContentPreviews(revisionId, function(err, previews) {
              if (err) {
                  return callback(err);
              }
  
              // Generate an expiring signature.
              var signature = ContentUtil.signContent(ctx, contentId, contentObj.lastModified);
              var result = {
                  'files': previews,
                  'signature': signature
              };
  
              callback(null, result);
          });
      });
  };
  
  /**
   * Update a content item's displayName, description or visibility. This can only be done by the manager of that piece of content.
   *
   * @param  {Context}        ctx                     Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId               The id of the content object for which we want to update the content profile
   * @param  {Object}         profileFields           Object where the keys represent the profile fields that need to be updated and the values represent the new values for those profile fields e.g. {'displayName': 'New content name', 'description': 'New content description', 'visibility': 'private'}
   * @param  {Function}       callback                Standard callback function takes argument `err`
   * @param  {Object}         callback.err            Error object containing the error message
   * @param  {Content}        callback.newContentObj  The updated content object
   */
  var updateContentMetadata = module.exports.updateContentMetadata = function(ctx, contentId, profileFields, callback) {
      callback = callback || function() {};
  
      // Parameter validation
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      // Check that at a minimum name or description have been provided
      var fieldNames = profileFields ? _.keys(profileFields) : [];
      validator.check(fieldNames.length, {'code': 400, 'msg': 'You should at least specify a new displayName, description, visibility or link'}).min(1);
      for (var i = 0; i < fieldNames.length; i++) {
          var fieldName = fieldNames[i];
          validator.check(fieldName, {'code': 400, 'msg': fieldName + ' is not a recognized content profile field'}).isIn(['displayName', 'description', 'visibility', 'link']);
          if (fieldName === 'displayName') {
              validator.check(profileFields['displayName'], {'code': 400, 'msg': 'A displayName cannot be empty'}).notEmpty();
              validator.check(profileFields['displayName'], {'code': 400, 'msg': 'A displayName can only be 1000 characters long'}).isShortString();
          } else if (fieldName === 'description' && profileFields['description']) {
              validator.check(profileFields['description'], {'code': 400, 'msg': 'A description can only be 10000 characters long'}).isMediumString();
          } else if (fieldName === 'link') {
              validator.check(profileFields['link'], {'code': 400, 'msg': 'A valid link should be provided'}).isUrl();
          }
      }
      if (profileFields['visibility']) {
          validator.check(profileFields['visibility'], {'code': 400, 'msg': 'An invalid content visibility option has been provided. This can be "private", "loggedin" or "public"'}).isIn(_.values(ContentConstants.visibility));
      }
      validator.check(null, {'code': 401, 'msg': 'You have to be logged in to be able to update a content item'}).isLoggedInUser(ctx);
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // First check whether or not the current user is a manager of the piece of content
      canManage(ctx, contentId, function(err, canManage, oldContentObj) {
          if (err) {
              return callback(err);
          } else if (!canManage) {
              return callback({'code': 401, 'msg': 'You are not allowed to manage this piece of content'});
          }
  
          if (profileFields.link) {
              if (oldContentObj.resourceSubType !== 'link') {
                  return callback({'code': 400, 'msg': 'This piece of content is not a link'});
              } else if (profileFields.link !== oldContentObj.link) {
                  // Reset the previews object so we don't show the old preview items while the new link is still being processed
                  profileFields.previews = {'status': ContentConstants.previews.PENDING};
              }
          }
  
          ContentDAO.Content.updateContent(oldContentObj, profileFields, true, function(err, newContentObj) {
              if (err) {
                  return callback(err);
              }
  
              ContentAPI.emit(ContentConstants.events.UPDATED_CONTENT, ctx, newContentObj, oldContentObj);
  
              // Add the isManager, createdBy, .. properties.
              _getFullContentProfile(ctx, newContentObj, true, callback);
          });
      });
  };
  
  
  // TODO: Split this out once we reorganize the content API
  ///////////////////////////
  // Comment functionality //
  ///////////////////////////
  
  /**
   * Creates a comment on the specified content item. Returns an error if saving the comment goes wrong or the user doesn't have access.
   * Only logged in users who can see the content are able to post comments.
   *
   * @param  {Context}    ctx                         Standard context object, representing the currently logged in user and its tenant
   * @param  {String}     contentId                   The ID of the content to place the comment on
   * @param  {String}     body                        The comment to place on the content item
   * @param  {String}     [replyToCreatedTimestamp]   The timestamp (millis since the epoch) that the comment to which we're replying (if applicable) was created
   * @param  {Function}   [callback]                  Standard callback function takes argument `err`
   * @param  {Object}     [callback.err]              Error object containing the error message
   * @param  {Comment}    [callback.comment]          The created comment
   */
  var createComment = module.exports.createComment = function(ctx, contentId, body, replyToCreatedTimestamp, callback) {
      callback = callback || function() {};
  
      // Parameter validation
      var validator = new Validator();
      validator.check(null, {'code': 401, 'msg': 'Only authorized users can post comments'}).isLoggedInUser(ctx);
      validator.check(contentId, {'code': 400, 'msg': 'Invalid content resource id provided'}).isResourceId();
      validator.check(body, {'code': 400, 'msg': 'A comment must be provided'}).notEmpty();
      validator.check(body, {'code': 400, 'msg': 'A comment can only be 100000 characters long'}).isLongString();
      if (replyToCreatedTimestamp) {
          validator.check(replyToCreatedTimestamp, {'code': 400, 'msg': 'Invalid reply-to timestamp provided'}).isInt();
      }
  
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Verify the user has access to the content object
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          MessageBoxAPI.createMessage(contentId, ctx.user().id, body, {'replyToCreated': replyToCreatedTimestamp}, function(err, message) {
              if (err) {
                  return callback(err);
              }
  
              message.createdBy = ctx.user();
              ContentAPI.emit(ContentConstants.events.CREATED_COMMENT, ctx, message, contentObj);
              return callback(null, message);
          });
      });
  };
  
  /**
   * Retrieves the comments for a specified content item. Everyone who has access to the content item will be able to retrieve the list of comments.
   *
   * @param  {Context}    ctx                  Standard context object, representing the currently logged in user and its tenant
   * @param  {String}     contentId            The ID of the content to get comments for
   * @param  {String}     start                Determines the point at which content items are returned for paging purposed. If not provided, the first x elements will be returned
   * @param  {Integer}    limit                Number of items to return. Will default to 10 if not provided
   * @param  {Function}   callback             Standard callback function takes argument `err` and `comments`
   * @param  {Object}     callback.err         Error object containing the error message
   * @param  {Comment[]}  callback.comments    An array of Comment objects
   * @param  {String}     callback.nextToken   The value to provide in the `start` parameter to get the next set of results
   */
  var getComments = module.exports.getComments = function(ctx, contentId, start, limit, callback) {
      limit = OaeUtil.getNumberParam(limit, 10, 1);
  
      // Parameter validation
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'Invalid content resource id provided'}).isResourceId();
      validator.check(limit, {'code': 400, 'msg': 'A valid limit should be passed in'}).isInt();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          MessageBoxAPI.getMessagesFromMessageBox(contentId, start, limit, null, function(err, comments, nextToken) {
              if (err) {
                  return callback(err);
              }
  
              // Get information on the commenters
              var userIds = {};
              for (var i = 0; i < comments.length; i++) {
                  // Filter out duplicates
                  if (comments[i].createdBy) {
                      userIds[comments[i].createdBy] = true;
                  }
              }
              userIds = _.keys(userIds);
  
              // Get the basic principal profiles of the commenters to add to the comments as `createdBy`.
              PrincipalsUtil.getPrincipals(ctx, userIds, function(err, principals) {
                  if (err) {
                      return callback(err);
                  }
  
                  for (var i = 0; i < comments.length; i++) {
                      comments[i].createdBy = principals[comments[i].createdBy];
                  }
  
                  return callback(err, comments, nextToken);
              });
          });
      });
  };
  
  /**
   * Deletes a comment from a content item. Managers of the content can delete all comments whilst people that have access
   * to the content can only delete their own comments. Therefore, anonymous users will never be able to delete comments.
   *
   * @param  {Context}    ctx                     Standard context object, representing the currently logged in user and its tenant
   * @param  {String}     contentId               The ID of the content to delete the comment from
   * @param  {String}     commentCreatedDate      The timestamp (in millis since the epoch) that the comment we wish to delete was created
   * @param  {Function}   callback                Standard callback function takes argument `err`
   * @param  {Object}     callback.err            Error object containing the error message
   * @param  {Comment}    [callback.softDeleted]  When the comment has been soft deleted (because it has replies), a stripped down comment object representing the deleted comment will be returned, with the `deleted` parameter set to `false`. If the comment has been deleted from Cassandra, no comment object will be returned.
   */
  var deleteComment = module.exports.deleteComment = function(ctx, contentId, commentCreatedDate, callback) {
  
      var validator = new Validator();
      validator.check(null, {'code': 401, 'msg': 'Only authorized users can delete comments'}).isLoggedInUser(ctx);
      validator.check(contentId, {'code': 400, 'msg': 'A content id must be provided'}).isResourceId();
      validator.check(commentCreatedDate, {'code': 400, 'msg': 'A valid integer comment created timestamp must be specified'}).isInt();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          MessageBoxAPI.getMessages(contentId, [commentCreatedDate], null, function(err, messages) {
              if (err) {
                  return callback(err);
              } else if (messages.length === 0 || !messages[0]) {
                  return callback({'code': 404, 'msg': 'The specified comment does not exist'});
              }
  
              var message = messages[0];
              if (ctx.user().id === message.createdBy) {
                  return _deleteComment(contentId, commentCreatedDate, callback);
              }
  
              // We aren't the creator of the message, see if we can manage the content to do it
              _canManage(ctx, contentObj, function(err, canManage) {
                  if (err) {
                      return callback(err);
                  } else if (!canManage) {
                      return callback({'code': 401, 'msg': 'You do not have access to delete this comment'});
                  }
  
                  return _deleteComment(contentId, commentCreatedDate, callback);
              });
          });
      });
  };
  
  /**
   * Delete the comment (either hard or soft, depending on if there are any replies) with the given timestamp from the
   * message box with the given id.
   *
   * @param  {String}         messageBoxId            The id of the messagebox from which to delete the comment
   * @param  {String|Number}  commentCreatedTimestamp The timestamp (millis since the epoch) that this comment was created
   * @param  {Function}       callback                Invoked when the process completes
   * @param  {Object}         callback.err            An error that occurred, if any
   * @param  {Message}        [callback.comment]      If the comment was soft-deleted, this will be the model of the scrubbed deleted comment
   */
  var _deleteComment = function(messageBoxId, commentCreatedTimestamp, callback) {
      // Delete the comment using the "leaf" method, which will SOFT delete if the message has replies, or HARD delete if it does not
      MessageBoxAPI.deleteMessage(messageBoxId, commentCreatedTimestamp, {'deleteType': MessageBoxConstants.deleteTypes.LEAF}, function(err, deleteType, message) {
          if (err) {
              return callback(err);
          }
  
          if (deleteType === MessageBoxConstants.deleteTypes.SOFT) {
              // If a soft-delete occurred, we want to inform the consumer of the soft-delete message model
              return callback(null, message);
          } else {
              return callback();
          }
      });
  };
  
  
  ///////////////////////////
  // Library functionality //
  ///////////////////////////
  
  /**
   * Get the content items inside of an authorizable's library. If the user requests its own library or the library of a group he's a member of,
   * the full list will be returned. If the user is logged in, but not a manager of the library, the :loggedin stream will be returned, containing
   * only content that's visible to logged in people or the public. In case of an anonymous user, the :public stream will be returned, containing
   * only content that is public.
   *
   * @param  {Context}        ctx                 Standard context object containing the current user and the current tenant
   * @param  {String}         principalId         The principal id of the library we're requesting
   * @param  {String}         start               Determines the point at which content items are returned for paging purposed. If not provided, the first x elements will be returned
   * @param  {Integer}        limit               Number of items to return. Will default to 10 if not provided
   * @param  {Function}       callback            Standard callback function takes arguments `err` and `content`
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Content[]}      callback.content    Array of basic content profiles representing the requested items in the library
   * @param  {String}         callback.nextToken  The value to provide in the `start` parameter to get the next set of results
   */
  var getLibraryItems = module.exports.getLibraryItems = function(ctx, principalId, start, limit, callback) {
      limit = OaeUtil.getNumberParam(limit, 10, 1);
  
      var validator = new Validator();
      validator.check(principalId, {'code': 400, 'msg': 'A user or group id must be provided'}).isPrincipalId();
      validator.check(limit, {'code': 400, 'msg': 'A valid limit should be passed in'}).isInt();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Get the principal
      PrincipalsDAO.getPrincipal(principalId, function(err, principal) {
          if (err) {
              return callback(err);
          }
  
          // Determine which library visibility we need to fetch
          LibraryAPI.Authz.resolveTargetLibraryAccess(ctx, principal, function(err, hasAccess, visibility) {
              if (err) {
                  return callback(err);
              } else if (!hasAccess) {
                  return callback({'code': 401, 'msg': 'You do not have access to this library.'});
              }
  
              ContentDAO.Content.getLibraryItems(principalId, visibility, start, limit, function(err, contentObjects, nextToken) {
                  if (err) {
                      return callback(err);
                  }
  
                  _.each(contentObjects, function(contentObj) {
                      _augmentContent(ctx, contentObj);
                  });
  
                  return callback(null, contentObjects, nextToken);
              });
          });
      });
  };
  
  /**
   * Get the storage backend for a uri, if the uri is unspecified it will return the default backend for a tenant.
   * If the backend could not be found, this will throw an error!
   * The uri will be checked before defaulting to the configured tenant backend. This allows for a tenant
   * to switch storage systems yet still serve the old files.
   *
   * @param  {Context}    ctx     The current execution context.
   * @param  {String}     [uri]   An optional URI.
   * @return {Backend}            The appropriate backend.
   */
  var getStorageBackend = module.exports.getStorageBackend = function(ctx, uri) {
      var backendName = null;
      if (uri) {
          backendName = uri.split(':')[0];
      } else {
          // Use the tenant's default.
          backendName = Config.getValue(ctx.tenant().alias, 'storage', 'backend');
          if (backendName === null) {
              log().error('There was no storage backend configured, this should not happen.');
              throw new Error('There was no storage backend configured, this should not happen.');
          }
      }
      if (!_backends[backendName]) {
          try {
              _backends[backendName] = require('oae-content/lib/backends/' + backendName);
          } catch (err) {
              log().error({'err': err}, 'Couldn\'t load the backend %s', backendName);
              throw new Error('Could not find storage back-end ' + backendName);
          }
      }
  
      return _backends[backendName];
  };
  
  ///////////////
  // Revisions //
  ///////////////
  
  /**
   * Get the revisions that are associated to a piece of content.
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId           The id of the object for which we want to get the revisions
   * @param  {Integer}        [start]             Determines the point at which revisions are returned for paging purposes.  If not provided, the first x elements will be returned. The `created` value from the last retrieved revision should be used here.
   * @param  {Integer}        [limit]             Number of revisions to return. Will default to 10 if not provided
   * @param  {Function}       callback            Standard callback function takes arguments `err` and `revisions`
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Revision[]}     callback.revisions  Array that contains an object for each revision.
   * @param  {String}         callback.nextToken  The value to provide in the `start` parameter to get the next set of results
   */
  var getRevisions = module.exports.getRevisions = function(ctx, contentId, start, limit, callback) {
      limit = OaeUtil.getNumberParam(limit, 10, 1);
  
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A contentId must be provided'}).isResourceId();
      validator.check(limit, {'code': 400, 'msg': 'A valid limit should be passed in'}).isInt();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Check if the user has access on this contentId
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          // all columns except etherpadHtml
          var opts = {
              'fields': [
                  'contentId',
                  'created',
                  'createdBy',
                  'filename',
                  'mediumUri',
                  'mime',
                  'previews',
                  'previewsId',
                  'revisionId',
                  'size',
                  'thumbnailUri',
                  'uri',
                  'wideUri'
              ]
          };
          _getRevisions(ctx, contentObj, start, limit, opts, callback);
      });
  };
  
  /**
   * Internal method that retrieves revisions and augments them with the principal profile who created it.
   * This method performs no access checks.
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {Content}        contentObj          The content object for which we need to retrieve the revisions.
   * @param  {String}         start               Determines the point at which revisions are returned for paging purposes.  If not provided, the first x elements will be returned
   * @param  {Integer}        limit               Number of revisions to return. Will default to 10 if not provided
   * @param  {Object}         [opts]              Additional options
   * @param  {String[]}       [opts.fields]       The columns to get from cassandra if not specified all will be fetched
   * @param  {Function}       callback            Standard callback function takes arguments `err` and `revisions`
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Revision[]}     callback.revisions  Array that contains an object for each revision.
   * @param  {String}         callback.nextToken  The value to provide in the `start` parameter to get the next set of results
   * @api private
   */
  var _getRevisions = function(ctx, contentObj, start, limit, opts, callback) {
      // Page the query.
      ContentDAO.Revisions.getRevisions(contentObj.id, start, limit, opts, function(err, revisions, nextToken) {
          if (err) {
              return callback(err);
          }
  
          var userIds = _.map(revisions, function(revisions) { return revisions.createdBy; });
          PrincipalsUtil.getPrincipals(ctx, userIds, function(err, users) {
              if (err) {
                  return callback(err);
              }
  
              // Add the user profiles to the revisions.
              _.each(revisions, function(revision) {
                  if (users[revision.createdBy]) {
                      revision.createdBy = users[revision.createdBy];
                      _augmentRevision(ctx, revision, contentObj);
                  }
              });
  
              callback(null, revisions, nextToken);
          });
      });
  };
  
  /**
   * Get a specific revision for a piece of content.
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId           The id of the content for which we want to get the revision
   * @param  {String}         [revisionId]        The id of the revision that needs to be retrieved, if left null the latest will be retrieved.
   * @param  {Function}       callback            Standard callback function
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Revision}       callback.revision   The retrieved revision
   */
  var getRevision = module.exports.getRevision = function(ctx, contentId, revisionId, callback) {
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A valid contentId must be provided'}).isResourceId();
      if (revisionId) {
          validator.check(revisionId, {'code': 400, 'msg': 'A valid revisionId must be provided'}).isResourceId();
      }
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Check if the user has access on this contentId
      getContent(ctx, contentId, function(err, contentObj) {
          if (err) {
              return callback(err);
          }
  
          // The user has access, get the revision and augment it with a downloadload link
          // if this piece of content is a file.
          _getRevision(ctx, contentObj, revisionId, callback);
      });
  };
  
  /**
   * Internal method for retrieving a specific revision.
   * It's assumed that the parameters have been properly validated beforehand.
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {Content}        contentObj          The content object for which we need to retrieve a revision.
   * @param  {String}         [revisionId]        The id of the revision that needs to be retrieved, if left null the latest will be retrieved.
   * @param  {Function}       callback            Standard callback function
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Revision}       callback.revision   The retrieved revision
   * @api private
   */
  var _getRevision = function(ctx, contentObj, revisionId, callback) {
      if (!revisionId) {
          // Get the latest one.
          _getRevisions(ctx, contentObj, null, 1, null, function(err, revisions) {
              if (err) {
                  return callback(err);
              }
  
              if (revisions.length === 0) {
                  return callback({'code': 404, 'msg': 'No revision found for ' + contentObj.id});
              }
  
              // There is no need to augment the revisions here as that has already happened.
              callback(null, revisions[0]);
          });
      } else {
          ContentDAO.Revisions.getRevision(revisionId, function(err, revisionObj) {
              if (err) {
                  return callback(err);
              }
  
              // Double check that this revision is really attached to the specified contentId.
              // This is to counter that someone tries to get the revision of a piece of content he has no access to.
              // Ex: Alice has access to c:cam:aliceDoc but not to c:cam:bobDoc which has revision rev:cam:foo
              // doing getRevision(ctx, 'c:cam:aliceDoc', 'rev:cam:foo', ..) should return this error.
              if (revisionObj.contentId !== contentObj.id) {
                  return callback({'code': 400, 'msg': 'This revision ID is not associated with the specified piece of content.'});
              }
  
              _augmentRevision(ctx, revisionObj, contentObj);
              callback(null, revisionObj);
          });
      }
  };
  
  /**
   * Adds a download url for the thumbnail and medium image (if present).
   *
   * @param  {Context}    ctx         Standard context object, representing the currently logged in user and its tenant
   * @param  {Revision}   revision    The revision to augment
   * @param  {Content}    contentObj  The content object that the revision is attached to
   * @api private
   */
  var _augmentRevision = function(ctx, revision, contentObj) {
      var principalId = (ctx.user() ? ctx.user().id : '');
      if (revision.thumbnailUri) {
          revision.thumbnailUrl = ContentUtil.getDownloadUrlForUri(principalId, ctx.tenant().alias, revision.thumbnailUri);
      }
      if (revision.mediumUri) {
          revision.mediumUrl = ContentUtil.getDownloadUrlForUri(principalId, ctx.tenant().alias, revision.mediumUri);
      }
      if (contentObj.resourceSubType === 'file') {
          revision.downloadLink = getStorageBackend(ctx, revision.uri).getDownloadLink(ctx, revision.uri);
      }
  };
  
  /**
   * Takes a revision and makes it the "current" revision by creating a new one and copying the existing revision's fields.
   *
   * @param  {Context}        ctx                 Standard context object, representing the currently logged in user and its tenant
   * @param  {String}         contentId           The id of the content for which we want to restore a revision
   * @param  {String}         revisionId          The id of the revision that needs to be restored.
   * @param  {Function}       callback            Standard callback function
   * @param  {Object}         callback.err        Error object containing the error message
   * @param  {Revision}       callback.revision   The newly created revision
   */
  var restoreRevision = module.exports.restoreRevision = function(ctx, contentId, revisionId, callback) {
      var validator = new Validator();
      validator.check(contentId, {'code': 400, 'msg': 'A valid contentId must be provided'}).isResourceId();
      validator.check(revisionId, {'code': 400, 'msg': 'A valid revisionId must be provided'}).isResourceId();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Make sure the user is a manager of this piece of content and the revision exists.
      canManage(ctx, contentId, function(err, canManage, contentObj) {
          if (err) {
              return callback(err);
          } else if (!canManage) {
              return callback({'code': 401, 'msg': 'Manager rights are required to restore a revision.'});
          }
  
          ContentDAO.Revisions.getRevision(revisionId, function(err, revision) {
              if (err) {
                  return callback(err);
              } else if (contentObj.id !== revision.contentId) {
                  return callback({'code': 400, 'msg': 'The contentId specified is not the owner of the specified revisionId'});
              }
  
              // Create a new revision by copying from the specified revision
              var newRevisionId = _generateRevisionId(contentId);
              ContentDAO.Revisions.createRevision(newRevisionId, contentId, ctx.user().id, revision, function(err, newRevision) {
                  if (err) {
                      return callback(err);
                  }
  
                  /*!
                   * We need to update the content item in the Content CF.
                   * We do so by copying all the non-standard fields from the revision
                   * to the Content CF.
                   */
                  var blacklist = ['revisionId', 'contentId', 'createdBy', 'created', 'etherpadHtml', 'previewsId'];
                  var updates = _.omit(revision, blacklist);
  
                  // We also need to update the latest revisionID in the content CF.
                  updates.latestRevisionId = newRevisionId;
  
                  ContentDAO.Content.updateContent(contentObj, updates, true, function(err, newContentObj) {
                      if (err) {
                          return callback(err);
                      }
  
                      // Provide user-level data such as signed URLs for the consumer
                      _augmentRevision(ctx, newRevision, newContentObj);
  
                      // Emit an event
                      ContentAPI.emit(ContentConstants.events.RESTORED_REVISION, ctx, newContentObj, contentObj, revision);
  
                      // If this piece of content is a collaborative document,
                      // we need to set the text in etherpad.
                      if (contentObj.resourceSubType === 'collabdoc') {
                          Etherpad.setHTML(contentObj.id, contentObj.etherpadPadId, revision.etherpadHtml, function(err) {
                              if (err) {
                                  return callback(err);
                              }
                              callback(null, newRevision);
                          });
                      } else {
                          callback(null, newRevision);
                      }
                  });
              });
          });
      });
  };
  
  /**
   * Verifies if a uri and signature match up and returns an object that the REST handlers can use to redirect users
   * to the actual download page.
   *
   * @param  {Context}     ctx                                 Current execution context.
   * @param  {String}      uri                                 The URI of a file.
   * @param  {Number}      expires                             When the signature expires, in milliseconds since epoch.
   * @param  {String}      signature                           The signature of the uri.
   * @param  {Object}      callback.err                        Error object containing the error message
   * @param  {Object}      callback.data                       The object that contains the data that the REST handler can use.
   * @param  {Object}      callback.data.downloadLink          The link object with an http status code and a url
   * @param  {Object}      callback.data.downloadLink.code     The HTTP status code (usually 204 or 301/302)
   * @param  {Object}      callback.data.downloadLink.url      The url where the user should be redirected to.
   * @param  {Object}      callback.data.filename              The filename of the file that the user wishes to download.
   */
  var verifySignedUri = module.exports.verifySignedUri = function(ctx, uri, expires, signature, callback) {
      var validator = new Validator();
      validator.check(uri, {'code': 400, 'msg': 'Missing uri'}).notNull();
      validator.check(uri, {'code': 400, 'msg': 'Invalid uri'}).contains(':');
      validator.check(expires, {'code': 400, 'msg': 'Missing expires'}).notNull();
      validator.check(expires, {'code': 400, 'msg': 'Invalid expires'}).isInt();
      validator.check(signature, {'code': 400, 'msg': 'Missing signature'}).notNull();
      expires = parseInt(expires, 10);
      var principalId = (ctx.user()) ? ctx.user().id : '';
      var body = principalId + ':' + uri;
      validator.check(signature, {'code': 401, 'msg': 'Invalid signature'}).verifyExpiringSignature(ctx.tenant().alias, expires, body);
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
      var downloadLink = getStorageBackend(ctx, uri).getDownloadLink(ctx, uri);
      var data = {
          'downloadLink': downloadLink,
          'filename': Path.basename(uri)
      };
      callback(null, data);
  };
  
  ///////////////////////
  // Utility functions //
  ///////////////////////
  
  /**
   * Adds the signature and thumbnail url (if the piece of content has a thumbnail.)
   *
   * @param  {Context}    ctx         Standard context object, representing the currently logged in user and its tenant
   * @param  {Content}    contentObj  The content object that needs augmenting.
   */
  var _augmentContent = function(ctx, contentObj) {
      // Generate a signature for this content item.
      // In combination with the previews object, the UI should be able to construct
      // download URLs for the preview items.
      contentObj.signature = ContentUtil.signContent(ctx, contentObj.id, contentObj.lastModified);
  
      var principalId = (ctx.user() ? ctx.user().id : null);
      var tenantAlias = ctx.tenant().alias;
      if (contentObj.previews) {
          if (contentObj.previews.thumbnailUri) {
              contentObj.previews.thumbnailUrl = ContentUtil.getDownloadUrlForUri(principalId, tenantAlias, contentObj.previews.thumbnailUri);
          }
          if (contentObj.previews.smallUri) {
              contentObj.previews.smallUrl = ContentUtil.getDownloadUrlForUri(principalId, tenantAlias, contentObj.previews.smallUri);
          }
          if (contentObj.previews.mediumUri) {
              contentObj.previews.mediumUrl = ContentUtil.getDownloadUrlForUri(principalId, tenantAlias, contentObj.previews.mediumUri);
          }
          if (contentObj.previews.largeUri) {
              contentObj.previews.largeUrl = ContentUtil.getDownloadUrlForUri(principalId, tenantAlias, contentObj.previews.largeUri);
          }
      }
  };
  
  /**
   * Generates a new content ID.
   *
   * @param  {String}     tenantAlias     The tenant alias for the content item
   * @return {String}                     The new content ID.
   * @api private
   */
  var _generateContentId = function(tenantAlias) {
      return AuthzUtil.toId('c', tenantAlias, ShortId.generate());
  };
  
  /**
   * Generates a new revision ID for a content item by ID.
   *
   * @param  {String}     contentId   The ID of the content item for which to generate a new revision ID
   * @return {String}                 The new revision ID
   * @api private
   */
  var _generateRevisionId = function(contentId) {
      var tenantAlias = AuthzUtil.getResourceFromId(contentId).tenantAlias;
      return AuthzUtil.toId('rev', tenantAlias, ShortId.generate());
  };
  