app/controllers/Support.scala (187 lines of code) (raw):
package controllers
import java.io.File
import java.net.URI
import java.util.UUID
import java.util.concurrent.TimeUnit
import com.amazonaws.services.s3.model._
import com.gu.pandomainauth.PanDomainAuthSettingsRefresher
import model.command.CommandError._
import model.command.{ExpireSectionContentCommand, UnexpireSectionContentCommand, UpdateTagCommand}
import model.{DenormalisedTag, Image, ImageAsset}
import okhttp3.{Headers, OkHttpClient, Request}
import org.joda.time.DateTime
import helpers.JodaDateTimeFormat._
import permissions.ModifySectionExpiryPermissionsCheck
import play.api.Logging
import play.api.libs.json.{JsString, Json}
import play.api.libs.ws.WSClient
import play.api.mvc.{BaseController, ControllerComponents}
import repositories.{ContentAPI, TagLookupCache, TagRepository}
import services.{AWS, Config, FetchError, ImageMetadataService, InvalidImage}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
import scala.jdk.CollectionConverters._
class Support(
val wsClient: WSClient,
override val controllerComponents: ControllerComponents,
val panDomainSettings: PanDomainAuthSettingsRefresher
)(
implicit ec: ExecutionContext
)
extends BaseController
with PanDomainAuthActions
with Logging {
private val httpClient = new OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).build
def checkFileExtension(image: File, filename: String): Either[String, File] = {
val fileExtension = if(filename.contains(".")) filename.substring(filename.lastIndexOf(".")) else ""
fileExtension match {
case ".jpg" => Right(image)
case ".jpeg" => Right(image)
case ".png" => Right(image)
case ".bmp" => Right(image) // BMP is not preferable, but is technically supported.
case _ => Left("Image must have a file extension of '.png' or '.jpg'")
}
}
def imageMetadata(imageUrl: String = "") = APIAuthAction {
ImageMetadataService.fetch(imageUrl) match {
case Right(imageMetadata) => Ok(Json.toJson(imageMetadata))
case Left(err: FetchError) => NotFound(err.errMsg)
case Left(err: InvalidImage) => UnprocessableEntity(err.errMsg)
}
}
def validateImageDimensions(image: File, requiredWidth: Option[Long], requiredHeight: Option[Long]): Either[String, File] = {
val dimensionsAreValid = (requiredWidth, requiredHeight) match {
case (None, None) => true
case (Some(width), None) => width >= ImageMetadataService.imageWidth(image)
case (None, Some(height)) => height >= ImageMetadataService.imageHeight(image)
case (Some(width), Some(height)) => width >= ImageMetadataService.imageWidth(image) && height >= ImageMetadataService.imageHeight(image)
}
if(dimensionsAreValid) Right(image)
else Left(s"Image must have dimensions w:${requiredWidth.getOrElse("Not Specified")}px h:${requiredHeight.getOrElse("Not Specified")}px")
}
def uploadLogo(filename: String) = APIAuthAction(parse.temporaryFile) { req =>
val picture = req.body
val requiredWidth = req.getQueryString("width").map(_.toLong)
val requiredHeight = req.getQueryString("height").map(_.toLong)
val imageValidationResult = for {
fileWithValidExtension <- checkFileExtension(picture.path.toFile, filename)
image <- validateImageDimensions(fileWithValidExtension, requiredWidth, requiredHeight)
} yield image
imageValidationResult.fold(
err => BadRequest(err),
file => {
val dateSlug = new DateTime().toString("dd/MMM/yyyy")
val logoPath = s"commercial/sponsor/${dateSlug}/${UUID.randomUUID}-${filename}"
val contentType = req.contentType
val objectMetadata = new ObjectMetadata()
contentType.foreach(objectMetadata.setContentType(_))
AWS.frontendStaticFilesS3Client.putObject(
new PutObjectRequest("static-theguardian-com", logoPath, file)
.withMetadata(objectMetadata)
)
val uploadedImageUrl = s"https://static.theguardian.com/${logoPath}"
ImageMetadataService.fetch(uploadedImageUrl) match {
case Right(imageMetadata) =>
val image = Image(imageMetadata.mediaId, List(
ImageAsset(
imageUrl = uploadedImageUrl,
width = imageMetadata.width,
height = imageMetadata.height,
mimeType = imageMetadata.mimeType
)
))
Ok(Json.toJson(image))
case Left(err: FetchError) => InternalServerError(err.errMsg)
case Left(err: InvalidImage) => UnprocessableEntity(err.errMsg)
}
})
}
def previewCapiProxy(path: String) = APIAuthAction { request =>
val url = s"${Config().capiPreviewIAMUrl}/$path?${request.rawQueryString}"
logger.info(s"Requesting CAPI preview -> $url")
val req = new Request.Builder()
.url(url)
.headers(Headers.of(ContentAPI.signer.addIAMHeaders(Map.empty, URI.create(url)).asJava))
.build
val resp = httpClient.newCall(req).execute
resp.code match {
case 200 => {
Ok(resp.body.string).as(JSON)
}
case c => {
BadRequest
}
}
}
def flexPathMigrationSpecificData = Action {
Ok(
Json.toJson(TagLookupCache.allTags.get.map(tag => tag.id.toString -> JsString(tag.path)).toMap)
)
}
def flexSlugMigrationSpecificData = Action {
Ok(
Json.toJson(TagLookupCache.allTags.get.map(tag => tag.id.toString -> JsString(tag.slug)).toMap)
)
}
def unexpireSectionContent = (APIAuthAction andThen ModifySectionExpiryPermissionsCheck()).async { req =>
implicit val username = Option(req.user.email)
req.body.asJson.map { json =>
val sectionId = (json \ "sectionId").as[Long]
UnexpireSectionContentCommand(sectionId).process().map{ result =>
result.map(t => Ok("Unexpiry Completed Successfully")) getOrElse BadRequest("Failed to trigger unexpiry")
} recover {
commandErrorAsResult
}
}.getOrElse {
Future.successful(BadRequest("Expecting sectionId in JSON"))
}
}
def expireSectionContent = (APIAuthAction andThen ModifySectionExpiryPermissionsCheck()).async { req =>
implicit val username = Option(req.user.email)
req.body.asJson.map { json =>
val sectionId = (json \ "sectionId").as[Long]
ExpireSectionContentCommand(sectionId).process().map { result =>
result.map(t => Ok("Expiry Completed Successfully")) getOrElse BadRequest("Failed to trigger expiry")
} recover {
commandErrorAsResult
}
}.getOrElse {
Future.successful(BadRequest("Expecting sectionId in JSON"))
}
}
// TODO delete this!
def unexpireTag = APIAuthAction { req =>
implicit val username = Option(req.user.email)
req.body.asJson.map { json =>
val tagId = (json \ "tagId").as[Long]
import repositories.SponsorshipOperations
try {
SponsorshipOperations.unexpirePaidContentTag(tagId)
Ok
} catch {
case NonFatal(e) => BadRequest(e.toString)
}
}.getOrElse {
BadRequest("Expecting tagId in JSON")
}
}
def fixDanglingParents = APIAuthAction.async { req =>
implicit val username = Option(req.user.email)
val knownTags = TagRepository.loadAllTags
var danglingParentsCount = 0
val futures = knownTags flatMap {tag =>
tag.parents.flatMap { parentId =>
if (!knownTags.exists(tag => tag.id == parentId)) {
logger.info(s"Tag ID: ${tag.id}, detected dangling parent $parentId")
val updatedTag = tag.copy(parents = tag.parents.filterNot(_.equals(parentId)))
Some(UpdateTagCommand(DenormalisedTag(updatedTag)).process().map {result =>
result getOrElse InternalServerError(s"Could not update tag: ${tag.id}")
danglingParentsCount += 1
})
} else None
}
}
Future.sequence(futures).map { _ =>
Ok(s"Removed $danglingParentsCount Dangling Parents")
}
}
}