AbstractGalleryResource.java

/*
 * Copyright (C) 2000 - 2024 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.silverpeas.components.gallery.web;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.jboss.resteasy.plugins.providers.html.View;
import org.silverpeas.components.gallery.constant.GalleryResourceURIs;
import org.silverpeas.components.gallery.constant.MediaResolution;
import org.silverpeas.components.gallery.constant.MediaType;
import org.silverpeas.components.gallery.model.AlbumDetail;
import org.silverpeas.components.gallery.model.InternalMedia;
import org.silverpeas.components.gallery.model.Media;
import org.silverpeas.components.gallery.model.MediaPK;
import org.silverpeas.components.gallery.service.GalleryService;
import org.silverpeas.components.gallery.service.MediaServiceProvider;
import org.silverpeas.core.admin.user.model.SilverpeasRole;
import org.silverpeas.core.io.file.SilverpeasFile;
import org.silverpeas.core.io.file.SilverpeasFileProvider;
import org.silverpeas.core.io.media.Definition;
import org.silverpeas.core.io.media.video.ThumbnailPeriod;
import org.silverpeas.core.node.model.NodePK;
import org.silverpeas.kernel.util.StringUtil;
import org.silverpeas.core.web.http.FileResponse;
import org.silverpeas.core.web.rs.RESTWebService;

import javax.ws.rs.PathParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.StreamingOutput;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Collection;
import java.util.EnumSet;

import static org.silverpeas.components.gallery.constant.GalleryResourceURIs.GALLERY_BASE_URI;

/**
 * @author Yohann Chastagnier
 */
abstract class AbstractGalleryResource extends RESTWebService {

  @PathParam("componentInstanceId")
  private String componentInstanceId;

  /*
   * (non-Javadoc)
   * @see com.silverpeas.web.RESTWebService#getComponentId()
   */
  @Override
  public String getComponentId() {
    return componentInstanceId;
  }

  @Override
  protected String getResourceBasePath() {
    return GALLERY_BASE_URI;
  }

  /**
   * Converts the album into its corresponding web entity.
   * @param album the album.
   * @return the corresponding photo entity.
   */
  AlbumEntity asWebEntity(AlbumDetail album) {
    checkNotFoundStatus(album);
    AlbumEntity albumEntity = AlbumEntity.createFrom(album, getUserPreferences().getLanguage())
        .withURI(getUri().getRequestUri())
        .withParentURI(GalleryResourceURIs.buildAlbumURI(album.getFatherPK()));
    for (Media media : album.getMedia()) {
      if (hasUserMediaAccess(media)) {
        albumEntity.addMedia(asWebEntity(media, album));
      }
    }
    return albumEntity;
  }

  /**
   * Converts the photo into its corresponding web entity.
   * @param media the photo to convert.
   * @param album the album of the photo.
   * @return the corresponding photo entity.
   */
  private AbstractMediaEntity asWebEntity(Media media, AlbumDetail album) {
    final AbstractMediaEntity entity;
    switch (media.getType()) {
      case Photo:
        entity = PhotoEntity.createFrom(media.getPhoto())
            .withNormalUrl(GalleryResourceURIs.buildMediaContentURI(media, MediaResolution.NORMAL))
            .withPreviewUrl(GalleryResourceURIs.buildMediaContentURI(media, MediaResolution.PREVIEW))
            .withThumbUrl(URI.create(media.getApplicationThumbnailUrl(MediaResolution.SMALL)));
        break;
      case Video:
        entity = VideoEntity.createFrom(media.getVideo())
            .withThumbUrl(URI.create(media.getApplicationThumbnailUrl(MediaResolution.MEDIUM)));
        break;
      case Sound:
        entity = SoundEntity.createFrom(media.getSound())
            .withThumbUrl(URI.create(media.getApplicationThumbnailUrl(MediaResolution.MEDIUM)));
        break;
      case Streaming:
        entity = StreamingEntity.createFrom(media.getStreaming())
            .withOriginalUrl(URI.create(media.getStreaming().getHomepageUrl()))
            .withThumbUrl(URI.create(media.getApplicationThumbnailUrl(MediaResolution.MEDIUM)));
        break;
      default:
        throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
    }
    if (media.getInternalMedia() != null) {
      entity.withOriginalUrl(
          GalleryResourceURIs.buildMediaContentURI(media, MediaResolution.ORIGINAL));
    }
    return entity.withURI(GalleryResourceURIs.buildMediaInAlbumURI(album, media)).withParentURI(
        GalleryResourceURIs.buildAlbumURI(album));
  }

