oaeproject/Hilary

View on GitHub
packages/oae-content/lib/rest.js

Summary

Maintainability
F
1 wk
Test Coverage
A
97%
/*!
 * Copyright 2014 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.
 */

import * as querystring from 'node:querystring';
import {
  isResourceACollabDoc,
  isResourceACollabSheet,
  isResourceAFile,
  isResourceALink
} from 'oae-content/lib/backends/util.js';

import _ from 'underscore';

import { AuthzConstants } from 'oae-authz/lib/constants.js';
import * as OAE from 'oae-util/lib/oae.js';
import * as OaeUtil from 'oae-util/lib/util.js';

import * as ContentAPI from './api.js';
import { ContentConstants } from './constants.js';

/**
 * Verify the signature information provided by a signed download request and
 * pass it on to the download handler to complete the download request
 *
 * @param  {Request}     req    The Express Request object
 * @param  {Response}    res    The Express Response object
 * @api private
 */
const _handleSignedDownload = function (request, response) {
  ContentAPI.verifySignedDownloadQueryString(request.ctx, request.query, (error, downloadInfo) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    return _handleDownload(response, downloadInfo, true);
  });
};

/**
 * @REST postContentCreateCollabdoc
 *
 * Create a new collaborative document
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/create
 * @FormParam   {string}            displayName         The display name of the collaborative document
 * @FormParam   {string}            resourceSubType     The content item type                                                                       [collabdoc]
 * @FormParam   {string}            [description]       A longer description for the collaborative document
 * @FormParam   {string[]}          [managers]          Unique identifier(s) for users and groups to add as managers of the collaborative document. The user creating the collaborative document will be added as a manager automatically
 * @FormParam   {string[]}          [editors]           Unique identifier(s) for users and groups to add as editors of the collaborative document
 * @FormParam   {string[]}          [viewers]           Unique identifier(s) for users and groups to add as members of the collaborative document
 * @FormParam   {string[]}          [folders]           Unique identifier(s) for folders to which the collaborative document should be added
 * @FormParam   {string}            [visibility]        The visibility of the collaborative document. Defaults to the configured tenant default     [loggedin,private,public]
 * @Return      {BasicContent}                          The created collaborative document
 * @HttpResponse                    201                 Document created
 * @HttpResponse                    400                 A display name must be provided
 * @HttpResponse                    400                 A display name can be at most 1000 characters long
 * @HttpResponse                    400                 A description can only be 10000 characters long
 * @HttpResponse                    400                 A valid resourceSubType must be provided. This can be "file", "collabdoc" or "link"
 * @HttpResponse                    400                 An invalid content visibility option has been provided. This can be "private", "loggedin" or "public"
 * @HttpResponse                    400                 One or more target members being granted access are not authorized to become members on this content item
 * @HttpResponse                    400                 One or more target members being granted access do not exist
 * @HttpResponse                    400                 The additional members should be specified as an object
 * @HttpResponse                    401                 You have to be logged in to be able to create a content item
 */

/**
 * @REST postContentCreateCollabsheet
 *
 * Create a new collaborative spreadsheet
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/create
 * @FormParam   {string}            displayName         The display name of the collaborative spreadsheet
 * @FormParam   {string}            resourceSubType     The content item type                                                                       [spreadsheet]
 * @FormParam   {string}            [description]       A longer description for the collaborative spreadsheet
 * @FormParam   {string[]}          [managers]          Unique identifier(s) for users and groups to add as managers of the collaborative spreadsheet. The user creating the collaborative document will be added as a manager automatically
 * @FormParam   {string[]}          [editors]           Unique identifier(s) for users and groups to add as editors of the collaborative spreadsheet
 * @FormParam   {string[]}          [viewers]           Unique identifier(s) for users and groups to add as members of the collaborative spreadsheet
 * @FormParam   {string[]}          [folders]           Unique identifier(s) for folders to which the collaborative spreadsheet should be added
 * @FormParam   {string}            [visibility]        The visibility of the collaborative spreadsheet. Defaults to the configured tenant default     [loggedin,private,public]
 * @Return      {BasicContent}                          The created collaborative spreadsheet
 * @HttpResponse                    201                 Spreadsheet created
 * @HttpResponse                    400                 A display name must be provided
 * @HttpResponse                    400                 A display name can be at most 1000 characters long
 * @HttpResponse                    400                 A description can only be 10000 characters long
 * @HttpResponse                    400                 A valid resourceSubType must be provided. This can be "file", "collabdoc", "collabsheet" or "link"
 * @HttpResponse                    400                 An invalid content visibility option has been provided. This can be "private", "loggedin" or "public"
 * @HttpResponse                    400                 One or more target members being granted access are not authorized to become members on this content item
 * @HttpResponse                    400                 One or more target members being granted access do not exist
 * @HttpResponse                    400                 The additional members should be specified as an object
 * @HttpResponse                    401                 You have to be logged in to be able to create a content item
 */

