diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index 8548612dc..dac19a4fa 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -3,7 +3,6 @@ package controllers import java.util.{Base64, UUID} import akka.http.scaladsl.model.Uri -import javax.inject.Inject import controllers.sugar.Bakery import db.ModelService import db.impl.OrePostgresDriver.api._ @@ -22,7 +21,7 @@ import ore.rest.ProjectApiKeyTypes._ import ore.rest.{OreRestfulApi, OreWrites} import ore.{OreConfig, OreEnv} import play.api.cache.AsyncCacheApi -import play.api.i18n.MessagesApi +import play.api.i18n.{Lang, Messages, MessagesApi} import util.StatusZ import util.functional.{EitherT, OptionT, Id} import util.instances.future._ @@ -173,7 +172,8 @@ final class ApiController @Inject()(api: OreRestfulApi, } } - private def error(key: String, error: String) = Json.obj("errors" -> Map(key -> List(this.messagesApi(error)))) + private def error(key: String, error: String)(implicit messages: Messages) = + Json.obj("errors" -> Map(key -> List(messages(error)))) def deployVersion(version: String, pluginId: String, name: String): Action[AnyContent] = ProjectAction(pluginId).async { implicit request => version match { diff --git a/app/controllers/OreBaseController.scala b/app/controllers/OreBaseController.scala index c7ba032a1..4dfa37df7 100755 --- a/app/controllers/OreBaseController.scala +++ b/app/controllers/OreBaseController.scala @@ -45,7 +45,7 @@ abstract class OreBaseController(implicit val env: OreEnv, implicit override val users: UserBase = this.service.getModelBase(classOf[UserBase]) implicit override val projects: ProjectBase = this.service.getModelBase(classOf[ProjectBase]) implicit override val organizations: OrganizationBase = this.service.getModelBase(classOf[OrganizationBase]) - implicit val lang: Lang = Lang.defaultLang + override val signOns: ModelAccess[SignOn] = this.service.access[SignOn](classOf[SignOn]) override def notFound(implicit request: OreRequest[_]) = NotFound(views.html.errors.notFound()) diff --git a/app/controllers/Organizations.scala b/app/controllers/Organizations.scala index 3da73c48e..527871019 100755 --- a/app/controllers/Organizations.scala +++ b/app/controllers/Organizations.scala @@ -46,7 +46,7 @@ class Organizations @Inject()(forms: OreForms, */ def showCreator(): Action[AnyContent] = UserLock().async { implicit request => request.user.ownedOrganizations.size.map { size => - if (size >= this.createLimit) Redirect(ShowHome).withError(this.messagesApi("error.org.createLimit", this.createLimit)) + if (size >= this.createLimit) Redirect(ShowHome).withError(request.messages.apply("error.org.createLimit", this.createLimit)) else { Ok(views.createOrganization()) } diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index 300454af8..2765575aa 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -175,7 +175,7 @@ final class Reviews @Inject()(data: DataHelper, userId = id, originId = requestUser.id.get, notificationType = NotificationTypes.VersionReviewed, - message = messagesApi("notification.project.reviewed", project.slug, version.versionString) + messageArgs = List("notification.project.reviewed", project.slug, version.versionString) ) } } map (notificationTable ++= _) flatMap (service.DB.db.run(_)) // Batch insert all notifications diff --git a/app/controllers/Users.scala b/app/controllers/Users.scala index 53e71d5ee..e7e8fca68 100755 --- a/app/controllers/Users.scala +++ b/app/controllers/Users.scala @@ -192,7 +192,7 @@ class Users @Inject()(fakeUser: FakeUser, tagline <- bindFormEitherT[Future](this.forms.UserTagline)(_ => BadRequest) } yield { if (tagline.length > maxLen) { - Redirect(ShowUser(user)).flashing("error" -> this.messagesApi("error.tagline.tooLong", maxLen)) + Redirect(ShowUser(user)).flashing("error" -> request.messages.apply("error.tagline.tooLong", maxLen)) } else { user.setTagline(tagline) Redirect(ShowUser(user)) diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index 1e89e7c6b..6d22d18d1 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -659,7 +659,7 @@ class Projects @Inject()(stats: StatTracker, getProject(author, slug).map { project => this.projects.delete(project) UserActionLogger.log(request, LoggedAction.ProjectVisibilityChange, project.id.getOrElse(-1), "null", project.visibility.nameKey) - Redirect(ShowHome).withSuccess(this.messagesApi("project.deleted", project.name)) + Redirect(ShowHome).withSuccess(request.messages.apply("project.deleted", project.name)) }.merge } } @@ -676,7 +676,7 @@ class Projects @Inject()(stats: StatTracker, val comment = this.forms.NeedsChanges.bindFromRequest.get.trim data.project.setVisibility(VisibilityTypes.SoftDelete, comment, request.user.id.get).map { _ => UserActionLogger.log(request.request, LoggedAction.ProjectVisibilityChange, data.project.id.getOrElse(-1), VisibilityTypes.SoftDelete.nameKey, data.project.visibility.nameKey) - Redirect(ShowHome).withSuccess(this.messagesApi("project.deleted", data.project.name)) + Redirect(ShowHome).withSuccess(request.messages.apply("project.deleted", data.project.name)) } } diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index fb771f402..e4695e982 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -26,7 +26,7 @@ import ore.project.io.{DownloadTypes, InvalidPluginFileException, PluginFile, Pl import ore.{OreConfig, OreEnv, StatTracker} import play.api.Logger import play.api.cache.AsyncCacheApi -import play.api.i18n.MessagesApi +import play.api.i18n.{Lang, MessagesApi} import play.api.libs.json.Json import play.api.mvc.{Action, AnyContent, Request, Result} import play.filters.csrf.CSRF @@ -518,6 +518,7 @@ class Versions @Inject()(stats: StatTracker, ProjectAction(author, slug).async { request => val dlType = downloadType.flatMap(i => DownloadTypes.values.find(_.id == i)).getOrElse(DownloadTypes.UploadedFile) implicit val r: OreRequest[AnyContent] = request.request + implicit val lang: Lang = request.lang val project = request.data.project getVersion(project, target) .filterOrElse(v => !v.isReviewed, Redirect(ShowProject(author, slug)).withError("error.plugin.stateChanged")) diff --git a/app/controllers/sugar/Actions.scala b/app/controllers/sugar/Actions.scala index 7f6f2c818..5b8b79b65 100644 --- a/app/controllers/sugar/Actions.scala +++ b/app/controllers/sugar/Actions.scala @@ -16,6 +16,7 @@ import ore.permission.{EditPages, EditSettings, HideProjects, Permission} import play.api.cache.AsyncCacheApi import play.api.mvc.Results.{Redirect, Unauthorized} import play.api.mvc._ +import play.api.i18n.Messages import security.spauth.SingleSignOnConsumer import slick.jdbc.JdbcBackend @@ -203,7 +204,11 @@ trait Actions extends Calls with ActionHelpers { def transform[A](request: Request[A]): Future[OreRequest[A]] = { implicit val service: ModelService = users.service - HeaderData.of(request).map(new OreRequest(_, request)) + HeaderData.of(request).map { data => + val requestWithLang = + data.currentUser.flatMap(_.lang).fold(request)(lang => request.addAttr(Messages.Attrs.CurrentLang, lang)) + new OreRequest(data, requestWithLang) + } } } diff --git a/app/db/impl/OrePostgresDriver.scala b/app/db/impl/OrePostgresDriver.scala index 3997376a0..952bc0709 100644 --- a/app/db/impl/OrePostgresDriver.scala +++ b/app/db/impl/OrePostgresDriver.scala @@ -24,6 +24,7 @@ import ore.user.notification.NotificationTypes import ore.user.notification.NotificationTypes.NotificationType import slick.ast.BaseTypedType import slick.jdbc.JdbcType +import play.api.i18n.Lang /** * Custom Postgres driver to support array data and custom type mappings. @@ -55,6 +56,7 @@ trait OrePostgresDriver extends ExPostgresProfile with PgArraySupport with PgAgg implicit val visibiltyTypeMapper : JdbcType[Visibility] with BaseTypedType[Visibility] = MappedJdbcType.base[Visibility, Int](_.id, VisibilityTypes.withId) implicit val loggedActionMapper : JdbcType[LoggedAction] with BaseTypedType[LoggedAction] = MappedJdbcType.base[LoggedAction, Int](_.value, LoggedAction.withValue) implicit val loggedActionContextMapper : JdbcType[LoggedActionContext] with BaseTypedType[LoggedActionContext] = MappedJdbcType.base[LoggedActionContext, Int](_.value, LoggedActionContext.withValue) + implicit val langTypeMapper : JdbcType[Lang] with BaseTypedType[Lang] = MappedJdbcType.base[Lang, String](_.toLocale.toLanguageTag, Lang.apply) } } diff --git a/app/db/impl/access/OrganizationBase.scala b/app/db/impl/access/OrganizationBase.scala index 129c6b1d4..efe9b2101 100644 --- a/app/db/impl/access/OrganizationBase.scala +++ b/app/db/impl/access/OrganizationBase.scala @@ -25,7 +25,6 @@ class OrganizationBase(override val service: ModelService, extends ModelBase[Organization] { override val modelClass: Class[Organization] = classOf[Organization] - implicit val lang: Lang = Lang.defaultLang val Logger = play.api.Logger("Organizations") @@ -88,7 +87,7 @@ class OrganizationBase(override val service: ModelService, user.sendNotification(Notification( originId = org.id.get, notificationType = NotificationTypes.OrganizationInvite, - message = this.messages("notification.organization.invite", role.roleType.title, org.username) + messageArgs = List("notification.organization.invite", role.roleType.title, org.username) )) } }) diff --git a/app/db/impl/schema.scala b/app/db/impl/schema.scala index d5b5c2ddb..6a5b49481 100755 --- a/app/db/impl/schema.scala +++ b/app/db/impl/schema.scala @@ -24,6 +24,7 @@ import ore.project.io.DownloadTypes.DownloadType import ore.rest.ProjectApiKeyTypes.ProjectApiKeyType import ore.user.Prompts.Prompt import ore.user.notification.NotificationTypes.NotificationType +import play.api.i18n.Lang /* * Database schema definitions. Changes must be first applied as an evolutions @@ -234,9 +235,10 @@ class UserTable(tag: RowTag) extends ModelTable[User](tag, "users") with NameCol def joinDate = column[Timestamp]("join_date") def avatarUrl = column[String]("avatar_url") def readPrompts = column[List[Prompt]]("read_prompts") + def lang = column[Lang]("language") override def * = (id.?, createdAt.?, fullName.?, name, email.?, tagline.?, globalRoles, joinDate.?, - avatarUrl.?, readPrompts, pgpPubKey.?, lastPgpPubKeyUpdate.?, isLocked) <> ((User.apply _).tupled, + avatarUrl.?, readPrompts, pgpPubKey.?, lastPgpPubKeyUpdate.?, isLocked, lang.?) <> ((User.apply _).tupled, User.unapply) } @@ -323,11 +325,11 @@ class NotificationTable(tag: RowTag) extends ModelTable[Notification](tag, "noti def userId = column[Int]("user_id") def originId = column[Int]("origin_id") def notificationType = column[NotificationType]("notification_type") - def message = column[String]("message") + def messageArgs = column[List[String]]("message_args") def action = column[String]("action") def read = column[Boolean]("read") - override def * = (id.?, createdAt.?, userId, originId, notificationType, message, action.?, + override def * = (id.?, createdAt.?, userId, originId, notificationType, messageArgs, action.?, read) <> (Notification.tupled, Notification.unapply) } diff --git a/app/db/impl/table/ModelKeys.scala b/app/db/impl/table/ModelKeys.scala index 865fc4118..6fe3125dc 100755 --- a/app/db/impl/table/ModelKeys.scala +++ b/app/db/impl/table/ModelKeys.scala @@ -14,6 +14,7 @@ import ore.Colors.Color import ore.permission.role.RoleTypes.RoleType import ore.project.Categories.Category import ore.user.Prompts.Prompt +import play.api.i18n.Lang /** * Collection of String keys used for table bindings within Models. @@ -63,6 +64,7 @@ object ModelKeys { val JoinDate = new TimestampKey[User](_.joinDate, _.joinDate.orNull) val AvatarUrl = new StringKey[User](_.avatarUrl, _.avatarUrl.orNull) val ReadPrompts = new Key[User, List[Prompt]](_.readPrompts, _.readPrompts.toList) + val Language = new Key[User, Lang](_.lang, _.lang.orNull) // Organization val OrgOwnerId = new IntKey[Organization](_.userId, _.owner.userId) diff --git a/app/form/organization/OrganizationMembersUpdate.scala b/app/form/organization/OrganizationMembersUpdate.scala index 5edc590b8..83c9d261c 100644 --- a/app/form/organization/OrganizationMembersUpdate.scala +++ b/app/form/organization/OrganizationMembersUpdate.scala @@ -27,8 +27,6 @@ case class OrganizationMembersUpdate(override val users: List[Int], userUps: List[String], roleUps: List[String]) extends TOrganizationRoleSetBuilder { - implicit val lang: Lang = Lang.defaultLang - //noinspection ComparingUnrelatedTypes def saveTo(organization: Organization)(implicit cache: AsyncCacheApi, ex: ExecutionContext, messages: MessagesApi, users: UserBase): Unit = { if (!organization.isDefined) @@ -50,11 +48,12 @@ case class OrganizationMembersUpdate(override val users: List[Int], for (role <- this.build()) { val user = role.user dossier.addRole(role.copy(organizationId = orgId)) - user.flatMap { - _.sendNotification(Notification( + user.flatMap { user => + import user.langOrDefault + user.sendNotification(Notification( originId = orgId, notificationType = NotificationTypes.OrganizationInvite, - message = messages("notification.organization.invite", role.roleType.title, organization.name) + messageArgs = List("notification.organization.invite", role.roleType.title, organization.name) )) } } diff --git a/app/mail/EmailFactory.scala b/app/mail/EmailFactory.scala index 8e8d905fa..3705f5f88 100644 --- a/app/mail/EmailFactory.scala +++ b/app/mail/EmailFactory.scala @@ -16,11 +16,9 @@ final class EmailFactory @Inject()(override val messagesApi: MessagesApi, val AccountUnlocked = "email.accountUnlock" implicit val users: UserBase = this.service.getModelBase(classOf[UserBase]) - implicit val lang : Lang = Lang.defaultLang - def create(user: User, id: String)(implicit request: OreRequest[_]): Email = { - - Email( + import user.langOrDefault + Email( recipient = user.email.get, subject = this.messagesApi(s"$id.subject"), content = views.html.utils.email( diff --git a/app/models/admin/Review.scala b/app/models/admin/Review.scala index 2bb80a631..932e0287a 100644 --- a/app/models/admin/Review.scala +++ b/app/models/admin/Review.scala @@ -14,9 +14,10 @@ import play.api.libs.functional.syntax._ import play.twirl.api.Html import util.StringUtils import play.api.libs.json._ - import scala.concurrent.Future +import play.api.i18n.Messages + /** * Represents an approval instance of [[Project]] [[Version]]. @@ -133,7 +134,7 @@ case class Review(override val id: Option[Int] = None, * @param message */ case class Message(message: String, time: Long = System.currentTimeMillis(), action: String = "message") { - def getTime(implicit oreConfig: OreConfig): String = StringUtils.prettifyDateAndTime(new Timestamp(time)) + def getTime(implicit messages: Messages): String = StringUtils.prettifyDateAndTime(new Timestamp(time)) def isTakeover(): Boolean = action.equalsIgnoreCase("takeover") def isStop(): Boolean = action.equalsIgnoreCase("stop") def render(implicit oreConfig: OreConfig): Html = Page.Render(message) diff --git a/app/models/project/Project.scala b/app/models/project/Project.scala index e323a5478..011fb0ba3 100755 --- a/app/models/project/Project.scala +++ b/app/models/project/Project.scala @@ -38,6 +38,8 @@ import slick.lifted import slick.lifted.{Rep, TableQuery} import scala.concurrent.{ExecutionContext, Future} +import play.api.i18n.Messages + /** * Represents an Ore package. * @@ -690,7 +692,7 @@ case class Project(override val id: Option[Int] = None, * @param message */ case class Note(message: String, user: Int, time: Long = System.currentTimeMillis()) { - def getTime(implicit oreConfig: OreConfig): String = StringUtils.prettifyDateAndTime(new Timestamp(time)) + def getTime(implicit messages: Messages): String = StringUtils.prettifyDateAndTime(new Timestamp(time)) def render(implicit oreConfig: OreConfig): Html = Page.Render(message) } diff --git a/app/models/project/ProjectSettings.scala b/app/models/project/ProjectSettings.scala index 339b2729b..534cc9a95 100644 --- a/app/models/project/ProjectSettings.scala +++ b/app/models/project/ProjectSettings.scala @@ -49,7 +49,6 @@ case class ProjectSettings(override val id: Option[Int] = None, private var _forumSync: Boolean = true) extends OreModel(id, createdAt) with ProjectOwned { - implicit val lang: Lang = Lang.defaultLang override type M = ProjectSettings override type T = ProjectSettingsTable @@ -198,7 +197,7 @@ case class ProjectSettings(override val id: Option[Int] = None, userId = role.userId, originId = project.ownerId, notificationType = NotificationTypes.ProjectInvite, - message = messages("notification.project.invite", role.roleType.title, project.name)) + messageArgs = List("notification.project.invite", role.roleType.title, project.name)) } service.DB.db.run(TableQuery[NotificationTable] ++= notifications) // Bulk insert Notifications diff --git a/app/models/user/Notification.scala b/app/models/user/Notification.scala index c8246b130..94994ae3c 100644 --- a/app/models/user/Notification.scala +++ b/app/models/user/Notification.scala @@ -19,7 +19,8 @@ import scala.concurrent.{ExecutionContext, Future} * @param createdAt Instant of cretion * @param userId ID of User this notification belongs to * @param notificationType Type of notification - * @param message Message to display + * @param messageArgs The unlocalized message to display, with the + * parameters to use when localizing * @param action Action to perform on click * @param read True if notification has been read */ @@ -28,11 +29,13 @@ case class Notification(override val id: Option[Int] = None, override val userId: Int = -1, originId: Int, notificationType: NotificationType, - message: String, + messageArgs: List[String], action: Option[String] = None, private var read: Boolean = false) extends OreModel(id, createdAt) with UserOwned { + //TODO: Would be neat to have a NonEmptyList to get around guarding against this + require(messageArgs.nonEmpty, "Notification created with no message arguments") override type M = Notification override type T = NotificationTable diff --git a/app/models/user/User.scala b/app/models/user/User.scala index 1aa70e41a..d365afa1e 100644 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -31,6 +31,8 @@ import util.functional.OptionT import scala.concurrent.{ExecutionContext, Future} import scala.util.control.Breaks._ +import play.api.i18n.Lang + /** * Represents a Sponge user. * @@ -53,7 +55,8 @@ case class User(override val id: Option[Int] = None, private var _readPrompts: List[Prompt] = List(), private var _pgpPubKey: Option[String] = None, private var _lastPgpPubKeyUpdate: Option[Timestamp] = None, - private var _isLocked: Boolean = false) + private var _isLocked: Boolean = false, + private var _lang: Option[Lang] = None) extends OreModel(id, createdAt) with UserOwned with ScopeSubject @@ -248,6 +251,26 @@ case class User(override val id: Option[Int] = None, if (isDefined) update(Tagline) } + /** + * Returns this user's current language. + */ + def lang: Option[Lang] = _lang + + /** + * Returns this user's current language, or the default language if none + * was configured. + */ + implicit def langOrDefault: Lang = _lang.getOrElse(Lang.defaultLang) + + /** + * Sets this user's language. + * @param lang The new language. + */ + def setLang(lang: Option[Lang]) = { + this._lang = lang + if(isDefined) update(Language) + } + /** * Returns this user's global [[RoleType]]s. * @@ -385,6 +408,7 @@ case class User(override val id: Option[Int] = None, if (user != null) { this.setUsername(user.username) this.setEmail(user.email) + this.setLang(user.lang) user.avatarUrl.map { url => if (!url.startsWith("http")) { val baseUrl = config.security.get[String]("api.url") diff --git a/app/ore/project/NotifyWatchersTask.scala b/app/ore/project/NotifyWatchersTask.scala index 195de1065..b728a4c47 100644 --- a/app/ore/project/NotifyWatchersTask.scala +++ b/app/ore/project/NotifyWatchersTask.scala @@ -4,8 +4,6 @@ import db.impl.access.ProjectBase import models.project.{Project, Version} import models.user.Notification import ore.user.notification.NotificationTypes -import play.api.i18n.{Lang, MessagesApi} - import scala.concurrent.ExecutionContext /** @@ -14,21 +12,19 @@ import scala.concurrent.ExecutionContext * released. * * @param version New version - * @param messages MessagesApi instance * @param projects ProjectBase instance */ -case class NotifyWatchersTask(version: Version, project: Project, messages: MessagesApi)(implicit projects: ProjectBase, ec: ExecutionContext) +case class NotifyWatchersTask(version: Version, project: Project)(implicit projects: ProjectBase, ec: ExecutionContext) extends Runnable { - implicit val lang: Lang = Lang.defaultLang - def run(): Unit = { val notification = Notification( originId = project.ownerId, notificationType = NotificationTypes.NewProjectVersion, - message = messages("notification.project.newVersion", project.name, version.name), + messageArgs = List("notification.project.newVersion", project.name, version.name), action = Some(version.url(project)) ) + for { watchers <- project.watchers.all } yield { diff --git a/app/ore/project/factory/ProjectFactory.scala b/app/ore/project/factory/ProjectFactory.scala index 15ebddbd5..2ae84c74f 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -24,7 +24,7 @@ import ore.project.io.{InvalidPluginFileException, PluginFile, PluginUpload, Pro import ore.user.notification.NotificationTypes import org.spongepowered.plugin.meta.PluginMetadata import play.api.cache.SyncCacheApi -import play.api.i18n.{Lang, MessagesApi} +import play.api.i18n.Messages import security.pgp.PGPVerifier import util.StringUtils._ import util.functional.{EitherT, OptionT} @@ -55,11 +55,9 @@ trait ProjectFactory { val pgp: PGPVerifier = new PGPVerifier val dependencyVersionRegex: Regex = "^[0-9a-zA-Z\\.\\,\\[\\]\\(\\)-]+$".r - implicit val messages: MessagesApi implicit val config: OreConfig implicit val forums: OreDiscourseApi implicit val env: OreEnv = this.fileManager.env - implicit val lang: Lang = Lang.defaultLang var isPgpEnabled: Boolean = this.config.security.get[Boolean]("requirePgp") @@ -71,7 +69,7 @@ trait ProjectFactory { * @param owner Upload owner * @return Loaded PluginFile */ - def processPluginUpload(uploadData: PluginUpload, owner: User): PluginFile = { + def processPluginUpload(uploadData: PluginUpload, owner: User)(implicit messages: Messages): PluginFile = { val pluginFileName = uploadData.pluginFileName var signatureFileName = uploadData.signatureFileName @@ -112,7 +110,7 @@ trait ProjectFactory { def processSubsequentPluginUpload(uploadData: PluginUpload, owner: User, - project: Project)(implicit ec: ExecutionContext): EitherT[Future, String, PendingVersion] = { + project: Project)(implicit ec: ExecutionContext, messages: Messages): EitherT[Future, String, PendingVersion] = { val plugin = this.processPluginUpload(uploadData, owner) if (!plugin.meta.get.getId.equals(project.pluginId)) EitherT.leftT("error.version.invalidPluginId") @@ -296,7 +294,7 @@ trait ProjectFactory { user.sendNotification(Notification( originId = ownerId, notificationType = NotificationTypes.ProjectInvite, - message = messages("notification.project.invite", role.roleType.title, project.name) + messageArgs = List("notification.project.invite", role.roleType.title, project.name) )) } } @@ -366,7 +364,7 @@ trait ProjectFactory { val tags = spongeTag ++ forgeTag // Notify watchers - this.actorSystem.scheduler.scheduleOnce(Duration.Zero, NotifyWatchersTask(newVersion, project, messages)) + this.actorSystem.scheduler.scheduleOnce(Duration.Zero, NotifyWatchersTask(newVersion, project)) project.setLastUpdated(this.service.theTime) @@ -446,6 +444,5 @@ class OreProjectFactory @Inject()(override val service: ModelService, override val config: OreConfig, override val forums: OreDiscourseApi, override val cacheApi: SyncCacheApi, - override val messages: MessagesApi, override val actorSystem: ActorSystem) extends ProjectFactory diff --git a/app/ore/project/io/PluginFile.scala b/app/ore/project/io/PluginFile.scala index 70be4f576..7826721c6 100755 --- a/app/ore/project/io/PluginFile.scala +++ b/app/ore/project/io/PluginFile.scala @@ -10,8 +10,8 @@ import models.user.User import ore.user.UserOwned import org.apache.commons.codec.digest.DigestUtils import org.spongepowered.plugin.meta.{McModInfo, PluginMetadata} -import play.api.i18n.{Lang, MessagesApi} +import play.api.i18n.Messages import scala.collection.JavaConverters._ import scala.util.control.Breaks._ @@ -22,8 +22,6 @@ import scala.util.control.Breaks._ */ class PluginFile(private var _path: Path, val signaturePath: Path, val user: User) extends UserOwned { - implicit val lang: Lang = Lang.defaultLang - private val MetaFileName = "mcmod.info" private var _meta: Option[PluginMetadata] = None @@ -83,7 +81,7 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use * @return Result of parse */ @throws[InvalidPluginFileException] - def loadMeta()(implicit messages: MessagesApi): PluginMetadata = { + def loadMeta()(implicit messages: Messages): PluginMetadata = { var jarIn: JarInputStream = null try { // Find plugin JAR @@ -102,12 +100,12 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use } if (!metaFound) - throw InvalidPluginFileException("error.plugin.metaNotFound") + throw InvalidPluginFileException(messages("error.plugin.metaNotFound")) // Read the meta file val metaList = McModInfo.DEFAULT.read(jarIn).asScala.toList if (metaList.isEmpty) - throw InvalidPluginFileException("error.plugin.metaNotFound") + throw InvalidPluginFileException(messages("error.plugin.metaNotFound")) // Parse plugin meta info val meta = metaList.head @@ -131,7 +129,7 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use if (jarIn != null) jarIn.close() else - throw InvalidPluginFileException("error.plugin.unexpected") + throw InvalidPluginFileException(messages("error.plugin.unexpected")) } } @@ -141,7 +139,7 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use * @return InputStream of JAR */ @throws[IOException] - def newJarStream: InputStream = { + def newJarStream(implicit messages: Messages): InputStream = { if (this.path.toString.endsWith(".jar")) Files.newInputStream(this.path) else { @@ -150,7 +148,7 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use } } - private def findTopLevelJar(zip: ZipFile): ZipEntry = { + private def findTopLevelJar(zip: ZipFile)(implicit messages: Messages): ZipEntry = { if (this.path.toString.endsWith(".jar")) throw new Exception("Plugin is already JAR") @@ -168,7 +166,7 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use } if (pluginEntry == null) - throw InvalidPluginFileException("error.plugin.jarNotFound") + throw InvalidPluginFileException(messages("error.plugin.jarNotFound")) pluginEntry } diff --git a/app/security/spauth/SingleSignOnConsumer.scala b/app/security/spauth/SingleSignOnConsumer.scala index 8379df352..1fd9a80dc 100644 --- a/app/security/spauth/SingleSignOnConsumer.scala +++ b/app/security/spauth/SingleSignOnConsumer.scala @@ -1,7 +1,7 @@ package security.spauth import java.math.BigInteger -import java.net.{URLDecoder, URLEncoder} +import java.net.URLEncoder import java.security.SecureRandom import java.util.Base64 @@ -14,9 +14,14 @@ import play.api.http.Status import play.api.libs.ws.WSClient import util.functional.OptionT import util.instances.future._ +import util.syntax._ import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future} +import scala.util.Try + +import akka.http.scaladsl.model.Uri +import play.api.i18n.Lang /** * Manages authentication to Sponge services. @@ -116,45 +121,31 @@ trait SingleSignOnConsumer { } // decode payload - val decoded = URLDecoder.decode(new String(Base64.getMimeDecoder.decode(payload)), this.CharEncoding) + val query = Uri.Query(Base64.getMimeDecoder.decode(payload)) Logger.info("Decoded payload:") - Logger.info(decoded) + Logger.info(query.toString()) // extract info - val params = decoded.split('&') - var nonce: String = null - var externalId: Int = -1 - var username: String = null - var email: String = null - var avatarUrl: String = null - - for (param <- params) { - val data = param.split('=') - val value = if (data.length > 1) data(1) else null - data(0) match { - case "nonce" => nonce = value - case "external_id" => externalId = Integer.parseInt(value) - case "username" => username = value - case "email" => email = value - case "avatar_url" => avatarUrl = value - case _ => - } + val info = for { + nonce <- query.get("nonce") + externalId <- query.get("external_id").flatMap(s => Try(s.toInt).toOption) + username <- query.get("username") + email <- query.get("email") + } yield { + nonce -> SpongeUser(externalId, username, email, query.get("avatar_url"), query.get("language").flatMap(Lang.get)) } - if (externalId == -1 || username == null || email == null || nonce == null) { - Logger.info(" Incomplete payload.") - return OptionT.none[Future, SpongeUser] - } - - OptionT.liftF(isNonceValid(nonce)).subflatMap { - case false => - Logger.info(" Invalid nonce.") - None - case true => - val user = SpongeUser(externalId, username, email, Option(avatarUrl)) - Logger.info(" " + user) - Some(user) - } + OptionT + .fromOption[Future](info) + .semiFlatMap { case (nonce, user) => isNonceValid(nonce).tupleRight(user)} + .subflatMap { + case (false, _) => + Logger.info(" Invalid nonce.") + None + case (true, user) => + Logger.info(" " + user) + Some(user) + } } private def hmac_sha256(data: Array[Byte]): String = { diff --git a/app/security/spauth/SpongeAuthApi.scala b/app/security/spauth/SpongeAuthApi.scala index 84cb7535b..6ce91eb15 100644 --- a/app/security/spauth/SpongeAuthApi.scala +++ b/app/security/spauth/SpongeAuthApi.scala @@ -17,6 +17,7 @@ import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ import play.api.Configuration +import play.api.i18n.Lang /** * Interfaces with the SpongeAuth Web API @@ -36,7 +37,8 @@ trait SpongeAuthApi { (JsPath \ "id").read[Int] and (JsPath \ "username").read[String] and (JsPath \ "email").read[String] and - (JsPath \ "avatar_url").readNullable[String] + (JsPath \ "avatar_url").readNullable[String] and + (JsPath \ "language").readNullable[String].map(_.flatMap(Lang.get)) )(SpongeUser.apply _) /** diff --git a/app/security/spauth/SpongeUser.scala b/app/security/spauth/SpongeUser.scala index 7631e7f4f..28957f2c8 100644 --- a/app/security/spauth/SpongeUser.scala +++ b/app/security/spauth/SpongeUser.scala @@ -1,5 +1,7 @@ package security.spauth +import play.api.i18n.Lang + /** * Represents a Sponge user. * @@ -7,4 +9,4 @@ package security.spauth * @param username Username * @param email Email */ -case class SpongeUser(id: Int, username: String, email: String, avatarUrl: Option[String]) +case class SpongeUser(id: Int, username: String, email: String, avatarUrl: Option[String], lang: Option[Lang]) diff --git a/app/util/StringUtils.scala b/app/util/StringUtils.scala index 7c1ecac34..62e097b02 100644 --- a/app/util/StringUtils.scala +++ b/app/util/StringUtils.scala @@ -1,11 +1,11 @@ package util import java.nio.file.{Files, Path} -import java.text.{MessageFormat, SimpleDateFormat} +import java.text.{DateFormat, MessageFormat} import java.util.Date import db.impl.OrePostgresDriver.api._ -import ore.OreConfig +import play.api.i18n.Messages /** * Helper class for handling User input. @@ -18,8 +18,8 @@ object StringUtils { * @param date Date to format * @return Standard formatted date */ - def prettifyDate(date: Date)(implicit config: OreConfig): String - = new SimpleDateFormat(config.ore.get[String]("date-format")).format(date) + def prettifyDate(date: Date)(implicit messages: Messages): String = + DateFormat.getDateInstance(DateFormat.DEFAULT, messages.lang.locale).format(date) /** * Returns a URL readable string from the specified string. @@ -76,6 +76,6 @@ object StringUtils { * @param date Date to format * @return Standard formatted date */ - def prettifyDateAndTime(date: Date)(implicit config: OreConfig): String - = new SimpleDateFormat(config.ore.get[String]("date-and-time-format")).format(date) + def prettifyDateAndTime(date: Date)(implicit messages: Messages): String = + DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, messages.lang.locale).format(date) } diff --git a/app/views/users/notifications.scala.html b/app/views/users/notifications.scala.html index 96c040fc9..ab5e7038e 100755 --- a/app/views/users/notifications.scala.html +++ b/app/views/users/notifications.scala.html @@ -22,6 +22,14 @@ } } +@formatNotification(notification: Notification) = @{ + notification.messageArgs match { + case head :: Nil => Html(messages(head)) + case head :: tail => Html(messages(head, tail: _*)) + case Nil => throw new IllegalStateException("Got notification with no arguments") + } +} + @bootstrap.layout(messages("notification.plural")) { @@ -73,7 +81,7 @@

data-id="@notification.id.get"> @userAvatar(Some(origin.name), origin.avatarUrl, clazz = "user-avatar-s") - @notification.message + @formatNotification(notification) @if(!notification.isRead) { diff --git a/conf/application.conf.template b/conf/application.conf.template index 26b416333..79de637d6 100755 --- a/conf/application.conf.template +++ b/conf/application.conf.template @@ -74,8 +74,6 @@ security { # Ore configuration ore { - date-format = "MMM dd, yyyy" - date-and-time-format = "MMM dd, yyyy HH:mm" debug = false debug-level = 3 # Used in /admin/seed route. Run "gradle build" in OreTestPlugin before using that route diff --git a/conf/evolutions/default/95.sql b/conf/evolutions/default/95.sql new file mode 100644 index 000000000..82c090dfb --- /dev/null +++ b/conf/evolutions/default/95.sql @@ -0,0 +1,17 @@ +# --- !Ups + +ALTER TABLE users ADD COLUMN language VARCHAR(16); + +ALTER TABLE notifications ADD COLUMN message_args VARCHAR(255)[]; +UPDATE notifications SET message_args = ARRAY[message]; + +ALTER TABLE notifications DROP COLUMN message; + +# --- !Downs + +ALTER TABLE users DROP COLUMN language; + +ALTER TABLE notifications ADD COLUMN message VARCHAR(255); +UPDATE notifications SET message = message_args[0]; + +ALTER TABLE notifications DROP COLUMN message_args;