  /**
   * Centralization of getting of media data.
   * @param expectedMediaType expected media type
   * @param albumId the identifier of the album in which the media must exist
   * @param mediaId the identifier of the expected media
   * @return the corresponding media entity.
   */
  AbstractMediaEntity getMediaEntity(final MediaType expectedMediaType, final String albumId,
      final String mediaId) {
    try {
      final AlbumDetail album = getMediaService().getAlbum(new NodePK(albumId, getComponentId()));
      final Media media = getMediaService().getMedia(new MediaPK(mediaId, getComponentId()));
      checkNotFoundStatus(media);
      verifyUserMediaAccess(media);
      verifyMediaIsInAlbum(media, album);
      // Verifying the physical file exists and that the type of media is the one expected
      if (media.getInternalMedia() != null) {
        checkMediaExistsWithRequestedMimeType(expectedMediaType, media, MediaResolution.PREVIEW);
      }
      // Getting the web entity
      return asWebEntity(media, album);
    } catch (final WebApplicationException ex) {
      throw ex;
    } catch (final Exception ex) {
      throw new WebApplicationException(ex, Status.SERVICE_UNAVAILABLE);
    }
  }

  /**
   * Gets the media and verifies the user rights.
   * @param expectedMediaType expected media type
   * @param mediaId the media identifier
   * @param requestedMediaResolution requested media resolution
   * @param size a specific size applied on requested resolution
   * @return the requested media.
   */
  private Pair<Media, SilverpeasFile> getCheckedMedia(final MediaType expectedMediaType,
      final String mediaId, final MediaResolution requestedMediaResolution, final String size) {
    try {
      final Media media = getMediaService().getMedia(new MediaPK(mediaId, getComponentId()));
      checkNotFoundStatus(media);
      verifyUserMediaAccess(media);
      // Adjusting the resolution according to the user rights
      MediaResolution mediaResolution = getUserMediaResolution(requestedMediaResolution, media);
      // Verifying the physical file exists and that the type of media is the one expected
      final SilverpeasFile file = media.getFile(mediaResolution, size);
      if (!file.exists() || expectedMediaType != media.getType()) {
        throw new WebApplicationException(Status.NOT_FOUND);
      }
      return Pair.of(media, file);
    } catch (final WebApplicationException ex) {
      throw ex;
    } catch (final Exception ex) {
      throw new WebApplicationException(ex, Status.SERVICE_UNAVAILABLE);
    }
  }

  /**
   * Gets the user resolution according to its rights and the resolution requested.
   * @param requestedMediaResolution the requested media resolution.
   * @param media the media.
   * @return a {@link MediaResolution} instance.
   */
  private MediaResolution getUserMediaResolution(final MediaResolution requestedMediaResolution,
      final Media media) {
    MediaResolution mediaResolution = MediaResolution.ORIGINAL;
    if (media.getType().isPhoto()) {
      mediaResolution = requestedMediaResolution;
      if (MediaResolution.ORIGINAL == requestedMediaResolution && !isUserPrivileged() &&
          !media.isDownloadable()) {
        mediaResolution = MediaResolution.PREVIEW;
      }
    }
    return mediaResolution;
  }