/**
 * @REST postContentCreateFile
 *
 * Create new file
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/create
 * @FormParam   {string}            displayName         The display name of the file
 * @FormParam   {File}              file                The binary content for the file
 * @FormParam   {string}            resourceSubType     The content item type                                                                [file]
 * @FormParam   {string}            [description]       A longer description for the file
 * @FormParam   {string[]}          [managers]          Unique identifier(s) for users and groups to add as managers of the file. The user creating the file will be added as a manager automatically
 * @FormParam   {string[]}          [viewers]           Unique identifier(s) for users and groups to add as members of the file
 * @FormParam   {string[]}          [folders]           Unique identifier(s) for folders to which the file should be added
 * @FormParam   {string}            [visibility]        The visibility of the file. Defaults to the configured tenant default               [loggedin,private,public]
 * @Return      {BasicContent}                          The created file
 * @HttpResponse                    201                 File uploaded
 * @HttpResponse                    400                 A display name must be provided
 * @HttpResponse                    400                 A display name can be at most 1000 characters long
 * @HttpResponse                    400                 A description can only be 10000 characters long
 * @HttpResponse                    400                 A valid resourceSubType must be provided. This can be "file", "collabdoc", "collabsheet" or "link"
 * @HttpResponse                    400                 An invalid content visibility option has been provided. This can be "private", "loggedin" or "public"
 * @HttpResponse                    400                 One or more target members being granted access are not authorized to become members on this content item
 * @HttpResponse                    400                 One or more target members being granted access do not exist
 * @HttpResponse                    400                 The additional members should be specified as an object
 * @HttpResponse                    401                 Anonymous users are not allowed to upload files
 * @HttpResponse                    401                 You have to be logged in to be able to create a content item
 */

/**
 * @REST postContentCreateLink
 *
 * Create new link
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/create
 * @FormParam   {string}            displayName         The display name of the link
 * @FormParam   {string}            link                The URL to which the link points
 * @FormParam   {string}            resourceSubType     The content item type                                                                       [link]
 * @FormParam   {string}            [description]       A longer description for the link
 * @FormParam   {string[]}          [managers]          Unique identifier(s) for users and groups to add as managers of the link. The user creating the link will be added as a manager automatically
 * @FormParam   {string[]}          [viewers]           Unique identifier(s) for users and groups to add as members of the link
 * @FormParam   {string[]}          [folders]           Unique identifier(s) for folders to which the link should be added
 * @FormParam   {string}            [visibility]        The visibility of the link. Defaults to the configured tenant default                       [loggedin,private,public]
 * @Return      {BasicContent}                          The created link
 * @HttpResponse                    201                 Link created
 * @HttpResponse                    400                 A display name must be provided
 * @HttpResponse                    400                 A display name can be at most 1000 characters long
 * @HttpResponse                    400                 A description can only be 10000 characters long
 * @HttpResponse                    400                 A valid link must be provided
 * @HttpResponse                    400                 A valid resourceSubType must be provided. This can be "file", "collabdoc", "collabsheet" or "link"
 * @HttpResponse                    400                 An invalid content visibility option has been provided. This can be "private", "loggedin" or "public"
 * @HttpResponse                    400                 One or more target members being granted access are not authorized to become members on this content item
 * @HttpResponse                    400                 One or more target members being granted access do not exist
 * @HttpResponse                    400                 The additional members should be specified as an object
 * @HttpResponse                    401                 You have to be logged in to be able to create a content item
 */
OAE.tenantRouter.on('post', '/api/content/create', (request, response) => {
  // Ensure proper arrays for the multi-value parameters
  request.body.managers = OaeUtil.toArray(request.body.managers);
  request.body.editors = OaeUtil.toArray(request.body.editors);
  request.body.viewers = OaeUtil.toArray(request.body.viewers);
  request.body.folders = OaeUtil.toArray(request.body.folders);

  // Construct a hash for additional members that maps each user to their role
  const additionalMembers = {};
  _.each(request.body.managers, (userId) => {
    additionalMembers[userId] = AuthzConstants.role.MANAGER;
  });
  _.each(request.body.editors, (userId) => {
    additionalMembers[userId] = AuthzConstants.role.EDITOR;
  });
  _.each(request.body.viewers, (userId) => {
    additionalMembers[userId] = AuthzConstants.role.VIEWER;
  });

  let uploadedFile = null;
  if (request.files && request.files.file) {
    uploadedFile = request.files.file;
  }

  _createContent(
    request.ctx,
    request.body.resourceSubType,
    request.body.displayName,
    request.body.description,
    request.body.visibility,
    request.body.link,
    uploadedFile,
    additionalMembers,
    request.body.folders,
    (error, contentObject) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      // Set the response type to text/plain for file uploads, as the UI uses an iFrame upload mechanism
      // to support IE9 file uploads. If the response type is not set to text/plain, IE9 will try to
      // download the response
      if (request.files && request.files.file) {
        response.set('Content-Type', 'text/plain');
      }

      return response.status(201).send(contentObject);
    }
  );
});

/**
 * Create a piece of content
 *
 * @param  {Context}        ctx                     Current execution context
 * @param  {String}         resourceSubType         The type of content to create
 * @param  {String}         displayName             The display name of the content item
 * @param  {String}         [description]           A longer description for the content item
 * @param  {String}         [visibility]            The visibility of the content item. One of `public`, `loggedin`, `private`
 * @param  {String}         [link]                  The URL when creating a content item of resourceSubType `link`
 * @param  {File}           [uploadedFile]          The file object when creating a content item of resourceSubType `file`
 * @param  {String[]}       folders               The ids of folders where the content item should be added to
 * @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", as well as "editor" for collabdocs or collabsheets
 * @param  {Function}       callback                Standard callback function
 * @param  {Object}         callback.err            An error object, if any
 * @param  {Content}        callback.content        The created content object
 * @api private
 */
const _createContent = function (
  ctx,
  resourceSubType,
  displayName,
  description,
  visibility,
  link,
  uploadedFile,
  additionalMembers,
  folders,
  callback
) {
  // Link creation
  if (isResourceALink(resourceSubType)) {
    return ContentAPI.createLink(
      ctx,
      {
        displayName,
        description,
        visibility,
        link,
        additionalMembers,
        folders
      },
      callback
    );

    // File creation
  }

  if (isResourceAFile(resourceSubType)) {
    return ContentAPI.createFile(
      ctx,
      { displayName, description, visibility, file: uploadedFile, additionalMembers, folders },
      callback
    );

    // Collaborative document creation
  }

  if (isResourceACollabDoc(resourceSubType)) {
    return ContentAPI.createCollabDoc(ctx, displayName, description, visibility, additionalMembers, folders, callback);

    // Not a recognized file type
  }

  // Collaborative spreadsheet creation
  if (isResourceACollabSheet(resourceSubType)) {
    return ContentAPI.createCollabSheet(
      ctx,
      displayName,
      description,
      visibility,
      additionalMembers,
      folders,
      callback
    );
  }

  return callback({
    code: 400,
    msg: 'Unrecognized resourceSubType. Accepted values are "link", "file", "collabdoc" and "collabsheet"'
  });
};

/**
 * @REST deleteContentContentId
 *
 * Delete a content item
 *
 * @Server      tenant
 * @Method      DELETE
 * @Path        /content/{contentId}
 * @PathParam   {string}                contentId           The id of the content item to delete
 * @Return      {void}
 * @HttpResponse                        200                 Content deleted
 * @HttpResponse                        400                 A content id must be provided
 * @HttpResponse                        401                 You are not allowed to manage this piece of content
 * @HttpResponse                        401                 You have to be logged in to be able to delete a content item'
 */
OAE.tenantRouter.on('delete', '/api/content/:contentId', (request, response) => {
  ContentAPI.deleteContent(request.ctx, request.params.contentId, (error) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).end();
  });
});

/**
 * @REST getContentContentId
 *
 * Get a full content item profile
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}
 * @PathParam   {string}                contentId           The id of the content item to get
 * @Return      {Content}                                   Full content profile
 * @HttpResponse                        200                 Content available
 * @HttpResponse                        400                 A content id must be provided
 */
OAE.tenantRouter.on('get', '/api/content/:contentId', (request, response) => {
  ContentAPI.getFullContentProfile(request.ctx, request.params.contentId, (error, contentProfile) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).send(contentProfile);
  });
});

/**
 * @REST postContentContentId
 *
 * Update a content item
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/{contentId}
 * @PathParam   {string}                contentId           The id of the content item to update
 * @FormParam   {string}                [description]       Updated description for the content item
 * @FormParam   {string}                [displayName]       Updated display name for the content item
 * @FormParam   {string}                [link]              Updated URL for a link
 * @FormParam   {string}                [visibility]        Updated visibility for the discussion           [loggedin,private,public]
 * @Return      {Content}                                   The updated content item
 * @HttpResponse                        200                 Content updated
 * @HttpResponse                        400                 ... is not a recognized content profile field
 * @HttpResponse                        400                 A content id must be provided
 * @HttpResponse                        400                 A display name cannot be empty
 * @HttpResponse                        400                 A display name can be at most 1000 characters long
 * @HttpResponse                        400                 A description can only be 10000 characters long
 * @HttpResponse                        400                 A valid link should be provided
 * @HttpResponse                        400                 An invalid content visibility option has been provided. This can be "private", "loggedin" or "public"
 * @HttpResponse                        400                 This piece of content is not a link
 * @HttpResponse                        400                 You should at least specify a new displayName, description, visibility or link
 * @HttpResponse                        401                 You are not allowed to manage this piece of content
 * @HttpResponse                        401                 You have to be logged in to be able to update a content item
 */
OAE.tenantRouter.on('post', '/api/content/:contentId', (request, response) => {
  ContentAPI.updateContentMetadata(request.ctx, request.params.contentId, request.body, (error, newContentObject) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).send(newContentObject);
  });
});

/**
 * @REST getContentContentIdDownload
 *
 * Download the latest revision of a file
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}/download
 * @PathParam   {string}                contentId           The id of the file to download
 * @Return      {File}                                      The latest revision of the file
 * @HttpResponse                        200                 Content provided
 * @HttpResponse                        302                 Redirecting to content
 * @HttpResponse                        400                 A valid contentId must be provided
 * @HttpResponse                        400                 Only file content items can be downloaded
 * @HttpResponse                        404                 Content not available
 */
OAE.tenantRouter.on('get', '/api/content/:contentId/download', (request, response) => {
  ContentAPI.getRevisionDownloadInfo(request.ctx, request.params.contentId, null, (error, downloadInfo) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    return _handleDownload(response, downloadInfo, false);
  });
});

/**
 * @REST getContentContentIdDownloadRevisionId
 *
 * Download a revision of a file
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}/download/{revisionId}
 * @PathParam   {string}                contentId           The id of the file to download
 * @PathParam   {string}                revisionId          The id of the revision to download
 * @Return      {File}                                      The specified revision of the file
 * @HttpResponse                        200                 Content provided
 * @HttpResponse                        302                 Redirecting to content
 * @HttpResponse                        400                 A valid contentId must be provided
 * @HttpResponse                        400                 If provided, the revisionId must be valid and pointing to an existing revision
 * @HttpResponse                        400                 No revision id provided and content item does not have a latest revision id
 * @HttpResponse                        400                 Only file content items can be downloaded
 * @HttpResponse                        400                 The revision id provided is not associated with the specified content item
 * @HttpResponse                        404                 Content not available
 */