  /**
   * Centralization of getting of media content.
   * @param expectedMediaType expected media type
   * @param mediaId the media identifier
   * @param requestedMediaResolution requested media resolution
   * @param size a specific size applied on requested resolution
   * @return the response.
   */
  Response getMediaContent(final MediaType expectedMediaType, final String mediaId,
      final MediaResolution requestedMediaResolution, final String size) {
    try {
      final Pair<Media, SilverpeasFile> checkedMedia =
          getCheckedMedia(expectedMediaType, mediaId, requestedMediaResolution, size);
      final Media media = checkedMedia.getLeft();
      final SilverpeasFile file = checkedMedia.getRight();
      return FileResponse.fromRest(getHttpServletRequest(), getHttpServletResponse())
          .forceMimeType(((InternalMedia) media).getFileMimeType().getMimeType())
          .forceFileId(mediaId)
          .silverpeasFile(file)
          .build();
    } catch (final WebApplicationException ex) {
      throw ex;
    } catch (final Exception ex) {
      throw new WebApplicationException(ex, Status.SERVICE_UNAVAILABLE);
    }
  }

  /**
   * Centralization of getting of media embed.
   * @param expectedMediaType expected media type
   * @param mediaId the media identifier
   * @param requestedMediaResolution requested media resolution
   */
  View getMediaEmbed(final MediaType expectedMediaType, final String mediaId,
      final MediaResolution requestedMediaResolution) {
    try {
      final Media media = getMediaService().getMedia(new MediaPK(mediaId, getComponentId()));
      checkNotFoundStatus(media);
      verifyUserMediaAccess(media);
      // Adjusting the resolution according to the user rights
      MediaResolution mediaResolution = MediaResolution.PREVIEW;
      if (requestedMediaResolution.getWidth() != null) {
        mediaResolution = requestedMediaResolution;
      }
      // Verifying the physical file exists and that the type of media is the one expected
      checkMediaExistsWithRequestedMimeType(expectedMediaType, media, mediaResolution);

      final Definition definition;
      MediaType mediaType = media.getType();
      if (mediaType == MediaType.Video) {
        definition = media.getVideo().getDefinition();
      } else {
        definition = Definition.of(mediaResolution.getWidth(), mediaResolution.getHeight());
      }

      getHttpServletRequest().setAttribute("mediaUrl", media.getApplicationOriginalUrl());
      getHttpServletRequest().setAttribute("posterUrl", media.getApplicationThumbnailUrl(mediaResolution));
      getHttpServletRequest().setAttribute("playerType", media.getType().getName());
      getHttpServletRequest().setAttribute("mimeType", media.getInternalMedia().getFileMimeType().getMimeType());
      getHttpServletRequest().setAttribute("definition", definition);
      getHttpServletRequest().setAttribute("backgroundColor", getHttpServletRequest().getParameter("backgroundColor"));
      getHttpServletRequest().setAttribute("autoPlay", getHttpServletRequest().getParameter("autoPlay"));

      return new View("/media/jsp/embed.jsp");
    } catch (final WebApplicationException ex) {
      throw ex;
    } catch (final Exception ex) {
      throw new WebApplicationException(ex, Status.SERVICE_UNAVAILABLE);
    }
  }

  /**
   * Verifies the physical file exists and that the type of media is the one expected.
   * @param requestedMediaType the requested media type (from the request).
   * @param media the media to verify.
   * @param mediaResolution the media resolution to get (from the service).
   */
  private void checkMediaExistsWithRequestedMimeType(final MediaType requestedMediaType,
      final Media media, final MediaResolution mediaResolution) {
    final SilverpeasFile file = media.getFile(mediaResolution);
    if (!file.exists() || requestedMediaType != media.getType()) {
      throw new WebApplicationException(Status.NOT_FOUND);
    }
  }