OAE.tenantRouter.on('get', '/api/content/:contentId/download/:revisionId', (request, response) => {
  ContentAPI.getRevisionDownloadInfo(
    request.ctx,
    request.params.contentId,
    request.params.revisionId,
    (error, downloadInfo) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      return _handleDownload(response, downloadInfo, true);
    }
  );
});

/**
 * @REST postContentContentIdNewversion
 *
 * Upload a new version of a file
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/{contentId}/newversion
 * @PathParam   {string}            contentId               The id of the file to upload a new version for
 * @FormParam   {File}              file                    The new binary content for the file
 * @Return      {Content}                                   The updated content item
 * @HttpResponse                    200                     New version accepted
 * @HttpResponse                    400                     A content id must be provided
 * @HttpResponse                    400                     This content object is not a file
 * @HttpResponse                    401                     You are not allowed to manage this piece of content
 * @HttpResponse                    401                     You have to be logged in to be able to update a content item
 * @HttpResponse                    404                     Content not available
 */
OAE.tenantRouter.on('post', '/api/content/:contentId/newversion', (request, response) => {
  if (!request.files || !request.files.file) {
    return response.status(400).send('Missing file parameter');
  }

  ContentAPI.updateFileBody(
    request.ctx,
    request.params.contentId,
    request.files.file,
    (error, updatedContentObject) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      // Set the response type to text/plain, as the UI uses an iFrame upload mechanism to support IE9
      // file uploads. If the response type is not set to text/plain, IE9 will try to download the response.
      response.set('Content-Type', 'text/plain');
      response.status(200).send(updatedContentObject);
    }
  );
});

/**
 * @REST postContentContentIdJoin
 *
 * Join a collaborative document or spreadsheet
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/{contentId}/join
 * @PathParam   {string}            contentId               The id of the collaborative document to join
 * @Return      {CollabdocJoinInfo}                         Information on how to join the collaborative document
 * @HttpResponse                    200                     Joined collabdoc or spreadsheet
 * @HttpResponse                    400                     This is not a collaborative document or spreadsheet
 * @HttpResponse                    401                     You need to be a manager of this piece of content to be able to join it
 * @HttpResponse                    404                     Content not available
 */
OAE.tenantRouter.on('post', '/api/content/:contentId/join', (request, response) => {
  ContentAPI.joinCollabDoc(request.ctx, request.params.contentId, (error, data) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).send(data);
  });
});

/**
 * @REST postContentContentIdRevisionsRevisionIdPreviews
 *
 * Attach a preview item to a content item
 *
 * @Api         private
 * @Server      tenant
 * @Method      POST
 * @Path        /content/{contentId}/revisions/{revisionId}/previews
 * @PathParam   {string}            contentId               The id of the content item to attach a preview to
 * @PathParam   {string}            revisionId              The id of the revision to attach a preview to
 * @BodyParam   {UpdatedPreview}    body                    Updated preview metadata
 * @FormParam   {File}              file                    The binary content of the preview
 * @Return      {void}
 * @HttpResponse                    201                     Preview added
 * @HttpResponse                    400                     Malformed metadata object. Expected proper JSON for: ...
 * @HttpResponse                    400                     Missing or invalid contentId
 * @HttpResponse                    400                     Missing or invalid revisionId
 * @HttpResponse                    400                     Specified revisionId does not belong to the specifed content item
 * @HttpResponse                    400                     The status parameter must be one of: `done`, `error` or `ignored`
 * @HttpResponse                    401                     Only administrators can attach preview items to a content item
 * @HttpResponse                    404                     Content not available
 */
OAE.tenantRouter.on('post', '/api/content/:contentId/revisions/:revisionId/previews', (request, response) => {
  let contentMetadata = null;
  let previewMetadata = null;
  let sizes = null;
  let files = null;
  try {
    contentMetadata = JSON.parse(request.body.contentMetadata);
    previewMetadata = JSON.parse(request.body.previewMetadata);
    sizes = JSON.parse(request.body.sizes);

    if (request.body.links) {
      files = JSON.parse(request.body.links);
    }
  } catch {
    let invalidField = null;
    if (!contentMetadata) {
      invalidField = 'contentMetadata';
    } else if (!previewMetadata) {
      invalidField = 'previewMetadata';
    } else if (!sizes) {
      invalidField = 'sizes';
    } else if (!files) {
      invalidField = 'links';
    }

    return response.status(400).send('Malformed metadata object. Expected proper JSON for: ' + invalidField);
  }

  if (request.files) {
    files = files || {};
    _.extend(files, request.files);
  }

  ContentAPI.setPreviewItems(
    request.ctx,
    {
      contentId: request.params.contentId,
      revisionId: request.params.revisionId,
      status: request.body.status,
      files,
      sizes,
      contentMetadata,
      previewMetadata
    },
    (error) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      response.status(201).end();
    }
  );
});

/**
 * @REST getContentContentIdRevisionsRevisionIdPreviews
 *
 * Get the previews for a revision
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}/revisions/{revisionId}/previews
 * @PathParam   {string}            contentId               The id of the content item to get the previews for
 * @PathParam   {string}            revisionId              The id of the revision to get the previews for
 * @Return      {PreviewsList}                              The revision previews
 * @HttpResponse                    200                     Previews available
 * @HttpResponse                    400                     A content id must be provided
 * @HttpResponse                    401                     You don't have access to this piece of content
 * @HttpResponse                    404                     Content not available
 */
OAE.tenantRouter.on('get', '/api/content/:contentId/revisions/:revisionId/previews', (request, response) => {
  ContentAPI.getPreviewItems(request.ctx, request.params.contentId, request.params.revisionId, (error, previews) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).send(previews);
  });
});

/**
 * @REST getContentContentIdRevisionsRevisionIdPreviewsItem
 *
 * Download a preview
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}/revisions/{revisionId}/previews/{item}
 * @PathParam   {string}             contentId              The id of the content item to download the preview for
 * @PathParam   {string}             revisionId             The id of the revision to download the preview for
 * @PathParam   {string}             item                   The preview item to download
 * @QueryParam  {string}             expires                The timestamp (millis since epoch) at which the signature expires
 * @QueryParam  {string}             signature              The access control signature
 * @Return      {File}                                      The preview
 * @HttpResponse                        200                 Preview provided
 * @HttpResponse                        302                 Redirecting to preview
 * @HttpResponse                        400                 A valid contentId must be provided
 * @HttpResponse                        400                 A valid revisionId must be provided
 * @HttpResponse                        400                 Missing preview item
 * @HttpResponse                        400                 No revision id provided and content item does not have a latest revision id
 * @HttpResponse                        400                 Only file content items can be downloaded
 * @HttpResponse                        400                 The revision id provided is not associated with the specified content item
 * @HttpResponse                        401                 Invalid content signature data for accessing previews
 * @HttpResponse                        404                 Preview not available
 */
OAE.tenantRouter.on('get', '/api/content/:contentId/revisions/:revisionId/previews/:item', (request, response) => {
  const signature = {
    signature: request.query.signature,
    expires: request.query.expires,
    lastModified: request.query.lastmodified
  };
  ContentAPI.getSignedPreviewDownloadInfo(
    request.ctx,
    request.params.contentId,
    request.params.revisionId,
    request.params.item,
    signature,
    (error, downloadInfo) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      return _handleDownload(response, downloadInfo, true);
    }
  );
});

/**
 * @REST getContentContentIdRevisions
 *
 * Get the revisions for a content item
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}/revisions
 * @PathParam   {string}            contentId           The id of the content item to get the revisions for
 * @QueryParam  {number}            [limit]             The maximum number of results to return. Default: 10
 * @QueryParam  {string}            [start]             The revision paging token from which to start fetching revisions
 * @Return      {Revisions}                             The revisions for the specified content item
 * @HttpResponse                    200                 Revisions available
 * @HttpResponse                    400                 A valid contentId must be provided
 * @HttpResponse                    400                 A valid limit should be passed in
 * @HttpResponse                    404                 Content not available
 */
OAE.tenantRouter.on('get', '/api/content/:contentId/revisions', (request, response) => {
  const limit = OaeUtil.getNumberParam(request.query.limit, 10, 1, 25);
  ContentAPI.getRevisions(
    request.ctx,
    request.params.contentId,
    request.query.start,
    limit,
    (error, revisions, nextToken) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      response.status(200).send({ results: revisions, nextToken });
    }
  );
});

/**
 * @REST getContentContentIdRevisionsRevisionId
 *
 * Get a revision
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}/revisions/{revisionId}
 * @PathParam   {string}            contentId           The id of the content item to get the revision for
 * @PathParam   {string}            revisionId          The id of the revision to get
 * @Return      {Revision}                              The revision
 * @HttpResponse                    200                 Revision available
 * @HttpResponse                    400                 A valid contentId must be provided
 * @HttpResponse                    400                 A valid revisionId must be provided
 * @HttpResponse                    404                 Content not available
 */
OAE.tenantRouter.on('get', '/api/content/:contentId/revisions/:revisionId', (request, response) => {
  ContentAPI.getRevision(request.ctx, request.params.contentId, request.params.revisionId, (error, revision) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).send(revision);
  });
});

/**
 * @REST postContentContentIdRevisionsRevisionIdRestore
 *
 * Restore a revision
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/{contentId}/revisions/{revisionId}/restore
 * @PathParam   {string}            contentId           The id of the content item to restore the revision for
 * @PathParam   {string}            revisionId          The id of the revision to restore
 * @Return      {Revision}                              The restored revision
 * @HttpResponse                    200                 Revision restored
 * @HttpResponse                    400                 A valid contentId must be provided
 * @HttpResponse                    400                 A valid revisionId must be provided
 * @HttpResponse                    400                 The contentId specified is not the owner of the specified revisionId
 * @HttpResponse                    401                 Manager rights are required to restore a revision
 * @HttpResponse                    404                 Content not available
 */
OAE.tenantRouter.on('post', '/api/content/:contentId/revisions/:revisionId/restore', (request, response) => {
  ContentAPI.restoreRevision(request.ctx, request.params.contentId, request.params.revisionId, (error, newRevision) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).send(newRevision);
  });
});