  /**
   * Centralization of getting video media thumbnail.
   * @param mediaId the media identifier
   * @param thumbnailId the thumbnail identifier
   * @param sizeDirective the size directive with pattern (optional)
   * @return the response.
   */
  Response getMediaThumbnail(final String mediaId,
      final String thumbnailId, final String sizeDirective) {
    try {
      final Media media = getMediaService().getMedia(new MediaPK(mediaId, getComponentId()));
      checkNotFoundStatus(media);
      verifyUserMediaAccess(media);
      // Verifying the physical file exists
      String filename = ThumbnailPeriod.fromIndex(thumbnailId).getFilename();
      if (StringUtil.isDefined(sizeDirective)) {
        filename = sizeDirective + "/" + filename;
      }
      final SilverpeasFile thumbFile =
          SilverpeasFileProvider.getFile(FileUtils
              .getFile(Media.BASE_PATH.getPath(), media.getComponentInstanceId(),
                  media.getWorkspaceSubFolderName(), filename).getPath());
      if (!thumbFile.exists()) {
        throw new WebApplicationException(Status.NOT_FOUND);
      }

      return loadFileContent(thumbFile)
          .header("Content-Type", thumbFile.getMimeType())
          .header("Content-Length", thumbFile.length())
          .header("Content-Disposition", "inline; filename=\"" + thumbFile.getName() + "\"")
          .build();
    } catch (final WebApplicationException ex) {
      throw ex;
    } catch (final Exception ex) {
      throw new WebApplicationException(ex, Status.SERVICE_UNAVAILABLE);
    }
  }

  /**
   * Indicates if the current user is a privileged one.
   * @return true if user has privileged
   */
  private boolean isUserPrivileged() {
    Collection<SilverpeasRole> userRoles = getUserRoles();
    return EnumSet.of(SilverpeasRole.ADMIN, SilverpeasRole.PUBLISHER, SilverpeasRole.WRITER,
        SilverpeasRole.PRIVILEGED_USER).stream().anyMatch(userRoles::contains);
  }

  /**
   * Centralization
   * @param object any object
   */
  void checkNotFoundStatus(Object object) {
    boolean isNotFound = false;
    if (object == null) {
      isNotFound = true;
    } else if (object instanceof Media) {
      Media media = (Media) object;
      switch (media.getType()) {
        case Photo:
          isNotFound = media.getPhoto() == null;
          break;
        case Video:
          isNotFound = media.getVideo() == null;
          break;
        case Sound:
          isNotFound = media.getSound() == null;
          break;
        case Streaming:
          isNotFound = media.getStreaming() == null;
          break;
        default:
          isNotFound = true;
      }
    }
    if (isNotFound) {
      throw new WebApplicationException(Status.NOT_FOUND);
    }
  }

  /**
   * Centralization
   * @param media the media to check access
   * @return true if user has media access
   */
  private boolean hasUserMediaAccess(Media media) {
    return media.canBeAccessedBy(getUser());
  }

  /**
   * Verifying that the authenticated user is authorized to view the given media.
   * @param media a media for which the access has to be verified.
   * @throws javax.ws.rs.WebApplicationException if user is not authorized to view the media
   */
  void verifyUserMediaAccess(Media media) {
    if (!hasUserMediaAccess(media)) {
      throw new WebApplicationException(Response.Status.FORBIDDEN);
    }
  }

  /**
   * @throws javax.ws.rs.WebApplicationException if the given media is not included in the given
   * album.
   */
  private void verifyMediaIsInAlbum(Media media, AlbumDetail album) {
    if (!album.getMedia().contains(media)) {
      throw new WebApplicationException(Response.Status.FORBIDDEN);
    }
  }

  private Response.ResponseBuilder loadFileContent(final File file) {
    StreamingOutput streamingOutput = output -> {
      try (final InputStream mediaStream = FileUtils.openInputStream(file)) {
        IOUtils.copy(mediaStream, output);
      } catch (IOException e) {
        throw new WebApplicationException(e, Status.NOT_FOUND);
      }
    };
    return Response.ok(streamingOutput);
  }

  /**
   * @return gallery media service layer
   */
  GalleryService getMediaService() {
    return MediaServiceProvider.getMediaService();
  }
}