/**
 * @REST getContentContentIdMembers
 *
 * Get the members of a content item and their roles
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}/members
 * @PathParam   {string}            contentId           The id of the content item to get the members for
 * @QueryParam  {number}            [limit]             The maximum number of results to return. Default: 10
 * @QueryParam  {string}            [start]             The content paging token from which to start fetching content members
 * @Return      {MembersResponse}                       Members of the specified content item
 * @HttpResponse                    200                 Members available
 * @HttpResponse                    400                 A valid content id must be provided
 * @HttpResponse                    400                 A valid limit should be passed in
 * @HttpResponse                    401                 You are not authorized to access the members of this content item
 * @HttpResponse                    404                 Content not available
 */
OAE.tenantRouter.on('get', '/api/content/:contentId/members', (request, response) => {
  const limit = OaeUtil.getNumberParam(request.query.limit, 10, 1, 25);
  ContentAPI.getContentMembersLibrary(
    request.ctx,
    request.params.contentId,
    request.query.start,
    limit,
    (error, members, nextToken) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      response.status(200).send({ results: members, nextToken });
    }
  );
});

/**
 * @REST postContentContentIdMembers
 *
 * Update the members of a content item
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/{contentId}/members
 * @PathParam   {string}                contentId           The id of the content item to update the members for
 * @BodyParam   {ContentMembersUpdate}  body                Object that describes the membership updates to apply to the content item
 * @Return      {void}
 * @HttpResponse                        200                 Members updated
 * @HttpResponse                        400                 A valid content id must be provided
 * @HttpResponse                        400                 A principalId needs to be specified for a role
 * @HttpResponse                        400                 At least one role change needs to be applied
 * @HttpResponse                        400                 Invalid principal id specified: ...
 * @HttpResponse                        400                 Invalid role provided
 * @HttpResponse                        400                 You should specify at least 1 user/group to set content permissions on
 * @HttpResponse                        401                 You are not allowed to manage this piece of content
 * @HttpResponse                        404                 Content not available
 */
OAE.tenantRouter.on('post', '/api/content/:contentId/members', (request, response) => {
  // Parse the incoming false values
  const requestKeys = _.keys(request.body);
  for (const element of requestKeys) {
    request.body[element] = OaeUtil.castToBoolean(request.body[element]);
  }

  ContentAPI.setContentPermissions(request.ctx, request.params.contentId, request.body, (error) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).end();
  });
});

/**
 * @REST getContentContentIdInvitations
 *
 * Get all the invitations associated to a content item
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}/invitations
 * @PathParam   {string}                contentId           The id of the content item for which to get invitations
 * @Return      {InvitationsResponse}                       The invitations associated to the content item
 * @HttpResponse                        200                 Invitations available
 * @HttpResponse                        400                 A valid content id must be provided
 * @HttpResponse                        401                 You are not allowed to get invitations for this content item
 * @HttpResponse                        404                 Content not available
 */
OAE.tenantRouter.on('get', '/api/content/:contentId/invitations', (request, response) => {
  ContentAPI.getContentInvitations(request.ctx, request.params.contentId, (error, invitations) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    return response.status(200).send({ results: invitations });
  });
});

/**
 * @REST postContentContentIdInvitationsEmailResend
 *
 * Resend an invitation to a content item
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/{contentId}/invitations/{email}/resend
 * @PathParam   {string}                contentId           The id of the content item for which to get invitations
 * @PathParam   {string}                email               The email for which to resend the invitation
 * @Return      {void}
 * @HttpResponse                        200                 Invitation was resent
 * @HttpResponse                        400                 A valid content id must be provided
 * @HttpResponse                        400                 A valid email must be provided
 * @HttpResponse                        401                 You are not allowed to resend invitations for this content item
 * @HttpResponse                        404                 Content not available
 * @HttpResponse                        404                 No invitation for the specified email exists for the content item
 */
OAE.tenantRouter.on('post', '/api/content/:contentId/invitations/:email/resend', (request, response) => {
  ContentAPI.resendContentInvitation(request.ctx, request.params.contentId, request.params.email, (error) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    return response.status(200).end();
  });
});

/**
 * @REST postContentContentIdShare
 *
 * Share a content item
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/{contentId}/share
 * @PathParam   {string}        contentId           The id of the content item to share
 * @FormParam   {string[]}      viewers             Unique identifier(s) for users and groups to share the content item with
 * @Return      {void}
 * @HttpResponse                200                 Content shared
 * @HttpResponse                400                 A valid content id must be provided
 * @HttpResponse                400                 After this operation, the content item would be left without a manager
 * @HttpResponse                400                 At least one role change needs to be applied
 * @HttpResponse                400                 Invalid principal id specified: ...
 * @HttpResponse                400                 One or more target members being granted access are not authorized to become members on this content item
 * @HttpResponse                400                 One or more target members being granted access do not exist
 * @HttpResponse                400                 The content must at least be shared with 1 user or group
 * @HttpResponse                401                 You are not allowed to share this content
 * @HttpResponse                401                 You have to be logged in to be able to share content
 * @HttpResponse                404                 Content not available
 */
OAE.tenantRouter.on('post', '/api/content/:contentId/share', (request, response) => {
  // Make sure viewers is an array
  request.body.viewers = OaeUtil.toArray(request.body.viewers);
  ContentAPI.shareContent(request.ctx, request.params.contentId, request.body.viewers, (error) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).end();
  });
});

/**
 * @REST postContentContentIdMessages
 *
 * Create a new comment on a content item
 *
 * @Server      tenant
 * @Method      POST
 * @Path        /content/{contentId}/messages
 * @PathParam   {string}        contentId           The id of the content item to which to post the comment
 * @FormParam   {string}        body                The body of the comment
 * @FormParam   {string}        [replyTo]           The timestamp of the comment to which this comment is a reply. Not specifying this will create a top level comment
 * @Return      {Message}                           The created comment
 * @HttpResponse                201                 Comment created
 * @HttpResponse                400                 A comment can only be 100000 characters long
 * @HttpResponse                400                 A comment must be provided
 * @HttpResponse                400                 A messageBoxId must be specified.
 * @HttpResponse                400                 If the replyToCreated optional parameter is specified, it cannot be in the future.
 * @HttpResponse                400                 If the replyToCreated optional parameter is specified, it should be an integer.
 * @HttpResponse                400                 If the replyToCreated optional parameter is specified, it should not be null.
 * @HttpResponse                400                 Invalid content resource id provided
 * @HttpResponse                400                 Invalid reply-to timestamp provided
 * @HttpResponse                400                 Reply-to message does not exist
 * @HttpResponse                400                 The body of the message must be specified.
 * @HttpResponse                400                 The createdBy parameter must be a valid user id.
 * @HttpResponse                401                 Only authorized users can post comments
 * @HttpResponse                404                 Could not find principal with id ...
 * @HttpResponse                404                 Content not available
 * @HttpResponse                500                 Failed to create a new message
 */
OAE.tenantRouter.on('post', '/api/content/:contentId/messages', (request, response) => {
  ContentAPI.createComment(
    request.ctx,
    request.params.contentId,
    request.body.body,
    request.body.replyTo,
    (error, message) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      response.status(201).send(message);
    }
  );
});

/**
 * @REST getContentContentIdMessages
 *
 * Get the comments for a content item
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/{contentId}/messages
 * @PathParam   {string}            contentId           The id of the content item for which to get the comments
 * @QueryParam  {number}            [limit]             The maximum number of results to return. Default: 10
 * @QueryParam  {string}            [start]             The comments paging token from which to start fetching comments
 * @Return      {MessagesResponse}                      The comments on the content item
 * @HttpResponse                    200                 Comments available
 * @HttpResponse                    400                 A messageBoxId must be specified.
 * @HttpResponse                    400                 A timestamp cannot be in the future.
 * @HttpResponse                    400                 A timestamp cannot be null.
 * @HttpResponse                    400                 A timestamp should be an integer.
 * @HttpResponse                    400                 A valid limit should be passed in
 * @HttpResponse                    400                 Invalid content resource id provided
 */
OAE.tenantRouter.on('get', '/api/content/:contentId/messages', (request, response) => {
  const limit = OaeUtil.getNumberParam(request.query.limit, 10, 1, 25);
  ContentAPI.getComments(
    request.ctx,
    request.params.contentId,
    request.query.start,
    limit,
    (error, messages, nextToken) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      response.status(200).send({ results: messages, nextToken });
    }
  );
});

/**
 * @REST deleteContentContentIdMessagesCreated
 *
 * Delete a comment from a content item
 *
 * @Server      tenant
 * @Method      DELETE
 * @Path        /content/{contentId}/messages/{created}
 * @PathParam   {string}                contentId           The id of the content item from which to delete the comment
 * @PathParam   {string}                created             The timestamp of the comment that should be deleted
 * @Return      {Message}                                   The deleted comment
 * @HttpResponse                        200                 Comment deleted
 * @HttpResponse                        400                 A content id must be provided
 * @HttpResponse                        400                 A messageBoxId must be specified.
 * @HttpResponse                        400                 A timestamp cannot be in the future.
 * @HttpResponse                        400                 A timestamp cannot be null.
 * @HttpResponse                        400                 A timestamp should be an integer.
 * @HttpResponse                        400                 A valid integer comment created timestamp must be specified
 * @HttpResponse                        400                 If the deleteType is specified it should be one of: ...
 * @HttpResponse                        400                 The createdTimestamp cannot be in the future.
 * @HttpResponse                        400                 The createdTimestamp should be an integer.
 * @HttpResponse                        400                 The createdTimestamp should not be null.
 * @HttpResponse                        401                 Only authorized users can delete comments
 * @HttpResponse                        401                 You do not have access to delete this comment
 * @HttpResponse                        404                 Message not found.
 * @HttpResponse                        404                 The specified comment does not exist
 * @HttpResponse                        404                 The specified message did not exist
 */
OAE.tenantRouter.on('delete', '/api/content/:contentId/messages/:created', (request, response) => {
  ContentAPI.deleteComment(request.ctx, request.params.contentId, request.params.created, (error, deleted) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).send(deleted);
  });
});

/**
 * @REST getContentLibraryPrincipalId
 *
 * Get the content library items for a user or group
 *
 * @Server      tenant
 * @Method      GET
 * @Path        /content/library/{principalId}
 * @PathParam   {string}            principalId         The id of the principal whose content library to fetch
 * @QueryParam  {number}            [limit]             The maximum number of results to return. Default: 10
 * @QueryParam  {string}            [start]             The content paging token from which to start fetching content items
 * @Return      {ContentLibrary}                        The content library items for the specified user or group
 * @HttpResponse                    200                 Library available
 * @HttpResponse                    400                 A user or group id must be provided
 * @HttpResponse                    400                 A valid limit should be passed in
 * @HttpResponse                    401                 You do not have access to this library
 */
OAE.tenantRouter.on('get', '/api/content/library/:principalId', (request, response) => {
  const limit = OaeUtil.getNumberParam(request.query.limit, 12, 1, 25);
  ContentAPI.getContentLibraryItems(
    request.ctx,
    request.params.principalId,
    request.query.start,
    limit,
    (error, items, nextToken) => {
      if (error) {
        return response.status(error.code).send(error.msg);
      }

      response.status(200).send({ results: items, nextToken });
    }
  );
});

/**
 * @REST deleteContentLibraryPrincipalIdContentId
 *
 * Remove a content item from a content library
 *
 * @Server      tenant
 * @Method      DELETE
 * @Path        /content/library/{principalId}/{contentId}
 * @PathParam   {string}            principalId             The id of the principal from whose content library to remove the content item
 * @PathParam   {string}            contentId               The id of the content item to remove from the library
 * @Return      {void}
 * @HttpResponse                    200                     Content deleted
 * @HttpResponse                    400                     A user or group id must be provided
 * @HttpResponse                    400                     A valid content id must be provided
 * @HttpResponse                    400                     At least one role change needs to be applied
 * @HttpResponse                    400                     Invalid principal id specified: ...
 * @HttpResponse                    400                     Invalid resource id provided.
 * @HttpResponse                    400                     Invalid role provided.
 * @HttpResponse                    400                     The requested change results in a piece of content with no managers
 * @HttpResponse                    400                     The specified piece of content is not in this library
 * @HttpResponse                    401                     You are not authorized to delete a piece of content from this library
 * @HttpResponse                    401                     You must be authenticated to remove a piece of content from a library
 */
OAE.tenantRouter.on('delete', '/api/content/library/:principalId/:contentId', (request, response) => {
  ContentAPI.removeContentFromLibrary(request.ctx, request.params.principalId, request.params.contentId, (error) => {
    if (error) {
      return response.status(error.code).send(error.msg);
    }

    response.status(200).end();
  });
});

/**
 * @REST getDownloadSigned
 *
 * Download a content item using an access control signature
 *
 * @Server      admin,tenant
 * @Method      GET
 * @Path        /download/signed
 * @QueryParam  {string}                expires             The timestamp (millis since epoch) at which the signature expires
 * @QueryParam  {string}                signature           The access control signature
 * @Return      {File}                                      The content item
 * @HttpResponse                        200                 Content provided
 * @HttpResponse                        302                 Redirecting to content
 * @HttpResponse                        401                 Invalid signature data for the provided download url
 * @HttpResponse                        404                 Content not available
 */
OAE.globalAdminRouter.on('get', '/api/download/signed', _handleSignedDownload);
OAE.tenantRouter.on('get', '/api/download/signed', _handleSignedDownload);

/**
 * Send the correct HTTP response according to a download strategy. Note that the "direct" download strategy is not being handled here
 * as the direct strategy should not be requested to the application for action, instead its target is delivered to the user verbatim
 * with no signature.
 *
 * @param  {Response}           res                     The Express Response object
 * @param  {Object}             downloadInfo            An object that represents an item that can be downloaded from the application
 * @param  {String}             downloadInfo.filename   The file name of the download
 * @param  {DownloadStrategy}   downloadInfo.strategy   How the application should deliver the download
 * @param  {Boolean}            [expiresMax]            Whether a far future expires response header should be set
 * @api private
 */
const _handleDownload = function (response, downloadInfo, expiresMax) {
  const downloadStrategy = downloadInfo.strategy;

  // A 204 suggest that the LB (nginx, apache, lighthttpd, ..) will be handling the download via the x-sendfile mechanism
  switch (downloadStrategy.strategy) {
    case ContentConstants.backend.DOWNLOAD_STRATEGY_INTERNAL: {
      // Nginx internal download
      response.setHeader('X-Accel-Redirect', downloadStrategy.target);

      // Apache internal download
      response.setHeader('X-Sendfile', downloadStrategy.target);

      // Lighthttpd internal download
      response.setHeader('X-LIGHTTPD-send-file', downloadStrategy.target);

      if (expiresMax) {
        // Add the cache headers manually as some webservers are not
        // able to deal with setting cache headers and internal redirects
        // @see https://github.com/oaeproject/Hilary/issues/995
        response.setHeader('Expires', 'Thu, 31 Dec 2037 23:55:55 GMT');
        response.setHeader('Cache-Control', 'max-age=315360000');
      }

      response.setHeader(
        'Content-Disposition',
        'attachment; filename="' + querystring.escape(downloadInfo.filename) + '"'
      );
      response.status(204).send(downloadStrategy.target);

      // A redirect strategy will invoke a redirect to the target

      break;
    }

    case ContentConstants.backend.DOWNLOAD_STRATEGY_REDIRECT: {
      // We can't guarantee that the backend won't want to update some details about the target over time. e.g., update some tracking
      // variables over time for analytics or additional security. Therefore, we do a temporary redirect (302)
      response.setHeader('Location', downloadStrategy.target);
      return response.status(302).end();

      // The app server will send the file to the client. This should *NOT* be used in production and is only really here for easier unit
      // testing purposes
    }

    case ContentConstants.backend.DOWNLOAD_STRATEGY_TEST: {
      return response.download(downloadStrategy.target);

      // In all other cases we respond with a 404
    }

    default: {
      response.status(404).end();
    }
  }
};