diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..35a7317fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ + + +* Browser: `Google Chrome` + +* Operating System: `Windows 10` + +* Error message (if applicable): `404` + +* Page link: `https://ore.spongepowered.org/YOUR_LINK` + +* Steps to reproduce: + + * Step 1 + * Step 2 + * Step 3 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..f2e177297 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +sudo: false +notifications: + email: false + +language: scala +jdk: + - oraclejdk8 + - openjdk8 + +scala: + - 2.12.6 + +# Caching taken from https://www.scala-sbt.org/1.0/docs/Travis-CI-with-sbt.html#Caching +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt + +before_cache: + # Cleanup the cached directories to avoid unnecessary cache updates + - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete + - find $HOME/.sbt -name "*.lock" -print -delete + +script: + - sbt ++$TRAVIS_SCALA_VERSION compile diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 71048aa6c..000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,11 +0,0 @@ -Browser & Version: `changeme` - -Operating System: `changeme` - -Error message (if applicable): `changeme` - -Steps to reproduce: - -* Step 1 -* Step 2 -* Step 3 diff --git a/OreTestPlugin/build.gradle b/OreTestPlugin/build.gradle index 8a927f390..e9f6b5cb0 100644 --- a/OreTestPlugin/build.gradle +++ b/OreTestPlugin/build.gradle @@ -21,7 +21,7 @@ apply plugin: 'signing' // id 'org.spongepowered.plugin' version '0.8.2-SNAPSHOT' //} -group = 'se.walkercrou' +group = 'org.spongepowered.ore' version = '1.0.1' description = 'Ore test plugin' @@ -60,5 +60,5 @@ oreDeploy { } dependencies { - compile 'org.spongepowered:spongeapi:6.0.0-SNAPSHOT' + compile 'org.spongepowered:spongeapi:7.0.0' } diff --git a/README.md b/README.md index 16957a549..d6b2fcfb9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Ore +Ore [![Build Status](https://travis-ci.org/SpongePowered/Ore.svg?branch=master)](https://travis-ci.org/SpongePowered/Ore) === Repository software for Sponge plugins and Forge mods https://ore.spongepowered.org/ @@ -9,10 +9,6 @@ Ore is written in Scala using the [Play](https://www.playframework.com/) framewo Running Ore is relatively simple. -**With Activator** -* Download and install the latest [Activator](https://www.lightbend.com/activator/download) distribution. -* Execute `activator run` in the project root. - **With SBT** * Download and install the latest [SBT](http://www.scala-sbt.org/download.html) version. * Execute `sbt run` in the project root. diff --git a/app/Bootstrap.scala b/app/Bootstrap.scala index 27bcb7598..65b141876 100644 --- a/app/Bootstrap.scala +++ b/app/Bootstrap.scala @@ -6,7 +6,6 @@ import discourse.OreDiscourseApi import javax.inject.{Inject, Singleton} import ore.OreConfig import ore.project.ProjectTask -import ore.user.UserSyncTask import org.bouncycastle.jce.provider.BouncyCastleProvider /** @@ -17,21 +16,18 @@ trait Bootstrap { val modelService: ModelService val forums: OreDiscourseApi val config: OreConfig - val userSync: UserSyncTask val projectTask: ProjectTask val Logger = play.api.Logger("Bootstrap") Logger.info("Initializing Ore...") - val time = System.currentTimeMillis() + val time: Long = System.currentTimeMillis() this.modelService.start() this.forums.projects = this.modelService.getModelBase(classOf[ProjectBase]) this.forums.start() - this.userSync.start() - this.projectTask.start() if (this.config.security.get[Boolean]("requirePgp")) @@ -45,5 +41,4 @@ trait Bootstrap { class BootstrapImpl @Inject()(override val modelService: ModelService, override val forums: OreDiscourseApi, override val config: OreConfig, - override val userSync: UserSyncTask, override val projectTask: ProjectTask) extends Bootstrap diff --git a/app/ErrorHandler.scala b/app/ErrorHandler.scala index a45a13760..01dc36920 100755 --- a/app/ErrorHandler.scala +++ b/app/ErrorHandler.scala @@ -25,10 +25,10 @@ class ErrorHandler @Inject()(env: Environment, extends DefaultHttpErrorHandler(env, conf, sourceMapper, router) with I18nSupport { - override def onProdServerError(request: RequestHeader, exception: UsefulException) = { - implicit val session = request.session - implicit val requestImpl = request - implicit val headerData = HeaderData() // Empty HeaderData on error + override def onProdServerError(request: RequestHeader, exception: UsefulException): Future[Result] = { + implicit val session: Session = request.session + implicit val requestImpl: RequestHeader = request + implicit val headerData: HeaderData = HeaderData() // Empty HeaderData on error Future.successful { if (exception.cause.isInstanceOf[TimeoutException]) @@ -39,9 +39,9 @@ class ErrorHandler @Inject()(env: Environment, } override def onNotFound(request: RequestHeader, message: String): Future[Result] = { - implicit val session = request.session - implicit val requestImpl = request - implicit val headerData = HeaderData() // Empty HeaderData on error + implicit val session: Session = request.session + implicit val requestImpl: RequestHeader = request + implicit val headerData: HeaderData = HeaderData() // Empty HeaderData on error Future.successful(NotFound(views.html.errors.notFound())) } diff --git a/app/Module.scala b/app/Module.scala index 1873b0210..a3bd1a377 100644 --- a/app/Module.scala +++ b/app/Module.scala @@ -12,7 +12,7 @@ import security.spauth.{SingleSignOnConsumer, SpongeAuth, SpongeAuthApi, SpongeS /** The Ore Module */ class Module(environment: Environment, configuration: Configuration) extends AbstractModule { - def configure() = { + def configure(): Unit = { bind(classOf[OreRestfulApi]).to(classOf[OreRestfulServer]) bind(classOf[StatTracker]).to(classOf[OreStatTracker]) bind(classOf[ProjectFactory]).to(classOf[OreProjectFactory]) diff --git a/app/assets/stylesheets/_topbar.scss b/app/assets/stylesheets/_topbar.scss index bfd2c00b9..11ee2924d 100644 --- a/app/assets/stylesheets/_topbar.scss +++ b/app/assets/stylesheets/_topbar.scss @@ -173,3 +173,13 @@ $sp_logo_width: 200px; left: 0; } } + +.ore-nav { + display: inline-block; + width: 15px !important; + height: auto; + margin: 0; + padding: 0 12px 4px 7px; + background-color: transparent; + border-radius: 0; +} diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 697f0e68e..3cf2b86bf 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -548,3 +548,25 @@ select.form-control, input.form-control { .no-padding { padding: 0; } + +.filter-user { + background-color: rgba(250, 250, 10, 0.4); +} +.filter-project { + background-color: rgba(10, 10, 250, 0.4); +} +.filter-version { + background-color: rgba(10, 250, 10, 0.4); +} +.filter-page { + background-color: rgba(250, 10, 250, 0.4); +} +.filter-action { + background-color: rgba(10, 250, 250, 0.4); +} +.filter-subject { + background-color: rgba(175, 150, 250, 0.4); +} +.table-super-condensed td { + padding: 2px !important; +} diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index 322476276..b95befac0 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._ @@ -11,8 +10,8 @@ import db.impl.ProjectApiKeyTable import form.OreForms import javax.inject.Inject import models.api.ProjectApiKey -import models.user.User -import ore.permission.EditApiKeys +import models.user.{LoggedAction, User, UserActionLogger} +import ore.permission.{EditApiKeys, ReviewProjects} import ore.permission.role.RoleTypes import ore.permission.role.RoleTypes.RoleType import ore.project.factory.{PendingVersion, ProjectFactory} @@ -21,7 +20,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._ @@ -34,6 +33,8 @@ import slick.lifted.Compiled import scala.concurrent.{ExecutionContext, Future} +import db.access.ModelAccess + /** * Ore API (v1) */ @@ -54,7 +55,8 @@ final class ApiController @Inject()(api: OreRestfulApi, import writes._ val files = new ProjectFiles(this.env) - val projectApiKeys = this.service.access[ProjectApiKey](classOf[ProjectApiKey]) + val projectApiKeys: ModelAccess[ProjectApiKey] = this.service.access[ProjectApiKey](classOf[ProjectApiKey]) + val Logger = play.api.Logger("SSO") private def ApiResult(json: Option[JsValue]): Result = json.map(Ok(_)).getOrElse(NotFound) @@ -65,7 +67,7 @@ final class ApiController @Inject()(api: OreRestfulApi, * @return JSON view of projects */ def listProjects(version: String, categories: Option[String], sort: Option[Int], q: Option[String], - limit: Option[Int], offset: Option[Int]) = Action.async { + limit: Option[Int], offset: Option[Int]): Action[AnyContent] = Action.async { version match { case "v1" => this.api.getProjectList(categories, sort, q, limit, offset).map(Ok(_)) case _ => Future.successful(NotFound) @@ -79,14 +81,14 @@ final class ApiController @Inject()(api: OreRestfulApi, * @param pluginId Plugin ID of project * @return Project with Plugin ID */ - def showProject(version: String, pluginId: String) = Action.async { + def showProject(version: String, pluginId: String): Action[AnyContent] = Action.async { version match { case "v1" => this.api.getProject(pluginId).map(ApiResult) case _ => Future.successful(NotFound) } } - def createKey(version: String, pluginId: String) = + def createKey(version: String, pluginId: String): Action[AnyContent] = (Action andThen AuthedProjectActionById(pluginId) andThen ProjectPermissionAction(EditApiKeys)) async { implicit request => val projectId = request.data.project.id.get val res = for { @@ -101,11 +103,11 @@ final class ApiController @Inject()(api: OreRestfulApi, value = UUID.randomUUID().toString.replace("-", ""))) ) } yield Created(Json.toJson(pak)) - + UserActionLogger.log(request.request, LoggedAction.ProjectSettingsChanged, projectId, s"${request.user.name} created a new ApiKey", "" ) res.getOrElse(BadRequest) } - def revokeKey(version: String, pluginId: String) = + def revokeKey(version: String, pluginId: String): Action[AnyContent] = (AuthedProjectActionById(pluginId) andThen ProjectPermissionAction(EditApiKeys)) { implicit request => val res = for { key <- bindFormOptionT[Id](this.forms.ProjectApiKeyRevoke) @@ -114,7 +116,7 @@ final class ApiController @Inject()(api: OreRestfulApi, key.remove() Ok } - + UserActionLogger.log(request.request, LoggedAction.ProjectSettingsChanged, request.data.project.id.get, s"${request.user.name} removed an ApiKey", "") res.getOrElse(BadRequest) } @@ -129,13 +131,32 @@ final class ApiController @Inject()(api: OreRestfulApi, * @return List of versions */ def listVersions(version: String, pluginId: String, channels: Option[String], - limit: Option[Int], offset: Option[Int]) = Action.async { + limit: Option[Int], offset: Option[Int]): Action[AnyContent] = Action.async { version match { - case "v1" => this.api.getVersionList(pluginId, channels, limit, offset).map(Some.apply).map(ApiResult) + case "v1" => this.api.getVersionList(pluginId, channels, limit, offset, onlyPublic = true).map(Some.apply).map(ApiResult) case _ => Future.successful(NotFound) } } + /** + * Almost like [[listVersions()]] but more intended for internal use. Shows all versions, but need authentification. + * + * @param version API version string + * @param pluginId Project plugin ID + * @param channels Channels to get versions from + * @param limit Amount to take + * @param offset Amount to drop + * @return List of versions + */ + def listAllVersions(version: String, pluginId: String, channels: Option[String], + limit: Option[Int], offset: Option[Int]): Action[AnyContent] = + (AuthedProjectActionById(pluginId) andThen PermissionAction(ReviewProjects)).async { + version match { + case "v1" => this.api.getVersionList(pluginId, channels, limit, offset, onlyPublic = false).map(Some.apply).map(ApiResult) + case _ => Future.successful(NotFound) + } + } + /** * Shows the specified Project Version. * @@ -144,16 +165,17 @@ final class ApiController @Inject()(api: OreRestfulApi, * @param name Version name * @return JSON view of Version */ - def showVersion(version: String, pluginId: String, name: String) = Action.async { + def showVersion(version: String, pluginId: String, name: String): Action[AnyContent] = Action.async { version match { case "v1" => this.api.getVersion(pluginId, name).map(ApiResult) case _ => Future.successful(NotFound) } } - 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) = ProjectAction(pluginId).async { implicit request => + def deployVersion(version: String, pluginId: String, name: String): Action[AnyContent] = ProjectAction(pluginId).async { implicit request => version match { case "v1" => val projectData = request.data @@ -215,7 +237,7 @@ final class ApiController @Inject()(api: OreRestfulApi, } } - def listPages(version: String, pluginId: String, parentId: Option[Int]) = Action.async { + def listPages(version: String, pluginId: String, parentId: Option[Int]): Action[AnyContent] = Action.async { version match { case "v1" => this.api.getPages(pluginId, parentId).value.map(ApiResult) case _ => Future.successful(NotFound) @@ -230,7 +252,7 @@ final class ApiController @Inject()(api: OreRestfulApi, * @param offset Offset to drop * @return List of users */ - def listUsers(version: String, limit: Option[Int], offset: Option[Int]) = Action.async { + def listUsers(version: String, limit: Option[Int], offset: Option[Int]): Action[AnyContent] = Action.async { version match { case "v1" => this.api.getUserList(limit, offset).map(Ok(_)) case _ => Future.successful(NotFound) @@ -244,7 +266,7 @@ final class ApiController @Inject()(api: OreRestfulApi, * @param username Username of user * @return User with username */ - def showUser(version: String, username: String) = Action.async { + def showUser(version: String, username: String): Action[AnyContent] = Action.async { version match { case "v1" => this.api.getUser(username).map(ApiResult) case _ => Future.successful(NotFound) @@ -259,7 +281,7 @@ final class ApiController @Inject()(api: OreRestfulApi, * @param versionName Version of the plugin * @return Tags for the version of the plugin */ - def listTags(version: String, plugin: String, versionName: String) = Action.async { + def listTags(version: String, plugin: String, versionName: String): Action[AnyContent] = Action.async { version match { case "v1" => this.api.getTags(plugin, versionName).value.map(ApiResult) case _ => Future.successful(NotFound) @@ -280,10 +302,12 @@ final class ApiController @Inject()(api: OreRestfulApi, */ def showStatusZ = Action(Ok(this.status.json)) - def syncSso() = Action.async { implicit request => + def syncSso(): Action[AnyContent] = Action.async { implicit request => val confApiKey = this.config.security.get[String]("sso.apikey") val confSecret = this.config.security.get[String]("sso.secret") + Logger.debug("Sync Request received") + bindFormEitherT[Future](this.forms.SyncSso)(hasErrors => BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson))) .filterOrElse(_._3 == confApiKey, BadRequest("API Key not valid")) //_3 is apiKey .filterOrElse( @@ -291,8 +315,12 @@ final class ApiController @Inject()(api: OreRestfulApi, BadRequest("Signature not matched") ) .map(t => Uri.Query(Base64.getMimeDecoder.decode(t._1))) //_1 is sso - .semiFlatMap(q => this.users.get(q.get("external_id").get.toInt).value.tupleLeft(q)) + .semiFlatMap{q => + Logger.debug("Sync Payload: " + q) + this.users.get(q.get("external_id").get.toInt).value.tupleLeft(q) + } .map { case (query, optUser) => + Logger.debug("Sync user found: " + optUser.isDefined) optUser.foreach { user => val email = query.get("email") val username = query.get("username") diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 6b660d5ba..0867f369c 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -2,36 +2,36 @@ package controllers import java.sql.Timestamp import java.time.Instant -import javax.inject.Inject import controllers.sugar.Bakery import controllers.sugar.Requests.AuthRequest import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ import db.impl._ -import db.impl.schema.{ProjectSchema, ReviewSchema, VersionSchema} -import db.{ModelFilter, ModelSchema, ModelService} +import db.impl.schema.ProjectSchema +import db.{ModelFilter, ModelService} import form.OreForms +import javax.inject.Inject import models.admin.Review -import models.project._ -import models.user.LoggedActionModel +import models.project.{Tag, _} import models.user.role._ -import models.viewhelper.{HeaderData, OrganizationData, ProjectData, ScopedOrganizationData} +import models.user.{LoggedAction, LoggedActionModel, User, UserActionLogger} +import models.viewhelper.{HeaderData, OrganizationData, ScopedOrganizationData} import ore.Platforms.Platform import ore.permission._ import ore.permission.role.{Role, RoleTypes} import ore.permission.scope.GlobalScope import ore.project.Categories.Category import ore.project.{Categories, ProjectSortingStrategies} -import ore.{OreConfig, OreEnv, Platforms} -import play.api.Logger +import ore.{OreConfig, OreEnv, PlatformCategory, Platforms} import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi +import play.api.mvc.{Action, ActionBuilder, AnyContent} import security.spauth.SingleSignOnConsumer import util.DataHelper import util.functional.OptionT -import util.syntax._ import util.instances.future._ +import util.syntax._ import views.{html => views} import scala.concurrent.{ExecutionContext, Future} @@ -58,7 +58,7 @@ final class Application @Inject()(data: DataHelper, * @return External link page */ def linkOut(remoteUrl: String) = OreAction { implicit request => - implicit val headerData = request.data + implicit val headerData: HeaderData = request.data Ok(views.linkout(remoteUrl)) } @@ -85,17 +85,20 @@ final class Application @Inject()(data: DataHelper, query: Option[String], sort: Option[Int], page: Option[Int], - platform: Option[String]) = OreAction async { implicit request => + platformCategory: Option[String], + platform: Option[String]): Action[AnyContent] = OreAction async { implicit request => // Get categories and sorting strategy - val canHideProjects = request.data.globalPerm(HideProjects) val currentUserId = request.data.currentUser.flatMap(_.id).getOrElse(-1) val ordering = sort.flatMap(ProjectSortingStrategies.withId).getOrElse(ProjectSortingStrategies.Default) - // TODO platform filter is not implemented + val pcat = platformCategory.flatMap(p => PlatformCategory.getPlatformCategories.find(_.name.equalsIgnoreCase(p))) val pform = platform.flatMap(p => Platforms.values.find(_.name.equalsIgnoreCase(p)).map(_.asInstanceOf[Platform])) - // val platformFilter = pform.map(actions.platformFilter).getOrElse(ModelFilter.Empty) + + // get the categories being queried + val categoryPlatformNames: List[String] = pcat.toList.flatMap(_.getPlatforms.map(_.name)) + val platformNames: List[String] = pform.map(_.name).toList ::: categoryPlatformNames map(_.toLowerCase) val categoryList: Seq[Category] = categories.fold(Categories.fromString(""))(s => Categories.fromString(s)).toSeq val q = query.fold("%")(qStr => s"%${qStr.toLowerCase}%") @@ -104,47 +107,59 @@ final class Application @Inject()(data: DataHelper, val p = page.getOrElse(1) val offset = (p - 1) * pageSize - val projectQuery = queryProjectRV filter { case (p, u, v) => - (LiteralColumn(true) === canHideProjects) || - (p.visibility === VisibilityTypes.Public) || - (p.visibility === VisibilityTypes.New) || - ((p.userId === currentUserId) && (p.visibility =!= VisibilityTypes.SoftDelete)) - } filter { case (p, u, v) => - (LiteralColumn(0) === categoryList.length) || (p.category inSetBind categoryList) - } filter { case (p, u, v) => - (p.name.toLowerCase like q) || - (p.description.toLowerCase like q) || - (p.ownerName.toLowerCase like q) || - (p.pluginId.toLowerCase like q) - } sortBy { case (p, u, v) => - ordering.fn(p) - } drop offset take pageSize - - def queryProjects() = { - for { - projects <- service.DB.db.run(projectQuery.result) - tags <- Future.sequence(projects.map(_._3.tags)) - } yield { - projects zip tags map { case ((p, u, v), t) => - (p, u, v, t) + for { + tags <- service.DB.db.run(TableQuery[TagTable].filter(_.name.toLowerCase inSetBind platformNames).result) + result <- { + val versionIdsOnPlatform = tags.flatMap(_.versionIds.asInstanceOf[List[Long]]).map(_.toInt) + + val projectQuery = queryProjectRV.filter { case (p, u, v) => + (LiteralColumn(true) === canHideProjects) || + (p.visibility === VisibilityTypes.Public) || + (p.visibility === VisibilityTypes.New) || + ((p.userId === currentUserId) && (p.visibility =!= VisibilityTypes.SoftDelete)) + } filter { case (p, u, v) => + (LiteralColumn(0) === categoryList.length) || (p.category inSetBind categoryList) + } filter { case (p, u, v) => + if (platformNames.isEmpty) LiteralColumn(true) + else p.recommendedVersionId inSet versionIdsOnPlatform + } filter { case (p, u, v) => + (p.name.toLowerCase like q) || + (p.description.toLowerCase like q) || + (p.ownerName.toLowerCase like q) || + (p.pluginId.toLowerCase like q) + } sortBy { case (p, u, v) => + ordering.fn(p) + } drop offset take pageSize + + def queryProjects(): Future[Seq[(Project, User, Version, List[Tag])]] = { + for { + projects <- service.DB.db.run(projectQuery.result) + tags <- Future.sequence(projects.map(_._3.tags)) + } yield { + projects zip tags map { case ((p, u, v), t) => + (p, u, v, t) + } + } } - } - } - queryProjects() map { data => - val catList = if (categoryList.isEmpty || Categories.visible.toSet.equals(categoryList.toSet)) None else Some(categoryList) - Ok(views.home(data, catList, query.find(_.nonEmpty), p, ordering, pform)) + queryProjects() map { data => + val catList = if (categoryList.isEmpty || Categories.visible.toSet.equals(categoryList.toSet)) None else Some(categoryList) + Ok(views.home(data, catList, query.find(_.nonEmpty), p, ordering, pcat, pform)) + } + } + } yield { + result } - } + } - def showQueue() = showQueueWithPage(0) + def showQueue(): Action[AnyContent] = showQueueWithPage(0) /** * Shows the moderation queue for unreviewed versions. * * @return View of unreviewed versions. */ - def showQueueWithPage(page: Int) = (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => + def showQueueWithPage(page: Int): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => // TODO: Pages val limit = 50 val offset = page * limit @@ -206,7 +221,7 @@ final class Application @Inject()(data: DataHelper, for { (v, u) <- versionTable joinLeft userTable on (_.authorId === _.id) - c <- channelTable if v.channelId === c.id && v.isReviewed =!= true + c <- channelTable if v.channelId === c.id && v.isReviewed =!= true && v.isNonReviewed =!= true p <- projectTable if v.projectId === p.id && p.visibility =!= VisibilityTypes.SoftDelete ou <- userTable if p.userId === ou.id } yield { @@ -220,7 +235,7 @@ final class Application @Inject()(data: DataHelper, * * @return Flag overview */ - def showFlags() = FlagAction.async { implicit request => + def showFlags(): Action[AnyContent] = FlagAction.async { implicit request => for { flags <- this.service.access[Flag](classOf[Flag]).filterNot(_.isResolved) (users, projects) <- (Future.sequence(flags.map(_.user)), Future.sequence(flags.map(_.project))).parTupled @@ -245,16 +260,19 @@ final class Application @Inject()(data: DataHelper, * @param resolved Resolved state * @return Ok */ - def setFlagResolved(flagId: Int, resolved: Boolean) = FlagAction.async { implicit request => + def setFlagResolved(flagId: Int, resolved: Boolean): Action[AnyContent] = FlagAction.async { implicit request => this.service.access[Flag](classOf[Flag]).get(flagId).semiFlatMap { flag => users.current.value.map { user => flag.setResolved(resolved, user) + flag.user.map { flagCreater => + UserActionLogger.log(request, LoggedAction.ProjectFlagResolved, flag.projectId, s"Flag Resolved by ${user.fold("unknown")(_.name)}", s"Flagged by ${flagCreater.name}") + } Ok } }.getOrElse(NotFound) } - def showHealth() = (Authenticated andThen PermissionAction[AuthRequest](ViewHealth)) async { implicit request => + def showHealth(): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](ViewHealth)) async { implicit request => ( projects.filter(p => p.topicId === -1 || p.postId === -1), projects.filter(_.isTopicDirty), @@ -279,7 +297,7 @@ final class Application @Inject()(data: DataHelper, /** * Helper route to reset Ore. */ - def reset() = (Authenticated andThen PermissionAction[AuthRequest](ResetOre)) { implicit request => + def reset(): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](ResetOre)) { implicit request => this.config.checkDebug() this.data.reset() cache.removeAll() @@ -291,7 +309,7 @@ final class Application @Inject()(data: DataHelper, * * @return Redirect home */ - def seed(users: Int, projects: Int, versions: Int, channels: Int) = { + def seed(users: Int, projects: Int, versions: Int, channels: Int): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](SeedOre)) { implicit request => this.config.checkDebug() this.data.seed(users, projects, versions, channels) @@ -305,7 +323,7 @@ final class Application @Inject()(data: DataHelper, * * @return Redirect home */ - def migrate() = (Authenticated andThen PermissionAction[AuthRequest](MigrateOre)) { implicit request => + def migrate(): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](MigrateOre)) { implicit request => this.data.migrate() Redirect(ShowHome) } @@ -313,7 +331,7 @@ final class Application @Inject()(data: DataHelper, /** * Show the activities page for a user */ - def showActivities(user: String) = (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => + def showActivities(user: String): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => this.users.withName(user).semiFlatMap { u => val activities: Future[Seq[(Object, Option[Project])]] = u.id match { case None => Future.successful(Seq.empty) @@ -360,7 +378,7 @@ final class Application @Inject()(data: DataHelper, * Show stats * @return */ - def showStats() = (Authenticated andThen PermissionAction[AuthRequest](ViewStats)).async { implicit request => + def showStats(): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](ViewStats)).async { implicit request => /** * Query to get a count where columnDate is equal to the date @@ -398,22 +416,47 @@ final class Application @Inject()(data: DataHelper, } } - def showLog() = showLogWithPage(0) + def showLog(oPage: Option[Int], userFilter: Option[Int], projectFilter: Option[Int], versionFilter: Option[Int], pageFilter: Option[Int], + actionFilter: Option[Int], subjectFilter: Option[Int]): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](ViewLogs)).async { implicit request => + val pageSize = 50 + val page = oPage.getOrElse(1) + val offset = (page - 1) * pageSize + + val default = LiteralColumn(true) + + val logQuery = queryLog.filter { case (action) => + (action.userId === userFilter).getOrElse(default) && + (action.filterProject === projectFilter).getOrElse(default) && + (action.filterVersion === versionFilter).getOrElse(default) && + (action.filterPage === pageFilter).getOrElse(default) && + (action.filterAction === actionFilter).getOrElse(default) && + (action.filterSubject === subjectFilter).getOrElse(default) + }.sortBy { case (action) => + action.id.desc + }.drop(offset).take(pageSize) + + ( + service.DB.db.run(logQuery.result), + service.access[LoggedActionModel](classOf[LoggedActionModel]).size, + request.currentUser.get can ViewIp in GlobalScope + ).parMapN { (actions, size, canViewIP) => + Ok(views.users.admin.log(actions, pageSize, offset, page, size, userFilter, projectFilter, versionFilter, pageFilter, actionFilter, subjectFilter, canViewIP)) + } + } + + private def queryLog = { + val tableLoggedAction = TableQuery[LoggedActionViewTable] - def showLogWithPage(page: Int) = (Authenticated andThen PermissionAction[AuthRequest](ViewLogs)).async { implicit request => - val limit = 50 - val offset = page * limit for { - actions <- service.access[LoggedActionModel](classOf[LoggedActionModel]).filter(u => true, limit, offset) - size <- service.access[LoggedActionModel](classOf[LoggedActionModel]).size + (action) <- tableLoggedAction } yield { - Ok(views.users.admin.log(actions, limit, offset, page, size)) + (action) } } - def UserAdminAction = Authenticated andThen PermissionAction[AuthRequest](UserAdmin) + def UserAdminAction: ActionBuilder[AuthRequest, AnyContent] = Authenticated andThen PermissionAction[AuthRequest](UserAdmin) - def userAdmin(user: String) = UserAdminAction.async { implicit request => + def userAdmin(user: String): Action[AnyContent] = UserAdminAction.async { implicit request => this.users.withName(user).semiFlatMap { u => for { isOrga <- u.isOrganization @@ -436,7 +479,7 @@ final class Application @Inject()(data: DataHelper, }.getOrElse(notFound) } - def updateUser(userName: String) = UserAdminAction.async { implicit request => + def updateUser(userName: String): Action[AnyContent] = UserAdminAction.async { implicit request => this.users.withName(userName).map { user => bindFormOptionT[Future](this.forms.UserAdminUpdate).flatMap { case (thing, action, data) => import play.api.libs.json._ @@ -498,7 +541,7 @@ final class Application @Inject()(data: DataHelper, * * @return Show page */ - def showProjectVisibility() = (Authenticated andThen PermissionAction[AuthRequest](ReviewVisibility)) async { implicit request => + def showProjectVisibility(): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](ReviewVisibility)) async { implicit request => val projectSchema = this.service.getSchema(classOf[ProjectSchema]) for { diff --git a/app/controllers/OreBaseController.scala b/app/controllers/OreBaseController.scala index 2d9642fe6..4dfa37df7 100755 --- a/app/controllers/OreBaseController.scala +++ b/app/controllers/OreBaseController.scala @@ -1,12 +1,13 @@ package controllers import controllers.sugar.Requests.{AuthRequest, AuthedProjectRequest, OreRequest} -import controllers.sugar.{Actions, Bakery} +import controllers.sugar.{Actions, Bakery, Requests} import db.ModelService import db.access.ModelAccess import db.impl.VersionTable import db.impl.access.{OrganizationBase, ProjectBase, UserBase} -import models.project.{Project, Version} +import db.impl.OrePostgresDriver.api._ +import models.project.{Project, Version, VisibilityTypes} import models.user.SignOn import ore.{OreConfig, OreEnv} import play.api.cache.AsyncCacheApi @@ -20,8 +21,11 @@ import scala.language.higherKinds import controllers.OreBaseController.{BindFormEitherTPartiallyApplied, BindFormOptionTPartiallyApplied} import play.api.data.Form +import slick.jdbc.JdbcBackend import util.functional.{EitherT, Monad, OptionT} +import ore.permission.ReviewProjects + /** * Represents a Secured base Controller for this application. */ @@ -36,12 +40,12 @@ abstract class OreBaseController(implicit val env: OreEnv, with Actions with I18nSupport { - implicit val db = service.DB.db + implicit val db: JdbcBackend#DatabaseDef = service.DB.db 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.defaultLang + override val signOns: ModelAccess[SignOn] = this.service.access[SignOn](classOf[SignOn]) override def notFound(implicit request: OreRequest[_]) = NotFound(views.html.errors.notFound()) @@ -59,6 +63,12 @@ abstract class OreBaseController(implicit val env: OreEnv, def getProject(author: String, slug: String)(implicit request: OreRequest[_]): EitherT[Future, Result, Project] = this.projects.withSlug(author, slug).toRight(notFound) + private def versionFindFunc(versionString: String, canSeeHiden: Boolean): VersionTable => Rep[Boolean] = v => { + val versionMatches = v.versionString.toLowerCase === versionString.toLowerCase + val isVisible = if(canSeeHiden) true.bind else v.visibility === VisibilityTypes.Public + versionMatches && isVisible + } + /** * Gets a project with the specified versionString, or returns a notFound. * @@ -69,7 +79,7 @@ abstract class OreBaseController(implicit val env: OreEnv, */ def getVersion(project: Project, versionString: String) (implicit request: OreRequest[_]): EitherT[Future, Result, Version] - = project.versions.find(equalsIgnoreCase[VersionTable](_.versionString, versionString)).toRight(notFound) + = project.versions.find(versionFindFunc(versionString, request.data.globalPerm(ReviewProjects))).toRight(notFound) /** * Gets a version with the specified author, project slug and version string @@ -91,13 +101,13 @@ abstract class OreBaseController(implicit val env: OreEnv, def bindFormOptionT[F[_]] = new BindFormOptionTPartiallyApplied[F] - def OreAction = Action andThen oreAction + def OreAction: ActionBuilder[OreRequest, AnyContent] = Action andThen oreAction /** Ensures a request is authenticated */ - def Authenticated = Action andThen authAction + def Authenticated: ActionBuilder[AuthRequest, AnyContent] = Action andThen authAction /** Ensures a user's account is unlocked */ - def UserLock(redirect: Call = ShowHome) = Authenticated andThen userLock(redirect) + def UserLock(redirect: Call = ShowHome): ActionBuilder[AuthRequest, AnyContent] = Authenticated andThen userLock(redirect) /** * Retrieves, processes, and adds a [[Project]] to a request. @@ -106,7 +116,7 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param slug Project slug * @return Request with a project if found, NotFound otherwise. */ - def ProjectAction(author: String, slug: String) = OreAction andThen projectAction(author, slug) + def ProjectAction(author: String, slug: String): ActionBuilder[Requests.ProjectRequest, AnyContent] = OreAction andThen projectAction(author, slug) /** @@ -115,7 +125,7 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param pluginId The project's unique plugin ID * @return Request with a project if found, NotFound otherwise */ - def ProjectAction(pluginId: String) = OreAction andThen projectAction(pluginId) + def ProjectAction(pluginId: String): ActionBuilder[Requests.ProjectRequest, AnyContent] = OreAction andThen projectAction(pluginId) /** * Ensures a request is authenticated and retrieves, processes, and adds a @@ -125,12 +135,12 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param slug Project slug * @return Authenticated request with a project if found, NotFound otherwise. */ - def AuthedProjectAction(author: String, slug: String, requireUnlock: Boolean = false) = { + def AuthedProjectAction(author: String, slug: String, requireUnlock: Boolean = false): ActionBuilder[AuthedProjectRequest, AnyContent] = { val first = if (requireUnlock) UserLock(ShowProject(author, slug)) else Authenticated first andThen authedProjectAction(author, slug) } - def AuthedProjectActionById(pluginId: String, requireUnlock: Boolean = true) = { + def AuthedProjectActionById(pluginId: String, requireUnlock: Boolean = true): ActionBuilder[AuthedProjectRequest, AnyContent] = { val first = if (requireUnlock) UserLock(ShowProject(pluginId)) else Authenticated first andThen authedProjectActionById(pluginId) } @@ -141,7 +151,7 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param organization Organization to retrieve * @return Request with organization if found, NotFound otherwise */ - def OrganizationAction(organization: String) = OreAction andThen organizationAction(organization) + def OrganizationAction(organization: String): ActionBuilder[Requests.OrganizationRequest, AnyContent] = OreAction andThen organizationAction(organization) /** * Ensures a request is authenticated and retrieves and adds a @@ -150,7 +160,7 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param organization Organization to retrieve * @return Authenticated request with Organization if found, NotFound otherwise */ - def AuthedOrganizationAction(organization: String, requireUnlock: Boolean = false) = { + def AuthedOrganizationAction(organization: String, requireUnlock: Boolean = false): ActionBuilder[Requests.AuthedOrganizationRequest, AnyContent] = { val first = if (requireUnlock) UserLock(ShowUser(organization)) else Authenticated first andThen authedOrganizationAction(organization) } @@ -162,7 +172,7 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param username User to check * @return [[OreAction]] if has permission */ - def UserAction(username: String) = Authenticated andThen userAction(username) + def UserAction(username: String): ActionBuilder[AuthRequest, AnyContent] = Authenticated andThen userAction(username) /** * Represents an action that requires a user to reenter their password. @@ -172,7 +182,7 @@ abstract class OreBaseController(implicit val env: OreEnv, * @param sig Incoming SSO signature * @return None if verified, Unauthorized otherwise */ - def VerifiedAction(username: String, sso: Option[String], sig: Option[String]) + def VerifiedAction(username: String, sso: Option[String], sig: Option[String]): ActionBuilder[AuthRequest, AnyContent] = UserAction(username) andThen verifiedAction(sso, sig) } @@ -187,4 +197,4 @@ object OreBaseController { def apply[A](form: Form[A])(implicit F: Monad[F], request: Request[_]): OptionT[F, A] = form.bindFromRequest().fold(_ => OptionT.none[F, A], OptionT.some[F](_)) } -} \ No newline at end of file +} diff --git a/app/controllers/Organizations.scala b/app/controllers/Organizations.scala index 3213407f1..527871019 100755 --- a/app/controllers/Organizations.scala +++ b/app/controllers/Organizations.scala @@ -18,6 +18,8 @@ import util.functional.Id import scala.concurrent.{ExecutionContext, Future} +import play.api.mvc.{Action, AnyContent} + /** * Controller for handling Organization based actions. */ @@ -42,9 +44,9 @@ class Organizations @Inject()(forms: OreForms, * * @return Organization creation panel */ - def showCreator() = UserLock().async { implicit request => + 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()) } @@ -57,7 +59,7 @@ class Organizations @Inject()(forms: OreForms, * * @return Redirect to organization page */ - def create() = UserLock().async { implicit request => + def create(): Action[AnyContent] = UserLock().async { implicit request => val user = request.user val failCall = routes.Organizations.showCreator() user.ownedOrganizations.size.flatMap { size => @@ -85,7 +87,7 @@ class Organizations @Inject()(forms: OreForms, * @param status Invite status * @return NotFound if invite doesn't exist, Ok otherwise */ - def setInviteStatus(id: Int, status: String) = Authenticated.async { implicit request => + def setInviteStatus(id: Int, status: String): Action[AnyContent] = Authenticated.async { implicit request => val user = request.user user.organizationRoles.get(id).map { role => status match { @@ -110,7 +112,7 @@ class Organizations @Inject()(forms: OreForms, * @param organization Organization to update avatar of * @return Json response with errors if any */ - def updateAvatar(organization: String) = EditOrganizationAction(organization) { implicit request => + def updateAvatar(organization: String): Action[AnyContent] = EditOrganizationAction(organization) { implicit request => // TODO implement me Ok } @@ -121,7 +123,7 @@ class Organizations @Inject()(forms: OreForms, * @param organization Organization to update * @return Redirect to Organization page */ - def removeMember(organization: String) = EditOrganizationAction(organization).async { implicit request => + def removeMember(organization: String): Action[AnyContent] = EditOrganizationAction(organization).async { implicit request => val res = for { name <- bindFormOptionT[Future](this.forms.OrganizationMemberRemove) user <- this.users.withName(name) @@ -139,7 +141,7 @@ class Organizations @Inject()(forms: OreForms, * @param organization Organization to update * @return Redirect to Organization page */ - def updateMembers(organization: String) = EditOrganizationAction(organization) { implicit request => + def updateMembers(organization: String): Action[AnyContent] = EditOrganizationAction(organization) { implicit request => bindFormOptionT[Id](this.forms.OrganizationUpdateMembers).map { update => update.saveTo(request.data.orga) Redirect(ShowUser(organization)) diff --git a/app/controllers/Reviews.scala b/app/controllers/Reviews.scala index d619e770e..bee2da1a0 100644 --- a/app/controllers/Reviews.scala +++ b/app/controllers/Reviews.scala @@ -3,7 +3,7 @@ package controllers import java.sql.Timestamp import java.time.Instant -import controllers.sugar.Bakery +import controllers.sugar.{Bakery, Requests} import controllers.sugar.Requests.AuthRequest import db.ModelService import db.impl.OrePostgresDriver.api._ @@ -12,7 +12,7 @@ import form.OreForms import javax.inject.Inject import models.admin.{Message, Review} import models.project.{Project, Version} -import models.user.{Notification, User} +import models.user.{LoggedAction, Notification, User, UserActionLogger} import ore.permission.ReviewProjects import ore.permission.role.Lifted import ore.permission.role.RoleTypes.RoleType @@ -20,7 +20,7 @@ import ore.user.notification.NotificationTypes import ore.{OreConfig, OreEnv} import play.api.cache.AsyncCacheApi import play.api.i18n.MessagesApi -import play.api.mvc.Result +import play.api.mvc.{Action, AnyContent, Result} import security.spauth.SingleSignOnConsumer import slick.lifted.{Rep, TableQuery} import util.DataHelper @@ -45,10 +45,10 @@ final class Reviews @Inject()(data: DataHelper, implicit override val service: ModelService)(implicit val ec: ExecutionContext) extends OreBaseController { - def showReviews(author: String, slug: String, versionString: String) = + def showReviews(author: String, slug: String, versionString: String): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects) andThen ProjectAction(author, slug)).async { request => - implicit val r = request.request - implicit val p = request.data.project + implicit val r: Requests.OreRequest[AnyContent] = request.request + implicit val p: Project = request.data.project val res = for { version <- getVersion(p, versionString) @@ -65,7 +65,7 @@ final class Reviews @Inject()(data: DataHelper, res.merge } - def createReview(author: String, slug: String, versionString: String) = { + def createReview(author: String, slug: String, versionString: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => getProjectVersion(author, slug, versionString).map { version => val review = new Review(Some(1), Some(Timestamp.from(Instant.now())), version.id.get, request.user.id.get, None, "") @@ -75,7 +75,7 @@ final class Reviews @Inject()(data: DataHelper, } } - def reopenReview(author: String, slug: String, versionString: String) = { + def reopenReview(author: String, slug: String, versionString: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => val res = for { version <- getProjectVersion(author, slug, versionString) @@ -93,7 +93,7 @@ final class Reviews @Inject()(data: DataHelper, } } - def stopReview(author: String, slug: String, versionString: String) = { + def stopReview(author: String, slug: String, versionString: String): Action[AnyContent] = { Authenticated andThen PermissionAction[AuthRequest](ReviewProjects) async { implicit request => val res = for { version <- getProjectVersion(author, slug, versionString) @@ -109,7 +109,7 @@ final class Reviews @Inject()(data: DataHelper, } } - def approveReview(author: String, slug: String, versionString: String) = { + def approveReview(author: String, slug: String, versionString: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)) async { implicit request => val ret = for { project <- getProject(author, slug) @@ -175,13 +175,13 @@ 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 } - def takeoverReview(author: String, slug: String, versionString: String) = { + def takeoverReview(author: String, slug: String, versionString: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val ret = for { version <- getProjectVersion(author, slug, versionString) @@ -209,7 +209,7 @@ final class Reviews @Inject()(data: DataHelper, } } - def editReview(author: String, slug: String, versionString: String, reviewId: Int) = { + def editReview(author: String, slug: String, versionString: String, reviewId: Int): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val res = for { version <- getProjectVersion(author, slug, versionString) @@ -224,7 +224,7 @@ final class Reviews @Inject()(data: DataHelper, } } - def addMessage(author: String, slug: String, versionString: String) = { + def addMessage(author: String, slug: String, versionString: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val ret = for { version <- getProjectVersion(author, slug, versionString) @@ -241,4 +241,19 @@ final class Reviews @Inject()(data: DataHelper, ret.merge } } + + def shouldReviewToggle(author: String, slug: String, versionString: String): Action[AnyContent] = { + Authenticated andThen PermissionAction[AuthRequest](ReviewProjects) async { implicit request => + val res = for { + version <- getProjectVersion(author, slug, versionString) + } yield { + + UserActionLogger.log(request, LoggedAction.VersionNonReviewChanged, version.id.getOrElse(-1), s"In review queue: ${version.isNonReviewed}", s"In review queue: ${!version.isNonReviewed}") + version.setIsNonReviewed(!version.isNonReviewed) + + Redirect(routes.Reviews.showReviews(author, slug, versionString)) + } + res.merge + } + } } diff --git a/app/controllers/Users.scala b/app/controllers/Users.scala index 1aadf41d7..8dbf4b021 100755 --- a/app/controllers/Users.scala +++ b/app/controllers/Users.scala @@ -10,7 +10,7 @@ import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject import mail.{EmailFactory, Mailer} -import models.user.{SignOn, User} +import models.user.{LoggedAction, SignOn, User, UserActionLogger} import models.viewhelper.{OrganizationData, ScopedOrganizationData} import ore.permission.ReviewProjects import ore.rest.OreWrites @@ -67,7 +67,7 @@ class Users @Inject()(fakeUser: FakeUser, * @param sig Incoming signature from auth * @return Logged in home */ - def logIn(sso: Option[String], sig: Option[String], returnPath: Option[String]) = Action.async { implicit request => + def logIn(sso: Option[String], sig: Option[String], returnPath: Option[String]): Action[AnyContent] = Action.async { implicit request => if (this.fakeUser.isEnabled) { // Log in as fake user (debug only) this.config.checkDebug() @@ -84,8 +84,6 @@ class Users @Inject()(fakeUser: FakeUser, val fromSponge = User.fromSponge(spongeUser) for { user <- this.users.getOrCreate(fromSponge) - _ <- user.pullForumData() - _ <- user.pullSpongeData() result <- this.redirectBack(request.flash.get("url").getOrElse("/"), user) } yield result }.getOrElse(Redirect(ShowHome).withError("error.loginFailed")) @@ -120,8 +118,8 @@ class Users @Inject()(fakeUser: FakeUser, * * @return Home page */ - def logOut(returnPath: Option[String]) = Action { implicit request => - Redirect(this.baseUrl + returnPath.getOrElse(request.path)).clearingSession().flashing("noRedirect" -> "true") + def logOut() = Action { implicit request => + Redirect(config.security.get[String]("api.url") + "/accounts/logout/").clearingSession().flashing("noRedirect" -> "true") } /** @@ -131,7 +129,7 @@ class Users @Inject()(fakeUser: FakeUser, * @param username Username to lookup * @return View of user projects page */ - def showProjects(username: String, page: Option[Int]) = OreAction async { implicit request => + def showProjects(username: String, page: Option[Int]): Action[AnyContent] = OreAction async { implicit request => val pageSize = this.config.users.get[Int]("project-page-size") val p = page.getOrElse(1) val offset = (p - 1) * pageSize @@ -153,7 +151,7 @@ class Users @Inject()(fakeUser: FakeUser, (p, user, v, tags) } val starredData = starred zip starredRv - Ok(views.users.projects(userData.get, orgaData.flatMap(a => scopedOrgaData.map(b => (a, b))), data, starredData, p)) + Ok(views.users.projects(userData.get, orgaData.flatMap(a => scopedOrgaData.map(b => (a, b))), data, starredData.take(5), p)) } }.getOrElse(notFound) } @@ -184,7 +182,7 @@ class Users @Inject()(fakeUser: FakeUser, * @param username User to update * @return View of user page */ - def saveTagline(username: String) = UserAction(username).async { implicit request => + def saveTagline(username: String): Action[AnyContent] = UserAction(username).async { implicit request => val maxLen = this.config.users.get[Int]("max-tagline-len") val res = for { @@ -192,8 +190,9 @@ 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 { + UserActionLogger.log(request, LoggedAction.UserTaglineChanged, user.id.get, tagline, user.tagline.getOrElse("null")) user.setTagline(tagline) Redirect(ShowUser(user)) } @@ -209,7 +208,7 @@ class Users @Inject()(fakeUser: FakeUser, * @param username User to save key to * @return JSON response */ - def savePgpPublicKey(username: String) = UserAction(username) { implicit request => + def savePgpPublicKey(username: String): Action[AnyContent] = UserAction(username) { implicit request => this.forms.UserPgpPubKey.bindFromRequest.fold( hasErrors => Redirect(ShowUser(username)).withFormErrors(hasErrors.errors), @@ -222,6 +221,7 @@ class Users @Inject()(fakeUser: FakeUser, // Send email notification this.mailer.push(this.emails.create(user, this.emails.PgpUpdated)) + UserActionLogger.log(request, LoggedAction.UserPgpKeySaved, user.id.get, "", "") Redirect(ShowUser(username)).flashing("pgp-updated" -> "true") } @@ -234,15 +234,16 @@ class Users @Inject()(fakeUser: FakeUser, * @param username Username to delete key for * @return Ok if deleted, bad request if didn't exist */ - def deletePgpPublicKey(username: String, sso: Option[String], sig: Option[String]) = { + def deletePgpPublicKey(username: String, sso: Option[String], sig: Option[String]): Action[AnyContent] = { VerifiedAction(username, sso, sig) { implicit request => - Logger.info("Deleting public key for " + username) + Logger.debug("Deleting public key for " + username) val user = request.user if (user.pgpPubKey.isEmpty) BadRequest else { user.setPgpPubKey(null) user.setLastPgpPubKeyUpdate(this.service.theTime) + UserActionLogger.log(request, LoggedAction.UserPgpKeyRemoved, user.id.get, "", "") Redirect(ShowUser(username)).flashing("pgp-updated" -> "true") } } @@ -255,7 +256,7 @@ class Users @Inject()(fakeUser: FakeUser, * @param locked True if user is locked * @return Redirection to user page */ - def setLocked(username: String, locked: Boolean, sso: Option[String], sig: Option[String]) = { + def setLocked(username: String, locked: Boolean, sso: Option[String], sig: Option[String]): Action[AnyContent] = { VerifiedAction(username, sso, sig) { implicit request => val user = request.user user.setLocked(locked) @@ -269,7 +270,7 @@ class Users @Inject()(fakeUser: FakeUser, * Shows a list of [[models.user.User]]s that have created a * [[models.project.Project]]. */ - def showAuthors(sort: Option[String], page: Option[Int]) = OreAction async { implicit request => + def showAuthors(sort: Option[String], page: Option[Int]): Action[AnyContent] = OreAction async { implicit request => val ordering = sort.getOrElse(ORDERING_PROJECTS) val p = page.getOrElse(1) this.users.getAuthors(ordering, p).map { u => @@ -281,7 +282,7 @@ class Users @Inject()(fakeUser: FakeUser, /** * Shows a list of [[models.user.User]]s that have Ore staff roles. */ - def showStaff(sort: Option[String], page: Option[Int]) = (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => + def showStaff(sort: Option[String], page: Option[Int]): Action[AnyContent] = (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val ordering = sort.getOrElse(ORDERING_ROLE) val p = page.getOrElse(1) this.users.getStaff(ordering, p).map { u => @@ -294,7 +295,7 @@ class Users @Inject()(fakeUser: FakeUser, * * @return Unread notifications */ - def showNotifications(notificationFilter: Option[String], inviteFilter: Option[String]) = { + def showNotifications(notificationFilter: Option[String], inviteFilter: Option[String]): Action[AnyContent] = { Authenticated.async { implicit request => val user = request.user @@ -325,7 +326,7 @@ class Users @Inject()(fakeUser: FakeUser, * @param id Notification ID * @return Ok if marked as read, NotFound if notification does not exist */ - def markNotificationRead(id: Int) = Authenticated.async { implicit request => + def markNotificationRead(id: Int): Action[AnyContent] = Authenticated.async { implicit request => request.user.notifications.get(id).map { notification => notification.setRead(read = true) Ok diff --git a/app/controllers/project/Channels.scala b/app/controllers/project/Channels.scala index a5b06e8f0..063893c15 100755 --- a/app/controllers/project/Channels.scala +++ b/app/controllers/project/Channels.scala @@ -1,7 +1,7 @@ package controllers.project import controllers.OreBaseController -import controllers.sugar.Bakery +import controllers.sugar.{Bakery, Requests} import db.ModelService import form.OreForms import javax.inject.Inject @@ -16,6 +16,8 @@ import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} import models.project.Project +import models.viewhelper.ProjectData +import play.api.mvc.{Action, AnyContent} import util.functional.EitherT import util.syntax._ @@ -45,8 +47,8 @@ class Channels @Inject()(forms: OreForms, * @param slug Project slug * @return View of channels */ - def showList(author: String, slug: String) = ChannelEditAction(author, slug).async { request => - implicit val r = request.request + def showList(author: String, slug: String): Action[AnyContent] = ChannelEditAction(author, slug).async { request => + implicit val r: Requests.AuthRequest[AnyContent] = request.request for { channels <- request.data.project.channels.toSeq versionCount <- Future.sequence(channels.map(_.versions.size)) @@ -63,7 +65,7 @@ class Channels @Inject()(forms: OreForms, * @param slug Project slug * @return Redirect to view of channels */ - def create(author: String, slug: String) = ChannelEditAction(author, slug).async { implicit request => + def create(author: String, slug: String): Action[AnyContent] = ChannelEditAction(author, slug).async { implicit request => val res = for { channelData <- bindFormEitherT[Future](this.forms.ChannelEdit)(hasErrors => Redirect(self.showList(author, slug)).withFormErrors(hasErrors.errors)) _ <- channelData.addTo(request.data.project).leftMap(error => Redirect(self.showList(author, slug)).withError(error)) @@ -80,7 +82,7 @@ class Channels @Inject()(forms: OreForms, * @param channelName Channel name * @return View of channels */ - def save(author: String, slug: String, channelName: String) = ChannelEditAction(author, slug).async { implicit request => + def save(author: String, slug: String, channelName: String): Action[AnyContent] = ChannelEditAction(author, slug).async { implicit request => implicit val project: Project = request.data.project val res = for { @@ -100,8 +102,8 @@ class Channels @Inject()(forms: OreForms, * @param channelName Channel name * @return View of channels */ - def delete(author: String, slug: String, channelName: String) = ChannelEditAction(author, slug).async { implicit request => - implicit val data = request.data + def delete(author: String, slug: String, channelName: String): Action[AnyContent] = ChannelEditAction(author, slug).async { implicit request => + implicit val data: ProjectData = request.data EitherT.right[Status](data.project.channels.all) .filterOrElse(_.size != 1, Redirect(self.showList(author, slug)).withError("error.channel.last")) .flatMap { channels => diff --git a/app/controllers/project/Pages.scala b/app/controllers/project/Pages.scala index 222d603e0..391d75140 100755 --- a/app/controllers/project/Pages.scala +++ b/app/controllers/project/Pages.scala @@ -1,13 +1,15 @@ package controllers.project +import java.nio.charset.StandardCharsets + import controllers.OreBaseController -import controllers.sugar.Bakery +import controllers.sugar.{Bakery, Requests} import db.impl.OrePostgresDriver.api._ import db.{ModelFilter, ModelService} import form.OreForms import javax.inject.Inject import models.project.{Page, Project} -import models.user.{LoggedActionContext, UserActionLogger, LoggedAction} +import models.user.{LoggedAction, LoggedActionContext, UserActionLogger} import ore.permission.EditPages import ore.{OreConfig, OreEnv, StatTracker} import play.api.cache.AsyncCacheApi @@ -17,7 +19,10 @@ import util.StringUtils._ import views.html.projects.{pages => views} import util.instances.future._ import util.syntax._ + import scala.concurrent.{ExecutionContext, Future} +import play.api.mvc.{Action, AnyContent} +import play.utils.UriEncoding /** * Controller for handling Page related actions. @@ -47,8 +52,9 @@ class Pages @Inject()(forms: OreForms, */ def withPage(project: Project, page: String): Future[(Option[Page], Boolean)] = { //TODO: Can the return type here be changed to OptionT[Future (Page, Boolean)]? - val parts = page.split("/") - if (parts.size == 2) { + val parts = page.split("/").map(page => UriEncoding.decodePathSegment(page, StandardCharsets.UTF_8)) + + if (parts.length == 2) { project.pages .find(equalsIgnoreCase(_.slug, parts(0))) .subflatMap(_.id) @@ -72,9 +78,9 @@ class Pages @Inject()(forms: OreForms, * @param page Page name * @return View of page */ - def show(author: String, slug: String, page: String) = ProjectAction(author, slug).async { request => + def show(author: String, slug: String, page: String): Action[AnyContent] = ProjectAction(author, slug).async { request => val data = request.data - implicit val r = request.request + implicit val r: Requests.OreRequest[AnyContent] = request.request withPage(data.project, page).flatMap { case (None, _) => Future.successful(notFound) @@ -98,8 +104,9 @@ class Pages @Inject()(forms: OreForms, * @param pageName Page name * @return Page editor */ - def showEditor(author: String, slug: String, pageName: String) = PageEditAction(author, slug).async { request => - implicit val r = request.request + def showEditor(author: String, slug: String, pageName: String): Action[AnyContent] = PageEditAction(author, slug) + .async { request => + implicit val r: Requests.AuthRequest[AnyContent] = request.request val data = request.data val parts = pageName.split("/") @@ -135,7 +142,7 @@ class Pages @Inject()(forms: OreForms, * @param page Page name * @return Project home */ - def save(author: String, slug: String, page: String) = PageEditAction(author, slug).async { implicit request => + def save(author: String, slug: String, page: String): Action[AnyContent] = PageEditAction(author, slug).async { implicit request => this.forms.PageEdit.bindFromRequest().fold( hasErrors => Future.successful(Redirect(self.show(author, slug, page)).withFormErrors(hasErrors.errors)), @@ -166,7 +173,7 @@ class Pages @Inject()(forms: OreForms, if (pageData.content.isDefined) { val oldPage = createdPage.contents val newPage = pageData.content.get - UserActionLogger.log(request.request, LoggedAction.ProjectPageEdited, data.project.id.getOrElse(-1), oldPage, newPage) + UserActionLogger.log(request.request, LoggedAction.ProjectPageEdited, createdPage.id.getOrElse(-1), newPage, oldPage) createdPage.setContents(newPage) } else Future.successful(createdPage) } map { _ => @@ -187,7 +194,7 @@ class Pages @Inject()(forms: OreForms, * @param page Page name * @return Redirect to Project homepage */ - def delete(author: String, slug: String, page: String) = PageEditAction(author, slug).async { implicit request => + def delete(author: String, slug: String, page: String): Action[AnyContent] = PageEditAction(author, slug).async { implicit request => val data = request.data withPage(data.project, page).map { optionPage => if (optionPage._1.isDefined) diff --git a/app/controllers/project/Projects.scala b/app/controllers/project/Projects.scala index 410ddaf58..519f1f083 100755 --- a/app/controllers/project/Projects.scala +++ b/app/controllers/project/Projects.scala @@ -3,17 +3,17 @@ package controllers.project import java.nio.file.{Files, Path} import controllers.OreBaseController -import controllers.sugar.Bakery +import controllers.sugar.{Bakery, Requests} import controllers.sugar.Requests.{AuthRequest, AuthedProjectRequest} import db.ModelService import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject -import models.project.{Note, VisibilityTypes} +import models.project.{Note, Project, VisibilityTypes} import models.user._ import ore.permission._ import ore.project.factory.ProjectFactory -import ore.project.io.{InvalidPluginFileException, PluginUpload} +import ore.project.io.{InvalidPluginFileException, PluginUpload, ProjectFiles} import ore.rest.ProjectApiKeyTypes import ore.user.MembershipDossier._ import ore.{OreConfig, OreEnv, StatTracker} @@ -28,7 +28,12 @@ import db.impl.OrePostgresDriver.api._ import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} -import play.api.mvc.Result +import db.impl.{ProjectMembersTable, ProjectRoleTable} +import models.user.role.ProjectRole +import models.viewhelper.ScopedOrganizationData +import ore.project.ProjectMember +import ore.user.MembershipDossier +import play.api.mvc.{Action, AnyContent, Result} import util.functional.{EitherT, Id, OptionT} import util.instances.future._ import util.syntax._ @@ -51,7 +56,7 @@ class Projects @Inject()(stats: StatTracker, extends OreBaseController { - implicit val fileManager = factory.fileManager + implicit val fileManager: ProjectFiles = factory.fileManager private val self = controllers.project.routes.Projects @@ -63,7 +68,7 @@ class Projects @Inject()(stats: StatTracker, * * @return Create project view */ - def showCreator() = UserLock() async { implicit request => + def showCreator(): Action[AnyContent] = UserLock() async { implicit request => for { orgas <- request.user.organizations.all @@ -81,7 +86,7 @@ class Projects @Inject()(stats: StatTracker, * * @return Result */ - def upload() = UserLock() { implicit request => + def upload(): Action[AnyContent] = UserLock() { implicit request => val user = request.user this.factory.getUploadError(user) match { case Some(error) => @@ -93,10 +98,15 @@ class Projects @Inject()(stats: StatTracker, case Some(uploadData) => try { val plugin = this.factory.processPluginUpload(uploadData, user) - val project = this.factory.startProject(plugin) - project.cache() - val model = project.underlying - Redirect(self.showCreatorWithMeta(model.ownerName, model.slug)) + plugin match { + case Right(pluginFile) => + val project = this.factory.startProject(pluginFile) + project.cache() + val model = project.underlying + Redirect(self.showCreatorWithMeta(model.ownerName, model.slug)) + case Left(errorMessage) => + Redirect(self.showCreator()).withError(errorMessage) + } } catch { case e: InvalidPluginFileException => Redirect(self.showCreator()).withErrors(Option(e.getMessage).toList) @@ -112,7 +122,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Create project view */ - def showCreatorWithMeta(author: String, slug: String) = UserLock().async { implicit request => + def showCreatorWithMeta(author: String, slug: String): Action[AnyContent] = UserLock().async { implicit request => this.factory.getPendingProject(author, slug) match { case None => Future.successful(Redirect(self.showCreator()).withError("error.project.timeout")) @@ -134,7 +144,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return View of members config */ - def showInvitationForm(author: String, slug: String) = UserLock().async { implicit request => + def showInvitationForm(author: String, slug: String): Action[AnyContent] = UserLock().async { implicit request => orgasUserCanUploadTo(request.user) flatMap { organisationUserCanUploadTo => this.factory.getPendingProject(author, slug) match { case None => Future.successful(Redirect(self.showCreator()).withError("error.project.timeout")) @@ -150,15 +160,15 @@ class Projects @Inject()(stats: StatTracker, val namespace = project.namespace this.cache.set(namespace, pendingProject) this.cache.set(namespace + '/' + version.underlying.versionString, version) - implicit val currentUser = request.user + implicit val currentUser: User = request.user - val authors = pendingProject.file.meta.get.getAuthors.asScala + val authors = pendingProject.file.data.get.authors.toList ( Future.sequence(authors.filterNot(_.equals(currentUser.username)).map(this.users.withName(_).value)), - this.forums.countUsers(authors.toList), + this.forums.countUsers(authors), pendingProject.underlying.owner.user ).parMapN { (users, registered, owner) => - Ok(views.invite(owner, pendingProject, users.flatten.toList, registered)) + Ok(views.invite(owner, pendingProject, users.flatten, registered)) } } ) @@ -188,7 +198,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Redirection to project page if successful */ - def showFirstVersionCreator(author: String, slug: String) = UserLock() { implicit request => + def showFirstVersionCreator(author: String, slug: String): Action[AnyContent] = UserLock() { implicit request => val res = for { pendingProject <- EitherT.fromOption[Id](this.factory.getPendingProject(author, slug), Redirect(self.showCreator()).withError( @@ -211,9 +221,9 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return View of project */ - def show(author: String, slug: String) = ProjectAction(author, slug) async { request => + def show(author: String, slug: String): Action[AnyContent] = ProjectAction(author, slug) async { request => val data = request.data - implicit val r = request.request + implicit val r: Requests.OreRequest[AnyContent] = request.request projects.queryProjectPages(data.project) flatMap { pages => val pageCount = pages.size + pages.map(_._2.size).sum @@ -227,7 +237,7 @@ class Projects @Inject()(stats: StatTracker, * @param pluginId Project pluginId * @return Redirect to project page. */ - def showProjectById(pluginId: String) = OreAction async { implicit request => + def showProjectById(pluginId: String): Action[AnyContent] = OreAction async { implicit request => this.projects.withPluginId(pluginId).fold(notFound) { project => Redirect(self.show(project.ownerName, project.slug)) } @@ -240,8 +250,8 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return View of project */ - def showDiscussion(author: String, slug: String) = ProjectAction(author, slug) async { request => - implicit val r = request.request + def showDiscussion(author: String, slug: String): Action[AnyContent] = ProjectAction(author, slug) async { request => + implicit val r: Requests.OreRequest[AnyContent] = request.request this.stats.projectViewed(request)(request => Ok(views.discuss(request.data, request.scoped))) } @@ -252,7 +262,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return View of discussion with new post */ - def postDiscussionReply(author: String, slug: String) = AuthedProjectAction(author, slug) async { implicit request => + def postDiscussionReply(author: String, slug: String): Action[AnyContent] = AuthedProjectAction(author, slug) async { implicit request => this.forms.ProjectReply.bindFromRequest.fold( hasErrors => Future.successful(Redirect(self.showDiscussion(author, slug)).withFormErrors(hasErrors.errors)), @@ -285,8 +295,8 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Issue tracker */ - def showIssues(author: String, slug: String) = ProjectAction(author, slug) { implicit request => - implicit val r = request.request + def showIssues(author: String, slug: String): Action[AnyContent] = ProjectAction(author, slug) { implicit request => + implicit val r: Requests.OreRequest[AnyContent] = request.request request.data.settings.issues match { case None => notFound case Some(link) => Redirect(link) @@ -300,8 +310,8 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Source code */ - def showSource(author: String, slug: String) = ProjectAction(author, slug) { implicit request => - implicit val r = request.request + def showSource(author: String, slug: String): Action[AnyContent] = ProjectAction(author, slug) { implicit request => + implicit val r: Requests.OreRequest[AnyContent] = request.request request.data.settings.source match { case None => notFound case Some(link) => Redirect(link) @@ -316,7 +326,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Project icon */ - def showIcon(author: String, slug: String) = Action async { implicit request => + def showIcon(author: String, slug: String): Action[AnyContent] = Action async { implicit request => // TODO maybe instead of redirect cache this on ore? this.projects.withSlug(author, slug).semiFlatMap { project => this.projects.fileManager.getIconPath(project) match { @@ -337,7 +347,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return View of project */ - def flag(author: String, slug: String) = AuthedProjectAction(author, slug).async { implicit request => + def flag(author: String, slug: String): Action[AnyContent] = AuthedProjectAction(author, slug).async { implicit request => val user = request.user val data = request.data user.hasUnresolvedFlagFor(data.project).map { @@ -363,7 +373,7 @@ class Projects @Inject()(stats: StatTracker, * @param watching True if watching * @return Ok */ - def setWatching(author: String, slug: String, watching: Boolean) = { + def setWatching(author: String, slug: String, watching: Boolean): Action[AnyContent] = { AuthedProjectAction(author, slug) async { implicit request => request.user.setWatching(request.data.project, watching).map(_ => Ok) } @@ -377,7 +387,7 @@ class Projects @Inject()(stats: StatTracker, * @param starred True if should set to starred * @return Result code */ - def setStarred(author: String, slug: String, starred: Boolean) = { + def setStarred(author: String, slug: String, starred: Boolean): Action[AnyContent] = { AuthedProjectAction(author, slug) { implicit request => if (request.data.project.ownerId != request.user.userId) { request.data.project.setStarredBy(request.user, starred) @@ -395,11 +405,21 @@ class Projects @Inject()(stats: StatTracker, * @param status Invite status * @return NotFound if invite doesn't exist, Ok otherwise */ - def setInviteStatus(id: Int, status: String) = Authenticated.async { implicit request => + def setInviteStatus(id: Int, status: String): Action[AnyContent] = Authenticated.async { implicit request => val user = request.user user.projectRoles.get(id).semiFlatMap { role => role.project.map { project => - val dossier = project.memberships + val dossier: MembershipDossier { + type MembersTable = ProjectMembersTable + + type MemberType = ProjectMember + + type RoleTable = ProjectRoleTable + + type ModelType = Project + + type RoleType = ProjectRole +} = project.memberships status match { case STATUS_DECLINE => dossier.removeRole(role) @@ -417,6 +437,53 @@ class Projects @Inject()(stats: StatTracker, }.getOrElse(NotFound) } + /** + * Sets the status of a pending Project invite on behalf of the Organization + * + * @param id Invite ID + * @param status Invite status + * @param behalf Behalf User + * @return NotFound if invite doesn't exist, Ok otherwise + */ + def setInviteStatusOnBehalf(id: Int, status: String, behalf: String): Action[AnyContent] = Authenticated.async { implicit request => + val user = request.user + val res = for { + orga <- organizations.withName(behalf) + orgaUser <- users.withName(behalf) + role <- orgaUser.projectRoles.get(id) + scopedData <- OptionT.liftF(ScopedOrganizationData.of(Some(user), orga)) + if scopedData.permissions.getOrElse(EditSettings, false) + project <- OptionT.liftF(role.project) + } yield { + val dossier: MembershipDossier { + type MembersTable = ProjectMembersTable + + type MemberType = ProjectMember + + type RoleTable = ProjectRoleTable + + type ModelType = Project + + type RoleType = ProjectRole + } = project.memberships + status match { + case STATUS_DECLINE => + dossier.removeRole(role) + Ok + case STATUS_ACCEPT => + role.setAccepted(true) + Ok + case STATUS_UNACCEPT => + role.setAccepted(false) + Ok + case _ => + BadRequest + } + } + + res.getOrElse(NotFound) + } + /** * Shows the project manager or "settings" pane. * @@ -424,8 +491,8 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Project manager */ - def showSettings(author: String, slug: String) = SettingsEditAction(author, slug) async { request => - implicit val r = request.request + def showSettings(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug) async { request => + implicit val r: AuthRequest[AnyContent] = request.request val projectData = request.data projectData.project.apiKeys.find(_.keyType === ProjectApiKeyTypes.Deployment).value.map { deployKey => Ok(views.settings(projectData, request.scoped, deployKey)) @@ -439,7 +506,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Ok or redirection if no file */ - def uploadIcon(author: String, slug: String) = SettingsEditAction(author, slug) { implicit request => + def uploadIcon(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug) { implicit request => request.body.asMultipartFormData.get.file("icon") match { case None => Redirect(self.showSettings(author, slug)).withError("error.noFile") @@ -462,7 +529,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Ok */ - def resetIcon(author: String, slug: String) = SettingsEditAction(author, slug) { implicit request => + def resetIcon(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug) { implicit request => val data = request.data val fileManager = this.projects.fileManager fileManager.getIconPath(data.project).foreach(Files.delete) @@ -480,9 +547,9 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Pending icon */ - def showPendingIcon(author: String, slug: String) = ProjectAction(author, slug) { implicit request => + def showPendingIcon(author: String, slug: String): Action[AnyContent] = ProjectAction(author, slug) { implicit request => val data = request.data - implicit val r = request.request + implicit val r: Requests.OreRequest[AnyContent] = request.request this.projects.fileManager.getPendingIconPath(data.project) match { case None => notFound case Some(path) => showImage(path) @@ -495,7 +562,7 @@ class Projects @Inject()(stats: StatTracker, * @param author Project owner * @param slug Project slug */ - def removeMember(author: String, slug: String) = SettingsEditAction(author, slug).async { implicit request => + def removeMember(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug).async { implicit request => val res = for { name <- bindFormOptionT[Future](this.forms.ProjectMemberRemove) user <- this.users.withName(name) @@ -503,7 +570,7 @@ class Projects @Inject()(stats: StatTracker, val project = request.data.project project.memberships.removeMember(user) UserActionLogger.log(request.request, LoggedAction.ProjectMemberRemoved, project.id.getOrElse(-1), - s"'$user.name' is not a member of ${project.ownerName}/${project.name}", s"'$user.name' is a member of ${project.ownerName}/${project.name}") + s"'${user.name}' is not a member of ${project.ownerName}/${project.name}", s"'${user.name}' is a member of ${project.ownerName}/${project.name}") Redirect(self.showSettings(author, slug)) } @@ -517,7 +584,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return View of project */ - def save(author: String, slug: String) = SettingsEditAction(author, slug).async { implicit request => + def save(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug).async { implicit request => orgasUserCanUploadTo(request.user) flatMap { organisationUserCanUploadTo => val data = request.data this.forms.ProjectSave(organisationUserCanUploadTo.toSeq).bindFromRequest().fold( @@ -540,7 +607,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Project homepage */ - def rename(author: String, slug: String) = SettingsEditAction(author, slug).async { implicit request => + def rename(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug).async { implicit request => val project = request.data.project val res = for { @@ -566,20 +633,21 @@ class Projects @Inject()(stats: StatTracker, * @param visibility Project visibility * @return Ok */ - def setVisible(author: String, slug: String, visibility: Int) = { + def setVisible(author: String, slug: String, visibility: Int): Action[AnyContent] = { (AuthedProjectAction(author, slug, requireUnlock = true) andThen ProjectPermissionAction(HideProjects)) async { implicit request => val newVisibility = VisibilityTypes.withId(visibility) request.user can newVisibility.permission in GlobalScope flatMap { perm => if (perm) { - val oldVisibility = request.data.project.visibility; - val change = if (newVisibility.showModal) { val comment = this.forms.NeedsChanges.bindFromRequest.get.trim request.data.project.setVisibility(newVisibility, comment, request.user.id.get) } else { request.data.project.setVisibility(newVisibility, "", request.user.id.get) } + + this.forums.changeTopicVisibility(request.data.project, VisibilityTypes.isPublic(newVisibility)) + UserActionLogger.log(request.request, LoggedAction.ProjectVisibilityChange, request.data.project.id.getOrElse(-1), newVisibility.nameKey, VisibilityTypes.NeedsChanges.nameKey) change.map(_ => Ok) } else { @@ -595,7 +663,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Redirect home */ - def publish(author: String, slug: String) = SettingsEditAction(author, slug) { implicit request => + def publish(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug) { implicit request => val data = request.data if (data.visibility == VisibilityTypes.New) { data.project.setVisibility(VisibilityTypes.Public, "", request.user.id.get) @@ -610,7 +678,7 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Redirect home */ - def sendForApproval(author: String, slug: String) = SettingsEditAction(author, slug) { implicit request => + def sendForApproval(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug) { implicit request => val data = request.data if (data.visibility == VisibilityTypes.NeedsChanges) { data.project.setVisibility(VisibilityTypes.NeedsApproval, "", request.user.id.get) @@ -619,9 +687,9 @@ class Projects @Inject()(stats: StatTracker, Redirect(self.show(data.project.ownerName, data.project.slug)) } - def showLog(author: String, slug: String) = { + def showLog(author: String, slug: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ViewLogs)) andThen ProjectAction(author, slug) async { request => - implicit val r = request.request + implicit val r: Requests.OreRequest[AnyContent] = request.request val project = request.data.project for { (changes, logger) <- (project.visibilityChangesByDate, project.logger).parTupled @@ -640,12 +708,12 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Home page */ - def delete(author: String, slug: String) = { + def delete(author: String, slug: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](HardRemoveProject)).async { implicit request => 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)) + UserActionLogger.log(request, LoggedAction.ProjectVisibilityChange, project.id.getOrElse(-1), "deleted", project.visibility.nameKey) + Redirect(ShowHome).withSuccess(request.messages.apply("project.deleted", project.name)) }.merge } } @@ -657,12 +725,16 @@ class Projects @Inject()(stats: StatTracker, * @param slug Project slug * @return Home page */ - def softDelete(author: String, slug: String) = SettingsEditAction(author, slug).async { implicit request => + def softDelete(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug).async { implicit request => val data = request.data val comment = this.forms.NeedsChanges.bindFromRequest.get.trim + val oldVisibility = data.project.visibility.nameKey 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)) + + this.forums.changeTopicVisibility(data.project, false) + + UserActionLogger.log(request.request, LoggedAction.ProjectVisibilityChange, data.project.id.getOrElse(-1), data.project.visibility.nameKey, oldVisibility) + Redirect(ShowHome).withSuccess(request.messages.apply("project.deleted", data.project.name)) } } @@ -672,9 +744,9 @@ class Projects @Inject()(stats: StatTracker, * @param author Project owner * @param slug Project slug */ - def showFlags(author: String, slug: String) = { + def showFlags(author: String, slug: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ReviewFlags)) andThen ProjectAction(author, slug) async { request => - implicit val r = request.request + implicit val r: Requests.OreRequest[AnyContent] = request.request getProject(author, slug).map { project => Ok(views.admin.flags(request.data)) }.merge @@ -687,7 +759,7 @@ class Projects @Inject()(stats: StatTracker, * @param author Project owner * @param slug Project slug */ - def showNotes(author: String, slug: String) = { + def showNotes(author: String, slug: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ReviewFlags)).async { implicit request => getProject(author, slug).semiFlatMap { project => Future.sequence(project.getNotes().map(note => users.get(note.user).value.map(user => (note, user)))) map { notes => @@ -697,7 +769,7 @@ class Projects @Inject()(stats: StatTracker, } } - def addMessage(author: String, slug: String) = { + def addMessage(author: String, slug: String): Action[AnyContent] = { (Authenticated andThen PermissionAction[AuthRequest](ReviewProjects)).async { implicit request => val res = for { project <- getProject(author, slug) diff --git a/app/controllers/project/Versions.scala b/app/controllers/project/Versions.scala index 8c1d0c4eb..d8379871f 100755 --- a/app/controllers/project/Versions.scala +++ b/app/controllers/project/Versions.scala @@ -6,17 +6,19 @@ import java.sql.Timestamp import java.util.{Date, UUID} import com.github.tminglei.slickpg.InetString + import controllers.OreBaseController -import controllers.sugar.Bakery -import controllers.sugar.Requests.{OreRequest, ProjectRequest} +import controllers.sugar.{Bakery, Requests} +import controllers.sugar.Requests.{AuthRequest, OreRequest, ProjectRequest} import db.ModelService import db.impl.OrePostgresDriver.api._ import discourse.OreDiscourseApi import form.OreForms import javax.inject.Inject + import models.project._ import models.viewhelper.{ProjectData, VersionData} -import ore.permission.{EditVersions, ReviewProjects} +import ore.permission.{EditSettings, EditVersions, HardRemoveProject, HardRemoveVersion, ReviewProjects, UploadVersions, ViewLogs} import ore.project.factory.TagAlias.ProjectTag import ore.project.factory.{PendingProject, PendingVersion, ProjectFactory} import ore.project.io.DownloadTypes._ @@ -24,16 +26,16 @@ 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.{Request, Result} +import play.api.mvc.{Action, AnyContent, Request, Result} import play.filters.csrf.CSRF import security.spauth.SingleSignOnConsumer import util.StringUtils._ import util.syntax._ import views.html.projects.{versions => views} import _root_.views.html.helper -import models.user.{UserActionLogger, LoggedAction} +import models.user.{LoggedAction, UserActionLogger} import ore.project.factory.TagAlias.ProjectTag import util.JavaUtils.autoClose import scala.concurrent.{ExecutionContext, Future} @@ -41,6 +43,8 @@ import scala.concurrent.{ExecutionContext, Future} import util.functional.{EitherT, OptionT} import util.instances.future._ +import db.impl.VersionTable + /** * Controller for handling Version related actions. */ @@ -64,6 +68,9 @@ class Versions @Inject()(stats: StatTracker, private def VersionEditAction(author: String, slug: String) = AuthedProjectAction(author, slug, requireUnlock = true) andThen ProjectPermissionAction(EditVersions) + private def VersionUploadAction(author: String, slug: String) + = AuthedProjectAction(author, slug, requireUnlock = true) andThen ProjectPermissionAction(UploadVersions) + /** * Shows the specified version view page. * @@ -72,8 +79,8 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return Version view */ - def show(author: String, slug: String, versionString: String) = ProjectAction(author, slug) async { request => - implicit val r = request.request + def show(author: String, slug: String, versionString: String): Action[AnyContent] = ProjectAction(author, slug) async { request => + implicit val r: OreRequest[AnyContent] = request.request val res = for { version <- getVersion(request.data.project, versionString) data <- EitherT.right[Result](VersionData.of(request, version)) @@ -91,9 +98,9 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return View of Version */ - def saveDescription(author: String, slug: String, versionString: String) = { + def saveDescription(author: String, slug: String, versionString: String): Action[AnyContent] = { VersionEditAction(author, slug).async { request => - implicit val r = request.request + implicit val r: Requests.AuthRequest[AnyContent] = request.request val res = for { version <- getVersion(request.data.project, versionString) description <- bindFormEitherT[Future](this.forms.VersionDescription)(_ => BadRequest: Result) @@ -117,9 +124,9 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return View of version */ - def setRecommended(author: String, slug: String, versionString: String) = { + def setRecommended(author: String, slug: String, versionString: String): Action[AnyContent] = { VersionEditAction(author, slug).async { implicit request => - implicit val r = request.request + implicit val r: Requests.AuthRequest[AnyContent] = request.request getVersion(request.data.project, versionString).map { version => request.data.project.setRecommendedVersion(version) UserActionLogger.log(request.request, LoggedAction.VersionAsRecommended, version.id.getOrElse(-1), "recommended version", "listed version") @@ -136,10 +143,10 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return View of version */ - def approve(author: String, slug: String, versionString: String) = { + def approve(author: String, slug: String, versionString: String): Action[AnyContent] = { (AuthedProjectAction(author, slug, requireUnlock = true) andThen ProjectPermissionAction(ReviewProjects)).async { implicit request => - implicit val r = request.request + implicit val r: Requests.AuthRequest[AnyContent] = request.request getVersion(request.data.project, versionString).map { version => version.setReviewed(reviewed = true) version.setReviewer(request.user) @@ -158,38 +165,34 @@ class Versions @Inject()(stats: StatTracker, * @param channels Visible channels * @return View of project */ - def showList(author: String, slug: String, channels: Option[String], page: Option[Int]) = { + def showList(author: String, slug: String, channels: Option[String]): Action[AnyContent] = { ProjectAction(author, slug).async { request => val data = request.data - implicit val r = request.request + implicit val r: OreRequest[AnyContent] = request.request + data.project.channels.toSeq.flatMap { allChannels => - var visibleNames: Option[Array[String]] = channels.map(_.toLowerCase.split(',')) - val visible: Option[Array[Channel]] = visibleNames.map(_.map { name => - allChannels.find(_.name.equalsIgnoreCase(name)) - }).map(_.flatten) - - val visibleIds: Array[Int] = visible.map(_.map(_.id.get)).getOrElse(allChannels.map(_.id.get).toArray) - - val pageSize = this.config.projects.get[Int]("init-version-load") - val p = page.getOrElse(1) - val futureVersions = data.project.versions.sorted( - ordering = _.createdAt.desc, - filter = _.channelId inSetBind visibleIds, - offset = pageSize * (p - 1), - limit = pageSize) - - if (visibleNames.isDefined && visibleNames.get.toSet.equals(allChannels.map(_.name.toLowerCase).toSet)) { - visibleNames = None + val visibleNames = channels.fold(allChannels.map(_.name.toLowerCase))(_.toLowerCase.split(',').toSeq) + val visible = allChannels.filter(ch => visibleNames.contains(ch.name.toLowerCase)) + val visibleIds = visible.map(_.id.get) + + def versionFilter(v: VersionTable): Rep[Boolean] = { + val inChannel = v.channelId inSetBind visibleIds + val isVisible = + if(r.data.globalPerm(ReviewProjects)) true: Rep[Boolean] + else v.visibility === VisibilityTypes.Public + inChannel && isVisible } + val futureVersionCount = data.project.versions.count(versionFilter) + + val visibleNamesForView = if(visibleNames == allChannels.map(_.name.toLowerCase)) Nil else visibleNames + for { - versions <- futureVersions + versionCount <- futureVersionCount r <- this.stats.projectViewed(request) { request => - Ok(views.list(data, request.scoped, allChannels, versions, visibleNames, p)) + Ok(views.list(data, request.scoped, allChannels, versionCount, visibleNamesForView)) } - } yield { - r - } + } yield r } } } @@ -201,9 +204,9 @@ class Versions @Inject()(stats: StatTracker, * @param slug Project slug * @return Version creation view */ - def showCreator(author: String, slug: String) = VersionEditAction(author, slug).async { request => + def showCreator(author: String, slug: String): Action[AnyContent] = VersionUploadAction(author, slug).async { request => val data = request.data - implicit val r = request.request + implicit val r: Requests.AuthRequest[AnyContent] = request.request data.project.channels.all.map { channels => Ok(views.create(data, data.settings.forumSync, None, Some(channels.toSeq), showFileControls = true)) } @@ -216,7 +219,7 @@ class Versions @Inject()(stats: StatTracker, * @param slug Project slug * @return Version create page (with meta) */ - def upload(author: String, slug: String) = VersionEditAction(author, slug).async { implicit request => + def upload(author: String, slug: String): Action[AnyContent] = VersionUploadAction(author, slug).async { implicit request => val call = self.showCreator(author, slug) val user = request.user @@ -231,8 +234,7 @@ class Versions @Inject()(stats: StatTracker, this.factory .processSubsequentPluginUpload(data, user, request.data.project) .leftMap(err => Redirect(call).withError(err)) - } - catch { + } catch { case e: InvalidPluginFileException => EitherT.leftT[Future, PendingVersion](Redirect(call).withErrors(Option(e.getMessage).toList)) } @@ -250,7 +252,7 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return Version create view */ - def showCreatorWithMeta(author: String, slug: String, versionString: String) = + def showCreatorWithMeta(author: String, slug: String, versionString: String): Action[AnyContent] = UserLock(ShowProject(author, slug)).async { implicit request => val success = OptionT.fromOption[Future](this.factory.getPendingVersion(author, slug, versionString)) // Get pending version @@ -285,7 +287,7 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return New version view */ - def publish(author: String, slug: String, versionString: String) = { + def publish(author: String, slug: String, versionString: String): Action[AnyContent] = { UserLock(ShowProject(author, slug)).async { implicit request => // First get the pending Version this.factory.getPendingVersion(author, slug, versionString) match { @@ -381,18 +383,54 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return Versions page */ - def delete(author: String, slug: String, versionString: String) = { - VersionEditAction(author, slug).async { implicit request => - implicit val r = request.request - implicit val p = request.data.project - getVersion(p, versionString).map { version => + def delete(author: String, slug: String, versionString: String): Action[AnyContent] = { + (Authenticated andThen PermissionAction[AuthRequest](HardRemoveVersion)).async { implicit request => + implicit val r: Request[AnyContent] = request.request + getProjectVersion(author, slug, versionString).map { version => this.projects.deleteVersion(version) - UserActionLogger.log(request.request, LoggedAction.VersionDeleted, version.id.getOrElse(-1), "null", "") - Redirect(self.showList(author, slug, None, None)) + UserActionLogger.log(request, LoggedAction.VersionDeleted, version.id.getOrElse(-1), "null", "") + Redirect(self.showList(author, slug, None)) }.merge } } + /** + * Soft deletes the specified version. + * + * @param author Project owner + * @param slug Project slug + * @return Home page + */ + def softDelete(author: String, slug: String, versionString: String): Action[AnyContent] = VersionEditAction(author, slug).async { request => + implicit val oreRequest: AuthRequest[AnyContent] = request.request + val project: Project = request.data.project + val res = for { + comment <- bindFormEitherT[Future](this.forms.NeedsChanges)(_ => BadRequest) + version <- getVersion(project, versionString) + _ <- EitherT.right[Result](this.projects.prepareDeleteVersion(version)) + _ <- EitherT.right[Result](version.setVisibility(VisibilityTypes.SoftDelete, comment, request.user.id.get)) + } yield Redirect(self.showList(author, slug, None)) + + res.merge + } + + def showLog(author: String, slug: String, versionString: String): Action[AnyContent] = { + (Authenticated andThen PermissionAction[AuthRequest](ViewLogs)) andThen ProjectAction(author, slug) async { request => + implicit val r: OreRequest[AnyContent] = request.request + implicit val project: Project = request.data.project + val res = for { + version <- getVersion(project, versionString) + changes <- EitherT.right[Result](version.visibilityChangesByDate) + changedBy <- EitherT.right[Result](Future.sequence(changes.map(_.created.value))) + } yield { + val visChanges = changes zip changedBy + Ok(views.log(project, version, visChanges)) + } + + res.merge + } + } + /** * Sends the specified Project Version to the client. * @@ -401,10 +439,10 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version string * @return Sent file */ - def download(author: String, slug: String, versionString: String, token: Option[String]) = { + def download(author: String, slug: String, versionString: String, token: Option[String]): Action[AnyContent] = { ProjectAction(author, slug).async { implicit request => val project = request.data.project - implicit val r = request.request + implicit val r: OreRequest[AnyContent] = request.request getVersion(project, versionString).semiFlatMap { version => sendVersion(project, version, token) }.merge @@ -475,10 +513,11 @@ class Versions @Inject()(stats: StatTracker, slug: String, target: String, downloadType: Option[Int], - api: Option[Boolean]) = { + api: Option[Boolean]): Action[AnyContent] = { ProjectAction(author, slug).async { request => val dlType = downloadType.flatMap(i => DownloadTypes.values.find(_.id == i)).getOrElse(DownloadTypes.UploadedFile) - implicit val r = request.request + 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")) @@ -535,7 +574,7 @@ class Versions @Inject()(stats: StatTracker, } } - def confirmDownload(author: String, slug: String, target: String, downloadType: Option[Int], token: String) = { + def confirmDownload(author: String, slug: String, target: String, downloadType: Option[Int], token: String): Action[AnyContent] = { ProjectAction(author, slug) async { request => implicit val r: OreRequest[_] = request.request getVersion(request.data.project, target) @@ -603,7 +642,7 @@ class Versions @Inject()(stats: StatTracker, * @param slug Project slug * @return Sent file */ - def downloadRecommended(author: String, slug: String, token: Option[String]) = { + def downloadRecommended(author: String, slug: String, token: Option[String]): Action[AnyContent] = { ProjectAction(author, slug).async { implicit request => val data = request.data data.project.recommendedVersion.flatMap { rv => @@ -621,10 +660,10 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return Sent file */ - def downloadJar(author: String, slug: String, versionString: String, token: Option[String]) = { + def downloadJar(author: String, slug: String, versionString: String, token: Option[String]): Action[AnyContent] = { ProjectAction(author, slug).async { implicit request => val project = request.data.project - implicit val r = request.request + implicit val r: OreRequest[AnyContent] = request.request getVersion(project, versionString).semiFlatMap(version => sendJar(project, version, token)).merge } } @@ -677,7 +716,7 @@ class Versions @Inject()(stats: StatTracker, * @param slug Project slug * @return Sent file */ - def downloadRecommendedJar(author: String, slug: String, token: Option[String]) = { + def downloadRecommendedJar(author: String, slug: String, token: Option[String]): Action[AnyContent] = { ProjectAction(author, slug).async { implicit request => val data = request.data data.project.recommendedVersion.flatMap { rv => @@ -694,10 +733,10 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return Sent file */ - def downloadJarById(pluginId: String, versionString: String, optToken: Option[String]) = { + def downloadJarById(pluginId: String, versionString: String, optToken: Option[String]): Action[AnyContent] = { ProjectAction(pluginId).async { implicit request => val project = request.data.project - implicit val r = request.request + implicit val r: OreRequest[AnyContent] = request.request getVersion(project, versionString).semiFlatMap { version => optToken.map { token => confirmDownload0(version.id.get, Some(JarFile.id), token)(request.request).value.flatMap { _ => @@ -715,7 +754,7 @@ class Versions @Inject()(stats: StatTracker, * @param pluginId Project unique plugin ID * @return Sent file */ - def downloadRecommendedJarById(pluginId: String, token: Option[String]) = { + def downloadRecommendedJarById(pluginId: String, token: Option[String]): Action[AnyContent] = { ProjectAction(pluginId).async { implicit request => val data = request.data data.project.recommendedVersion.flatMap { rv => @@ -732,10 +771,10 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version string * @return Sent file */ - def downloadSignature(author: String, slug: String, versionString: String) = { + def downloadSignature(author: String, slug: String, versionString: String): Action[AnyContent] = { ProjectAction(author, slug).async { implicit request => val project = request.data.project - implicit val r = request.request + implicit val r: OreRequest[AnyContent] = request.request getVersion(project, versionString).map(sendSignatureFile(_, project)).merge } } @@ -747,9 +786,9 @@ class Versions @Inject()(stats: StatTracker, * @param versionString Version name * @return Sent file */ - def downloadSignatureById(pluginId: String, versionString: String) = ProjectAction(pluginId).async { implicit request => + def downloadSignatureById(pluginId: String, versionString: String): Action[AnyContent] = ProjectAction(pluginId).async { implicit request => val project = request.data.project - implicit val r = request.request + implicit val r: OreRequest[AnyContent] = request.request getVersion(project, versionString).map(sendSignatureFile(_, project)).merge } @@ -760,9 +799,9 @@ class Versions @Inject()(stats: StatTracker, * @param slug Project slug * @return Sent file */ - def downloadRecommendedSignature(author: String, slug: String) = ProjectAction(author, slug) async { implicit request => - implicit val data = request.data - implicit val r = request.request + def downloadRecommendedSignature(author: String, slug: String): Action[AnyContent] = ProjectAction(author, slug) async { implicit request => + implicit val data: ProjectData = request.data + implicit val r: OreRequest[AnyContent] = request.request request.data.project.recommendedVersion.map(sendSignatureFile(_, request.data.project)) } @@ -772,8 +811,8 @@ class Versions @Inject()(stats: StatTracker, * @param pluginId Project unique plugin ID * @return Sent file */ - def downloadRecommendedSignatureById(pluginId: String) = ProjectAction(pluginId).async { implicit request => - implicit val r = request.request + def downloadRecommendedSignatureById(pluginId: String): Action[AnyContent] = ProjectAction(pluginId).async { implicit request => + implicit val r: OreRequest[AnyContent] = request.request request.data.project.recommendedVersion.map(sendSignatureFile(_, request.data.project)) } diff --git a/app/controllers/sugar/ActionHelpers.scala b/app/controllers/sugar/ActionHelpers.scala index e0fb3ea07..5ed1942a5 100644 --- a/app/controllers/sugar/ActionHelpers.scala +++ b/app/controllers/sugar/ActionHelpers.scala @@ -19,7 +19,7 @@ trait ActionHelpers { * @param form Form with error * @return Redirect to call */ - def FormError(call: Call, form: Form[_]) = { + def FormError(call: Call, form: Form[_]): Result = { checkNotNull(call, "null call", "") checkNotNull(form, "null form", "") checkArgument(form.errors.nonEmpty, "no errors", "") diff --git a/app/controllers/sugar/Actions.scala b/app/controllers/sugar/Actions.scala index 1f7f894a4..5f9866a84 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 @@ -63,16 +64,16 @@ trait Actions extends Calls with ActionHelpers { * @tparam R Type of ScopedRequest that is being checked * @return The ScopedRequest as an instance of R */ - def PermissionAction[R[_] <: ScopedRequest[_]](p: Permission)(implicit ec: ExecutionContext) = new ActionRefiner[ScopedRequest, R] { - def executionContext = ec + def PermissionAction[R[_] <: ScopedRequest[_]](p: Permission)(implicit ec: ExecutionContext): ActionRefiner[ScopedRequest, R] = new ActionRefiner[ScopedRequest, R] { + def executionContext: ExecutionContext = ec - private def log(success: Boolean, request: ScopedRequest[_]) = { + private def log(success: Boolean, request: ScopedRequest[_]): Unit = { val lang = if (success) "GRANTED" else "DENIED" - PermsLogger.info(s" ${request.user.name}@${request.path.substring(1)}") + PermsLogger.debug(s" ${request.user.name}@${request.path.substring(1)}") } - def refine[A](request: ScopedRequest[A]) = { - implicit val r = request + def refine[A](request: ScopedRequest[A]): Future[Either[Result, R[A]]] = { + implicit val r: ScopedRequest[A] = request request.user can p in request.subject flatMap { perm => if (!perm) { @@ -95,7 +96,9 @@ trait Actions extends Calls with ActionHelpers { * @param p Permission to check * @return An [[ProjectRequest]] */ - def ProjectPermissionAction(p: Permission)(implicit ec: ExecutionContext) = PermissionAction[AuthedProjectRequest](p) + def ProjectPermissionAction(p: Permission)(implicit ec: ExecutionContext): ActionRefiner[ScopedRequest, AuthedProjectRequest] = PermissionAction[AuthedProjectRequest](p) + + /** * A PermissionAction that uses an AuthedOrganizationRequest for the @@ -104,7 +107,7 @@ trait Actions extends Calls with ActionHelpers { * @param p Permission to check * @return [[OrganizationRequest]] */ - def OrganizationPermissionAction(p: Permission)(implicit ec: ExecutionContext) = PermissionAction[AuthedOrganizationRequest](p) + def OrganizationPermissionAction(p: Permission)(implicit ec: ExecutionContext): ActionRefiner[ScopedRequest, AuthedOrganizationRequest] = PermissionAction[AuthedOrganizationRequest](p) implicit final class ResultWrapper(result: Result) { @@ -115,7 +118,7 @@ trait Actions extends Calls with ActionHelpers { * @param maxAge Maximum session age * @return Result with token */ - def authenticatedAs(user: User, maxAge: Int = -1)(implicit ec: ExecutionContext) = { + def authenticatedAs(user: User, maxAge: Int = -1)(implicit ec: ExecutionContext): Future[Result] = { val session = Actions.this.users.createSession(user) val age = if (maxAge == -1) None else Some(maxAge) session.map { s => @@ -128,7 +131,7 @@ trait Actions extends Calls with ActionHelpers { * * @return */ - def clearingSession() = result.discardingCookies(DiscardingCookie(AuthTokenName)) + def clearingSession(): Result = result.discardingCookies(DiscardingCookie(AuthTokenName)) } @@ -158,8 +161,8 @@ trait Actions extends Calls with ActionHelpers { // Implementation - def userLock(redirect: Call)(implicit ec: ExecutionContext) = new ActionFilter[AuthRequest] { - def executionContext = ec + def userLock(redirect: Call)(implicit ec: ExecutionContext): ActionFilter[AuthRequest] = new ActionFilter[AuthRequest] { + def executionContext: ExecutionContext = ec def filter[A](request: AuthRequest[A]): Future[Option[Result]] = Future.successful { if (!request.user.isLocked) None @@ -167,8 +170,8 @@ trait Actions extends Calls with ActionHelpers { } } - def verifiedAction(sso: Option[String], sig: Option[String])(implicit ec: ExecutionContext) = new ActionFilter[AuthRequest] { - def executionContext = ec + def verifiedAction(sso: Option[String], sig: Option[String])(implicit ec: ExecutionContext): ActionFilter[AuthRequest] = new ActionFilter[AuthRequest] { + def executionContext: ExecutionContext = ec def filter[A](request: AuthRequest[A]): Future[Option[Result]] = { val auth = for { @@ -184,8 +187,8 @@ trait Actions extends Calls with ActionHelpers { } } - def userAction(username: String)(implicit ec: ExecutionContext) = new ActionFilter[AuthRequest] { - def executionContext = ec + def userAction(username: String)(implicit ec: ExecutionContext): ActionFilter[AuthRequest] = new ActionFilter[AuthRequest] { + def executionContext: ExecutionContext = ec def filter[A](request: AuthRequest[A]): Future[Option[Result]] = { Actions.this.users.requestPermission(request.user, username, EditSettings).transform { @@ -195,17 +198,22 @@ trait Actions extends Calls with ActionHelpers { } } - def oreAction(implicit ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = new ActionTransformer[Request, OreRequest] { - def executionContext = ec + def oreAction(implicit ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) + : ActionTransformer[Request, OreRequest] = new ActionTransformer[Request, OreRequest] { + def executionContext: ExecutionContext = ec def transform[A](request: Request[A]): Future[OreRequest[A]] = { - implicit val service = users.service - HeaderData.of(request).map(new OreRequest(_, request)) + implicit val service: ModelService = users.service + HeaderData.of(request).map { data => + val requestWithLang = + data.currentUser.flatMap(_.lang).fold(request)(lang => request.addAttr(Messages.Attrs.CurrentLang, lang)) + new OreRequest(data, requestWithLang) + } } } - def authAction(implicit ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = new ActionRefiner[Request, AuthRequest] { - def executionContext = ec + def authAction(implicit ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): ActionRefiner[Request, AuthRequest] = new ActionRefiner[Request, AuthRequest] { + def executionContext: ExecutionContext = ec def refine[A](request: Request[A]): Future[Either[Result, AuthRequest[A]]] = maybeAuthRequest(request, users.current(request, ec)) @@ -215,26 +223,28 @@ trait Actions extends Calls with ActionHelpers { private def maybeAuthRequest[A](request: Request[A], futUser: OptionT[Future, User])(implicit ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): Future[Either[Result, AuthRequest[A]]] = { futUser.semiFlatMap { user => - implicit val service = users.service + implicit val service: ModelService = users.service HeaderData.of(request).map(hd => new AuthRequest[A](user, hd, request)) }.toRight(onUnauthorized(request, ec)).leftSemiFlatMap(identity).value } - def projectAction(author: String, slug: String)(implicit modelService: ModelService, ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = new ActionRefiner[OreRequest, ProjectRequest] { - def executionContext = ec + def projectAction(author: String, slug: String)(implicit modelService: ModelService, ec: ExecutionContext, + asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): ActionRefiner[OreRequest, ProjectRequest] = new ActionRefiner[OreRequest, ProjectRequest] { + def executionContext: ExecutionContext = ec - def refine[A](request: OreRequest[A]) = maybeProjectRequest(request, Actions.this.projects.withSlug(author, slug)) + def refine[A](request: OreRequest[A]): Future[Either[Result, ProjectRequest[A]]] = maybeProjectRequest(request, Actions.this.projects.withSlug(author, slug)) } - def projectAction(pluginId: String)(implicit modelService: ModelService, ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = new ActionRefiner[OreRequest, ProjectRequest] { + def projectAction(pluginId: String)(implicit modelService: ModelService, ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): ActionRefiner[OreRequest, ProjectRequest] = new ActionRefiner[OreRequest, ProjectRequest] { def executionContext: ExecutionContext = ec - def refine[A](request: OreRequest[A]) = maybeProjectRequest(request, Actions.this.projects.withPluginId(pluginId)) + def refine[A](request: OreRequest[A]): Future[Either[Result, ProjectRequest[A]]] = maybeProjectRequest(request, Actions.this.projects.withPluginId(pluginId)) } - private def maybeProjectRequest[A](r: OreRequest[A], project: OptionT[Future, Project])(implicit modelService: ModelService, + private def maybeProjectRequest[A](r: OreRequest[A], project: OptionT[Future, Project])(implicit + modelService: ModelService, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext): Future[Either[Result, ProjectRequest[A]]] = { - implicit val request = r + implicit val request: OreRequest[A] = r project.flatMap { p => processProject(p, request.data.currentUser) }.semiFlatMap { p => @@ -244,12 +254,14 @@ trait Actions extends Calls with ActionHelpers { }.toRight(notFound).value } - private def toProjectRequest[T](project: Project)(f: (ProjectData, ScopedProjectData) => T)(implicit request: OreRequest[_], + private def toProjectRequest[T](project: Project)(f: (ProjectData, ScopedProjectData) => T)(implicit + request: OreRequest[_], modelService: ModelService, ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = { (ProjectData.of(project), ScopedProjectData.of(request.data.currentUser, project)).parMapN(f) } - private def processProject(project: Project, user: Option[User])(implicit ec: ExecutionContext) : OptionT[Future, Project] = { + private def processProject(project: Project, user: Option[User])(implicit ec: ExecutionContext) : OptionT[Future, + Project] = { if (project.visibility == VisibilityTypes.Public || project.visibility == VisibilityTypes.New) { OptionT.pure[Future](project) } else { @@ -278,12 +290,12 @@ trait Actions extends Calls with ActionHelpers { } def authedProjectActionImpl(project: OptionT[Future, Project])(implicit modelService: ModelService, ec: ExecutionContext, - asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = new ActionRefiner[AuthRequest, AuthedProjectRequest] { + asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): ActionRefiner[AuthRequest, AuthedProjectRequest] = new ActionRefiner[AuthRequest, AuthedProjectRequest] { - def executionContext = ec + def executionContext: ExecutionContext = ec - def refine[A](request: AuthRequest[A]) = { - implicit val r = request + def refine[A](request: AuthRequest[A]): Future[Either[Result, AuthedProjectRequest[A]]] = { + implicit val r: AuthRequest[A] = request project.flatMap { pr => processProject(pr, Some(request.user)).semiFlatMap { p => @@ -296,18 +308,21 @@ trait Actions extends Calls with ActionHelpers { } def authedProjectAction(author: String, slug: String)(implicit modelService: ModelService, ec: ExecutionContext, - asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = authedProjectActionImpl(projects.withSlug(author, slug)) + asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): ActionRefiner[AuthRequest, AuthedProjectRequest] = authedProjectActionImpl(projects.withSlug(author, slug)) def authedProjectActionById(pluginId: String)(implicit modelService: ModelService, ec: ExecutionContext, - asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = authedProjectActionImpl(projects.withPluginId(pluginId)) + asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): ActionRefiner[AuthRequest, AuthedProjectRequest] = authedProjectActionImpl(projects.withPluginId(pluginId)) + + def organizationAction(organization: String)(implicit modelService: ModelService, ec: ExecutionContext, - asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = new ActionRefiner[OreRequest, OrganizationRequest] { + asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): ActionRefiner[OreRequest, OrganizationRequest] + = new ActionRefiner[OreRequest, OrganizationRequest] { - def executionContext = ec + def executionContext: ExecutionContext = ec - def refine[A](request: OreRequest[A]) = { - implicit val r = request + def refine[A](request: OreRequest[A]): Future[Either[Result, OrganizationRequest[A]]] = { + implicit val r: OreRequest[A] = request getOrga(request, organization).value.flatMap { maybeOrgaRequest(_) { case (data, scoped) => new OrganizationRequest[A](data, scoped, request) @@ -317,11 +332,11 @@ trait Actions extends Calls with ActionHelpers { } def authedOrganizationAction(organization: String)(implicit modelService: ModelService, ec: ExecutionContext, - asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = new ActionRefiner[AuthRequest, AuthedOrganizationRequest] { - def executionContext = ec + asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef): ActionRefiner[AuthRequest, AuthedOrganizationRequest] = new ActionRefiner[AuthRequest, AuthedOrganizationRequest] { + def executionContext: ExecutionContext = ec - def refine[A](request: AuthRequest[A]) = { - implicit val r = request + def refine[A](request: AuthRequest[A]): Future[Either[Result, AuthedOrganizationRequest[A]]] = { + implicit val r: AuthRequest[A] = request getOrga(request, organization).value.flatMap { maybeOrgaRequest(_) { case (data, scoped) => @@ -332,7 +347,8 @@ trait Actions extends Calls with ActionHelpers { } - private def maybeOrgaRequest[T](maybeOrga: Option[Organization])(f: (OrganizationData, ScopedOrganizationData) => T)(implicit request: OreRequest[_], + private def maybeOrgaRequest[T](maybeOrga: Option[Organization])(f: (OrganizationData, ScopedOrganizationData) => + T)(implicit request: OreRequest[_], modelService: ModelService, ec: ExecutionContext, asyncCacheApi: AsyncCacheApi, db: JdbcBackend#DatabaseDef) = { maybeOrga match { case None => Future.successful(Left(notFound)) diff --git a/app/controllers/sugar/Calls.scala b/app/controllers/sugar/Calls.scala index 2ed44aff2..f5227d86c 100644 --- a/app/controllers/sugar/Calls.scala +++ b/app/controllers/sugar/Calls.scala @@ -13,7 +13,7 @@ trait Calls { /** * A call to the home page. */ - val ShowHome: Call = routes.Application.showHome(None, None, None, None, None) + val ShowHome: Call = routes.Application.showHome(None, None, None, None, None, None) /** * A call to a [[User]] page. diff --git a/app/controllers/sugar/Requests.scala b/app/controllers/sugar/Requests.scala index 92f7d18c6..b6dc1265e 100644 --- a/app/controllers/sugar/Requests.scala +++ b/app/controllers/sugar/Requests.scala @@ -18,8 +18,8 @@ object Requests { * @param request the request to wrap */ class OreRequest[A](val data: HeaderData, val request: Request[A]) extends WrappedRequest[A](request) { - def currentUser = data.currentUser - def hasUser = data.currentUser.isDefined + def currentUser: Option[User] = data.currentUser + def hasUser: Boolean = data.currentUser.isDefined } /** Represents a Request with a [[User]] and [[ScopeSubject]] */ diff --git a/app/db/Model.scala b/app/db/Model.scala index 731904448..af5b782ff 100644 --- a/app/db/Model.scala +++ b/app/db/Model.scala @@ -2,6 +2,8 @@ package db import java.sql.Timestamp +import scala.concurrent.Future + import com.google.common.base.Preconditions.checkNotNull import db.table.ModelTable import db.table.key.Key @@ -36,12 +38,12 @@ abstract class Model(val id: Option[Int], val createdAt: Option[Timestamp]) { se * * @param key Model key to update */ - def update[A](key: Key[M, A]) = Defined(key.update(this.asInstanceOf[M])) + def update[A](key: Key[M, A]): Future[Int] = Defined(key.update(this.asInstanceOf[M])) /** * Removes this model from it's table. */ - def remove() = Defined(this.service.delete(this.asInstanceOf[M])) + def remove(): Future[Int] = Defined(this.service.delete(this.asInstanceOf[M])) /** * Returns true if this Project is defined in the database. @@ -81,7 +83,7 @@ abstract class Model(val id: Option[Int], val createdAt: Option[Timestamp]) { se */ def isProcessed: Boolean = this._isProcessed - protected[db] def setProcessed(processed: Boolean) = this._isProcessed = processed + protected[db] def setProcessed(processed: Boolean): Unit = this._isProcessed = processed protected def Defined[R](f: => R): R = { if (isDefined) diff --git a/app/db/ModelAction.scala b/app/db/ModelAction.scala index 7ecf08367..16f21e384 100644 --- a/app/db/ModelAction.scala +++ b/app/db/ModelAction.scala @@ -34,7 +34,7 @@ object ModelAction { */ case class ModelSeqAction[M <: Model](override val action: DBIOAction[Seq[M], NoStream, Nothing]) extends AbstractModelAction(action) { - def processResult(service: ModelService, result: Seq[M]) = for (model <- result) yield { + def processResult(service: ModelService, result: Seq[M]): Seq[M] = for (model <- result) yield { process(service, model) } } diff --git a/app/db/ModelFilter.scala b/app/db/ModelFilter.scala index 866812ac6..285ead1f9 100644 --- a/app/db/ModelFilter.scala +++ b/app/db/ModelFilter.scala @@ -91,7 +91,7 @@ object ModelFilter { def Empty[M <: Model]: ModelFilter[M] = ModelFilter[M](_ => true) - def All[M <: Model] = ModelFilter[M](_ => false) + def All[M <: Model]: ModelFilter[M] = ModelFilter[M](_ => false) /** Filters models by ID */ def IdFilter[M <: Model](id: Int): ModelFilter[M] = ModelFilter[M](_.id === id) diff --git a/app/db/ModelRegistry.scala b/app/db/ModelRegistry.scala index 80ab06a37..927b4811e 100644 --- a/app/db/ModelRegistry.scala +++ b/app/db/ModelRegistry.scala @@ -65,7 +65,7 @@ trait ModelRegistry { * * @param base ModelBase */ - def registerModelBase(base: ModelBase[_ <: Model]) = { + def registerModelBase(base: ModelBase[_ <: Model]): Unit = { checkNotNull(base, "model base is null", "") this.modelBases += base.getClass -> base } diff --git a/app/db/ModelSchema.scala b/app/db/ModelSchema.scala index e89367eae..f20e79439 100644 --- a/app/db/ModelSchema.scala +++ b/app/db/ModelSchema.scala @@ -50,7 +50,7 @@ class ModelSchema[M <: Model](val service: ModelService, def withAssociation[Assoc <: AssociativeTable, A <: Model](association: ModelAssociation[Assoc], selfReference: Assoc => Rep[Int], targetClass: Class[A], - targetReference: Assoc => Rep[Int]) = { + targetReference: Assoc => Rep[Int]): ModelSchema[M] = { val tableClass = association.tableClass this.associations += tableClass -> association this.associatedModels += tableClass -> targetClass @@ -89,7 +89,7 @@ class ModelSchema[M <: Model](val service: ModelService, * @tparam C Child model type * @return This schema instance */ - def withChildren[C <: Model](childClass: Class[C], ref: C#T => Rep[Int]) = { + def withChildren[C <: Model](childClass: Class[C], ref: C#T => Rep[Int]): ModelSchema[M] = { this.children += childClass -> ref.asInstanceOf[ModelTable[_] => Rep[Int]] this } @@ -119,7 +119,7 @@ class ModelSchema[M <: Model](val service: ModelService, * @tparam S Sibling model type * @return This schema instance */ - def withSibling[S <: Model](siblingClass: Class[S], ref: M => Int) = { + def withSibling[S <: Model](siblingClass: Class[S], ref: M => Int): ModelSchema[M] = { this.siblings += siblingClass -> ref this } diff --git a/app/db/ModelService.scala b/app/db/ModelService.scala index b476ffa21..d8494c503 100644 --- a/app/db/ModelService.scala +++ b/app/db/ModelService.scala @@ -49,7 +49,7 @@ trait ModelService { /** * Performs initialization code for the ModelService. */ - def start() = {} + def start(): Unit = {} /** * Returns a current Timestamp. @@ -154,7 +154,7 @@ trait ModelService { * @tparam A Value type * @tparam M Model type */ - def set[A, M <: Model](model: M, column: M#T => Rep[A], value: A)(implicit mapper: JdbcType[A]) = { + def set[A, M <: Model](model: M, column: M#T => Rep[A], value: A)(implicit mapper: JdbcType[A]): Future[Int] = { DB.db.run { (for { row <- newAction[M](model.getClass) @@ -172,7 +172,7 @@ trait ModelService { * @tparam A MappedType type * @tparam M Model type */ - def setMappedType[A <: MappedType[A], M <: Model](model: M, column: M#T => Rep[A], value: A) = { + def setMappedType[A <: MappedType[A], M <: Model](model: M, column: M#T => Rep[A], value: A): Future[Int] = { import value.mapper set(model, column, value) } diff --git a/app/db/access/ModelAccess.scala b/app/db/access/ModelAccess.scala index 497ae68be..e7c570491 100644 --- a/app/db/access/ModelAccess.scala +++ b/app/db/access/ModelAccess.scala @@ -147,6 +147,13 @@ class ModelAccess[M <: Model](val service: ModelService, */ def filterNot(filter: M#T => Rep[Boolean], limit: Int = -1, offset: Int = -1)(implicit ec: ExecutionContext): Future[Seq[M]] = this.filter(!filter(_), limit, offset) + /** + * Counts how many elements in this set fulfill some predicate. + * @param predicate The predicate to use + * @return The amount of elements that fulfill the predicate. + */ + def count(predicate: M#T => Rep[Boolean]): Future[Int] = this.service.count(modelClass, (this.baseFilter && predicate).fn) + /** * Returns a Seq of this set. * diff --git a/app/db/access/ModelAssociationAccess.scala b/app/db/access/ModelAssociationAccess.scala index b01938270..742c5f332 100644 --- a/app/db/access/ModelAssociationAccess.scala +++ b/app/db/access/ModelAssociationAccess.scala @@ -25,7 +25,7 @@ class ModelAssociationAccess[Assoc <: AssociativeTable, M <: Model](service: Mod this.assoc.assoc(this.parent, model).map(_ => model) } - override def remove(model: M) = this.assoc.disassoc(this.parent, model) + override def remove(model: M): Future[Int] = this.assoc.disassoc(this.parent, model) override def removeAll(filter: M#T => Rep[Boolean] = _ => true) = throw new UnsupportedOperationException diff --git a/app/db/impl/OreModelProcessor.scala b/app/db/impl/OreModelProcessor.scala index 093ce72d2..bf090f05f 100644 --- a/app/db/impl/OreModelProcessor.scala +++ b/app/db/impl/OreModelProcessor.scala @@ -22,7 +22,7 @@ class OreModelProcessor(service: ModelService, auth: SpongeAuthApi) extends ModelProcessor(service) { - override def process[M <: Model](model: M) = { + override def process[M <: Model](model: M): M = { super.process(model) match { case oreModel: OreModel => oreModel.userBase = this.users diff --git a/app/db/impl/OrePostgresDriver.scala b/app/db/impl/OrePostgresDriver.scala index a600ad9c6..952bc0709 100644 --- a/app/db/impl/OrePostgresDriver.scala +++ b/app/db/impl/OrePostgresDriver.scala @@ -6,7 +6,7 @@ import db.table.key.Aliases import models.project.TagColors.TagColor import models.project.VisibilityTypes.Visibility import models.project.{TagColors, VisibilityTypes} -import models.user.{LoggedActionContext, LoggedAction} +import models.user.{LoggedAction, LoggedActionContext} import ore.Colors import ore.Colors.Color import ore.permission.role.RoleTypes @@ -22,37 +22,41 @@ import ore.user.Prompts import ore.user.Prompts.Prompt 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. */ trait OrePostgresDriver extends ExPostgresProfile with PgArraySupport with PgAggFuncSupport with PgNetSupport { - override val api = OreDriver + override val api: OreDriver.type = OreDriver def pgjson = "jsonb" object OreDriver extends API with ArrayImplicits with NetImplicits with Aliases { - implicit val colorTypeMapper = MappedJdbcType.base[Color, Int](_.id, Colors.apply) - implicit val tagColorTypeMapper = MappedJdbcType.base[TagColor, Int](_.id, TagColors.apply) - implicit val roleTypeTypeMapper = MappedJdbcType.base[RoleType, Int](_.roleId, RoleTypes.withId) - implicit val roleTypeListTypeMapper = new AdvancedArrayJdbcType[RoleType]("int2", + implicit val colorTypeMapper : JdbcType[Color] with BaseTypedType[Color] = MappedJdbcType.base[Color, Int](_.id, Colors.apply) + implicit val tagColorTypeMapper : JdbcType[TagColor] with BaseTypedType[TagColor] = MappedJdbcType.base[TagColor, Int](_.id, TagColors.apply) + implicit val roleTypeTypeMapper : JdbcType[RoleType] with BaseTypedType[RoleType] = MappedJdbcType.base[RoleType, Int](_.roleId, RoleTypes.withId) + implicit val roleTypeListTypeMapper : DriverJdbcType[List[RoleType]] = new AdvancedArrayJdbcType[RoleType]("int2", str => utils.SimpleArrayUtils.fromString[RoleType](s => RoleTypes.withId(Integer.parseInt(s)))(str).orNull, value => utils.SimpleArrayUtils.mkString[RoleType](_.roleId.toString)(value) ).to(_.toList) - implicit val categoryTypeMapper = MappedJdbcType.base[Category, Int](_.id, Categories.apply) - implicit val flagReasonTypeMapper = MappedJdbcType.base[FlagReason, Int](_.id, FlagReasons.apply) - implicit val notificationTypeTypeMapper = MappedJdbcType.base[NotificationType, Int](_.id, NotificationTypes.apply) - implicit val promptTypeMapper = MappedJdbcType.base[Prompt, Int](_.id, Prompts.apply) - implicit val promptListTypeMapper = new AdvancedArrayJdbcType[Prompt]("int2", + implicit val categoryTypeMapper : JdbcType[Category] with BaseTypedType[Category] = MappedJdbcType.base[Category, Int](_.id, Categories.apply) + implicit val flagReasonTypeMapper : JdbcType[FlagReason] with BaseTypedType[FlagReason] = MappedJdbcType.base[FlagReason, Int](_.id, FlagReasons.apply) + implicit val notificationTypeTypeMapper : JdbcType[NotificationType] with BaseTypedType[NotificationType] = MappedJdbcType.base[NotificationType, Int](_.id, NotificationTypes.apply) + implicit val promptTypeMapper : JdbcType[Prompt] with BaseTypedType[Prompt] = MappedJdbcType.base[Prompt, Int](_.id, Prompts.apply) + implicit val promptListTypeMapper : DriverJdbcType[List[Prompt]] = new AdvancedArrayJdbcType[Prompt]("int2", str => utils.SimpleArrayUtils.fromString[Prompt](s => Prompts(Integer.parseInt(s)))(str).orNull, value => utils.SimpleArrayUtils.mkString[Prompt](_.id.toString)(value) ).to(_.toList) - implicit val downloadTypeTypeMapper = MappedJdbcType.base[DownloadType, Int](_.id, DownloadTypes.apply) - implicit val projectApiKeyTypeTypeMapper = MappedJdbcType.base[ProjectApiKeyType, Int](_.id, ProjectApiKeyTypes.apply) - implicit val visibiltyTypeMapper = MappedJdbcType.base[Visibility, Int](_.id, VisibilityTypes.withId) - implicit val loggedActionMapper = MappedJdbcType.base[LoggedAction, Int](_.value, LoggedAction.withValue) - implicit val loggedActionContextMapper = MappedJdbcType.base[LoggedActionContext, Int](_.value, LoggedActionContext.withValue) + implicit val downloadTypeTypeMapper : JdbcType[DownloadType] with BaseTypedType[DownloadType] = MappedJdbcType.base[DownloadType, Int](_.id, DownloadTypes.apply) + implicit val projectApiKeyTypeTypeMapper: JdbcType[ProjectApiKeyType] with BaseTypedType[ProjectApiKeyType] = MappedJdbcType.base[ProjectApiKeyType, Int](_.id, ProjectApiKeyTypes.apply) + 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 cc31eecca..36dfc5645 100644 --- a/app/db/impl/access/OrganizationBase.scala +++ b/app/db/impl/access/OrganizationBase.scala @@ -24,8 +24,7 @@ class OrganizationBase(override val service: ModelService, implicit val users: UserBase) extends ModelBase[Organization] { - override val modelClass = classOf[Organization] - implicit val lang = Lang.defaultLang + override val modelClass: Class[Organization] = classOf[Organization] val Logger = play.api.Logger("Organizations") @@ -38,29 +37,29 @@ class OrganizationBase(override val service: ModelService, * @return New organization if successful, None otherwise */ def create(name: String, ownerId: Int, members: Set[OrganizationRole])(implicit cache: AsyncCacheApi, ec: ExecutionContext): EitherT[Future, String, Organization] = { - Logger.info("Creating Organization...") - Logger.info("Name : " + name) - Logger.info("Owner ID : " + ownerId) - Logger.info("Members : " + members.size) + Logger.debug("Creating Organization...") + Logger.debug("Name : " + name) + Logger.debug("Owner ID : " + ownerId) + Logger.debug("Members : " + members.size) // Create the organization as a User on SpongeAuth. This will reserve the // name so that no new users or organizations can create an account with // that name. We will give the organization a dummy email for continuity. // By default we use "@ore.spongepowered.org". - Logger.info("Creating on SpongeAuth...") + Logger.debug("Creating on SpongeAuth...") val dummyEmail = name + '@' + this.config.orgs.get[String]("dummyEmailDomain") val spongeResult = this.auth.createDummyUser(name, dummyEmail, verified = true) // Check for error spongeResult.leftMap { err => - Logger.info(" " + err) + Logger.debug(" " + err) err }.semiFlatMap { spongeUser => - Logger.info(" " + spongeUser) + Logger.debug(" " + spongeUser) // Next we will create the Organization on Ore itself. This contains a // reference to the Sponge user ID, the organization's username and a // reference to the User owner of the organization. - Logger.info("Creating on Ore...") + Logger.debug("Creating on Ore...") this.add(Organization(id = Some(spongeUser.id), username = name, _ownerId = ownerId)) }.semiFlatMap { org => // Every organization model has a regular User companion. Organizations @@ -69,8 +68,6 @@ class OrganizationBase(override val service: ModelService, // and should be treated as such. for { userOrg <- org.toUser.getOrElse(throw new IllegalStateException("User not created")) - _ <- userOrg.pullForumData() - _ <- userOrg.pullSpongeData() _ = userOrg.setGlobalRoles(userOrg.globalRoles + RoleTypes.Organization) _ <- // Add the owner org.memberships.addRole(OrganizationRole( @@ -80,7 +77,7 @@ class OrganizationBase(override val service: ModelService, _isAccepted = true)) _ <- { // Invite the User members that the owner selected during creation. - Logger.info("Inviting members...") + Logger.debug("Inviting members...") Future.sequence(members.map { role => // TODO remove role.user db access we really only need the userid we already have for notifications @@ -88,13 +85,13 @@ 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) )) } }) } } yield { - Logger.info(" " + org) + Logger.debug(" " + org) org } } diff --git a/app/db/impl/access/ProjectBase.scala b/app/db/impl/access/ProjectBase.scala index a1c1904dd..8491d8746 100644 --- a/app/db/impl/access/ProjectBase.scala +++ b/app/db/impl/access/ProjectBase.scala @@ -6,11 +6,12 @@ import java.sql.Timestamp import java.util.Date import com.google.common.base.Preconditions._ + import db.impl.OrePostgresDriver.api._ import db.impl.{PageTable, ProjectTableMain, VersionTable} import db.{ModelBase, ModelService} import discourse.OreDiscourseApi -import models.project.{Channel, Project, Version} +import models.project.{Channel, Page, Project, Version, VisibilityTypes} import ore.project.io.ProjectFiles import ore.{OreConfig, OreEnv} import slick.lifted.TableQuery @@ -27,11 +28,11 @@ class ProjectBase(override val service: ModelService, forums: OreDiscourseApi) extends ModelBase[Project] { - override val modelClass = classOf[Project] + override val modelClass: Class[Project] = classOf[Project] val fileManager = new ProjectFiles(this.env) - implicit val self = this + implicit val self: ProjectBase = this def missingFile(implicit ec: ExecutionContext): Future[Seq[Version]] = { val tableVersion = TableQuery[VersionTable] @@ -110,7 +111,7 @@ class ProjectBase(override val service: ModelService, * * @param project Project to save icon for */ - def savePendingIcon(project: Project) = { + def savePendingIcon(project: Project): Unit = { this.fileManager.getPendingIconPath(project).foreach { iconPath => val iconDir = this.fileManager.getIconDir(project.ownerName, project.name) if (notExists(iconDir)) @@ -126,7 +127,7 @@ class ProjectBase(override val service: ModelService, * @param project Project to rename * @param name New name to assign Project */ - def rename(project: Project, name: String)(implicit ec: ExecutionContext) = { + def rename(project: Project, name: String)(implicit ec: ExecutionContext): Future[Boolean] = { val newName = compact(name) val newSlug = slugify(newName) checkArgument(this.config.isValidProjectName(name), "invalid name", "") @@ -175,24 +176,30 @@ class ProjectBase(override val service: ModelService, } } - /** - * Irreversibly deletes this version. - * - * @param project Project context - */ - def deleteVersion(version: Version)(implicit project: Project = null, ec: ExecutionContext) = { + def prepareDeleteVersion(version: Version)(implicit ec: ExecutionContext): Future[Project] = for { - proj <- if (project != null) Future.successful(project) else version.project - size <- proj.versions.size - _ = checkArgument(size > 1, "only one version", "") + proj <- version.project + size <- proj.versions.count(_.visibility === VisibilityTypes.Public) + _ = checkArgument(size > 1, "only one public version", "") _ = checkArgument(proj.id.get == version.projectId, "invalid context id", "") rv <- proj.recommendedVersion projects <- proj.versions.sorted(_.createdAt.desc) // TODO optimize: only query one version - _ = if (version.equals(rv)) proj.setRecommendedVersion(projects.filterNot(_.equals(version)).head) + _ = { + if (version == rv) + proj.setRecommendedVersion(projects.filter(v => v != version && !v.isDeleted).head) + } + } yield proj + + /** + * Irreversibly deletes this version. + */ + def deleteVersion(version: Version)(implicit ec: ExecutionContext): Future[Project] = { + for { + proj <- prepareDeleteVersion(version) channel <- version.channel noVersions <- channel.versions.isEmpty _ <- { - val versionDir = this.fileManager.getVersionDir(proj.ownerName, project.name, version.name) + val versionDir = this.fileManager.getVersionDir(proj.ownerName, proj.name, version.name) FileUtils.deleteDirectory(versionDir) version.remove() } @@ -208,7 +215,7 @@ class ProjectBase(override val service: ModelService, * * @param project Project to delete */ - def delete(project: Project)(implicit ec: ExecutionContext) = { + def delete(project: Project)(implicit ec: ExecutionContext): Future[Int] = { FileUtils.deleteDirectory(this.fileManager.getProjectDir(project.ownerName, project.name)) if (project.topicId != -1) this.forums.deleteProjectTopic(project) @@ -217,7 +224,7 @@ class ProjectBase(override val service: ModelService, } - def queryProjectPages(project: Project)(implicit ec: ExecutionContext) = { + def queryProjectPages(project: Project)(implicit ec: ExecutionContext): Future[Seq[(Page, Seq[Page])]] = { val tablePage = TableQuery[PageTable] val pagesQuery = for { (pp, p) <- tablePage joinLeft tablePage on (_.id === _.parentId) diff --git a/app/db/impl/access/UserBase.scala b/app/db/impl/access/UserBase.scala index f38f2898c..96039b706 100755 --- a/app/db/impl/access/UserBase.scala +++ b/app/db/impl/access/UserBase.scala @@ -31,9 +31,9 @@ class UserBase(override val service: ModelService, import UserBase._ - override val modelClass = classOf[User] + override val modelClass: Class[User] = classOf[User] - implicit val self = this + implicit val self: UserBase = this /** * Returns the user with the specified username. If the specified username @@ -119,7 +119,7 @@ class UserBase(override val service: ModelService, sort match { // Sort case ORDERING_JOIN_DATE => if(reverse) users.joinDate.asc else users.joinDate.desc case ORDERING_ROLE => if(reverse) users.globalRoles.asc else users.globalRoles.desc - case ORDERING_USERNAME | _ => if(reverse) users.name.asc else users.joinDate.desc + case ORDERING_USERNAME | _ => if(reverse) users.name.asc else users.name.desc } } .drop(offset) diff --git a/app/db/impl/model/common/Hideable.scala b/app/db/impl/model/common/Hideable.scala index f1b2ffb1e..fb31eca22 100644 --- a/app/db/impl/model/common/Hideable.scala +++ b/app/db/impl/model/common/Hideable.scala @@ -1,8 +1,16 @@ package db.impl.model.common +import java.sql.Timestamp +import java.time.Instant + +import scala.concurrent.{ExecutionContext, Future} + import db.Model +import db.access.ModelAccess import db.impl.table.common.VisibilityColumn +import models.project.VisibilityTypes import models.project.VisibilityTypes.Visibility +import util.functional.OptionT /** * Represents a [[Model]] that has a toggleable visibility. @@ -11,6 +19,7 @@ trait Hideable extends Model { self => override type M <: Hideable { type M = self.M } override type T <: VisibilityColumn[M] + type ModelVisibilityChange <: VisibilityChange /** * Returns true if the [[Model]] is visible. @@ -19,4 +28,30 @@ trait Hideable extends Model { self => */ def visibility: Visibility + def isDeleted: Boolean = visibility == VisibilityTypes.SoftDelete + + /** + * Sets whether this project is visible. + * + * @param visibility True if visible + */ + def setVisibility(visibility: Visibility, comment: String, creator: Int)(implicit ec: ExecutionContext): Future[ModelVisibilityChange] + + /** + * Get VisibilityChanges + */ + def visibilityChanges: ModelAccess[ModelVisibilityChange] + + def visibilityChangesByDate(implicit ec: ExecutionContext): Future[Seq[ModelVisibilityChange]] = + visibilityChanges.all.map(_.toSeq.sortWith(byCreationDate)) + + def byCreationDate(first: ModelVisibilityChange, second: ModelVisibilityChange): Boolean = + first.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime < second.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime + + def lastVisibilityChange(implicit ec: ExecutionContext): OptionT[Future, ModelVisibilityChange] = + OptionT(visibilityChanges.all.map(_.toSeq.filter(cr => !cr.isResolved).sortWith(byCreationDate).headOption)) + + def lastChangeRequest(implicit ec: ExecutionContext): OptionT[Future, ModelVisibilityChange] = + OptionT(visibilityChanges.all.map(_.toSeq.filter(cr => cr.visibility == VisibilityTypes.NeedsChanges.id).sortWith(byCreationDate).lastOption)) + } diff --git a/app/db/impl/model/common/VisibilityChange.scala b/app/db/impl/model/common/VisibilityChange.scala new file mode 100644 index 000000000..2dbb50d59 --- /dev/null +++ b/app/db/impl/model/common/VisibilityChange.scala @@ -0,0 +1,27 @@ +package db.impl.model.common + +import java.sql.Timestamp + +import scala.concurrent.{ExecutionContext, Future} + +import db.Model +import db.impl.table.common.VisibilityChangeColumns +import models.user.User +import util.functional.OptionT + +trait VisibilityChange extends Model { self => + + type M <: VisibilityChange { type M = self.M } + type T <: VisibilityChangeColumns[M] + + def createdBy: Option[Int] + def comment: String + def resolvedAt: Option[Timestamp] + def resolvedBy: Option[Int] + def visibility: Int + + def created(implicit ec: ExecutionContext): OptionT[Future, User] + + /** Check if the change has been dealt with */ + def isResolved: Boolean = resolvedAt.isDefined +} diff --git a/app/db/impl/schema.scala b/app/db/impl/schema.scala index a6550d736..32acd0ca8 100755 --- a/app/db/impl/schema.scala +++ b/app/db/impl/schema.scala @@ -6,15 +6,15 @@ import com.github.tminglei.slickpg.InetString import db.impl.OrePostgresDriver.api._ import db.impl.schema._ import db.impl.table.StatTable -import db.impl.table.common.{DescriptionColumn, DownloadsColumn, VisibilityColumn} +import db.impl.table.common.{DescriptionColumn, DownloadsColumn, VisibilityChangeColumns, VisibilityColumn} import db.table.{AssociativeTable, ModelTable, NameColumn} -import models.admin.{ProjectLog, ProjectLogEntry, Review, VisibilityChange} +import models.admin._ import models.api.ProjectApiKey import models.project.TagColors.TagColor import models.project._ import models.statistic.{ProjectView, VersionDownload} import models.user.role.{OrganizationRole, ProjectRole, RoleModel} -import models.user.{LoggedActionContext, Notification, Organization, SignOn, User, LoggedAction, LoggedActionModel, Session => DbSession} +import models.user.{LoggedAction, LoggedActionContext, LoggedActionModel, Notification, Organization, SignOn, User, Session => DbSession} import ore.Colors.Color import ore.permission.role.RoleTypes.RoleType import ore.project.Categories.Category @@ -23,6 +23,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 @@ -162,7 +163,8 @@ class TagTable(tag: RowTag) extends ModelTable[ProjectTag](tag, "project_tags") class VersionTable(tag: RowTag) extends ModelTable[Version](tag, "project_versions") with DownloadsColumn[Version] - with DescriptionColumn[Version] { + with DescriptionColumn[Version] + with VisibilityColumn[Version] { def versionString = column[String]("version_string") def dependencies = column[List[String]]("dependencies") @@ -178,10 +180,11 @@ class VersionTable(tag: RowTag) extends ModelTable[Version](tag, "project_versio def fileName = column[String]("file_name") def signatureFileName = column[String]("signature_file_name") def tagIds = column[List[Int]]("tags") + def isNonReviewed = column[Boolean]("is_non_reviewed") override def * = (id.?, createdAt.?, projectId, versionString, dependencies, assets.?, channelId, fileSize, hash, authorId, description.?, downloads, isReviewed, reviewerId, approvedAt.?, - tagIds, fileName, signatureFileName) <> ((Version.apply _).tupled, Version.unapply) + tagIds, visibility, fileName, signatureFileName, isNonReviewed) <> ((Version.apply _).tupled, Version.unapply) } class DownloadWarningsTable(tag: RowTag) extends ModelTable[DownloadWarning](tag, "project_version_download_warnings") { @@ -232,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) } @@ -321,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) } @@ -364,16 +368,13 @@ class ReviewTable(tag: RowTag) extends ModelTable[Review](tag, "project_version_ override def * = (id.?, createdAt.?, versionId, userId, endedAt.?, comment) <> ((Review.apply _).tupled, Review.unapply) } -class VisibilityChangeTable(tag: RowTag) extends ModelTable[VisibilityChange](tag, "project_visibility_changes") { +class ProjectVisibilityChangeTable(tag: RowTag) + extends ModelTable[ProjectVisibilityChange](tag, "project_visibility_changes") + with VisibilityChangeColumns[ProjectVisibilityChange] { - def createdBy = column[Int]("created_by") - def projectId = column[Int]("project_id") - def comment = column[String]("comment") - def resolvedAt = column[Timestamp]("resolved_at") - def resolvedBy = column[Int]("resolved_by") - def visibility = column[Int]("visibility") + def projectId = column[Int]("project_id") - override def * = (id.?, createdAt.?, createdBy.?, projectId, comment, resolvedAt.?, resolvedBy.?, visibility) <> (VisibilityChange.tupled, VisibilityChange.unapply) + override def * = (id.?, createdAt.?, createdBy.?, projectId, comment, resolvedAt.?, resolvedBy.?, visibility) <> (ProjectVisibilityChange.tupled, ProjectVisibilityChange.unapply) } class LoggedActionTable(tag: RowTag) extends ModelTable[LoggedActionModel](tag, "logged_actions") { @@ -388,3 +389,48 @@ class LoggedActionTable(tag: RowTag) extends ModelTable[LoggedActionModel](tag, override def * = (id.?, createdAt.?, userId, address, action, actionContext, actionContextId, newState, oldState) <> (LoggedActionModel.tupled, LoggedActionModel.unapply) } +class VersionVisibilityChangeTable(tag: RowTag) + extends ModelTable[VersionVisibilityChange](tag, "project_version_visibility_changes") + with VisibilityChangeColumns[VersionVisibilityChange] { + + def versionId = column[Int]("version_id") + + override def * = (id.?, createdAt.?, createdBy.?, versionId, comment, resolvedAt.?, resolvedBy.?, visibility) <> (VersionVisibilityChange.tupled, VersionVisibilityChange.unapply) +} + +class LoggedActionViewTable(tag: RowTag) extends ModelTable[LoggedActionViewModel](tag, "v_logged_actions") { + + def userId = column[Int]("user_id") + def address = column[InetString]("address") + def action = column[LoggedAction]("action") + def actionContext = column[LoggedActionContext]("action_context") + def actionContextId = column[Int]("action_context_id") + def newState = column[String]("new_state") + def oldState = column[String]("old_state") + def uId = column[Int]("u_id") + def uName = column[String]("u_name") + def pId = column[Int]("p_id") + def pPluginId = column[String]("p_plugin_id") + def pSlug = column[String]("p_slug") + def pOwnerName = column[String]("p_owner_name") + def pvId = column[Int]("pv_id") + def pvVersionString = column[String]("pv_version_string") + def ppId = column[Int]("pp_id") + def ppSlug = column[String]("pp_slug") + def sId = column[Int]("s_id") + def sName = column[String]("s_name") + def filterProject = column[Int]("filter_project") + def filterVersion = column[Int]("filter_version") + def filterPage = column[Int]("filter_page") + def filterSubject = column[Int]("filter_subject") + def filterAction = column[Int]("filter_action") + + override def * = (id.?, createdAt.?, userId, address, action, actionContext, actionContextId, newState, oldState, uId, + uName, loggedProjectProjection, loggedProjectVersionProjection, loggedProjectPageProjection, loggedSubjectProjection, + filterProject.?, filterVersion.?, filterPage.?, filterSubject.?, filterAction.?) <> (LoggedActionViewModel.tupled, LoggedActionViewModel.unapply) + + def loggedProjectProjection = (pId.?, pPluginId.?, pSlug.?, pOwnerName.?) <> ((LoggedProject.apply _).tupled, LoggedProject.unapply) + def loggedProjectVersionProjection = (pvId.?, pvVersionString.?) <> ((LoggedProjectVersion.apply _).tupled, LoggedProjectVersion.unapply) + def loggedProjectPageProjection = (ppId.?, ppSlug.?) <> ((LoggedProjectPage.apply _).tupled, LoggedProjectPage.unapply) + def loggedSubjectProjection = (sId.?, sName.?) <> ((LoggedSubject.apply _).tupled, LoggedSubject.unapply) +} diff --git a/app/db/impl/service/OreModelConfig.scala b/app/db/impl/service/OreModelConfig.scala index bffb422bb..3e159c044 100644 --- a/app/db/impl/service/OreModelConfig.scala +++ b/app/db/impl/service/OreModelConfig.scala @@ -5,7 +5,7 @@ import db.impl._ import db.impl.schema._ import db.table.ModelAssociation import db.{ModelSchema, ModelService} -import models.admin.{ProjectLog, ProjectLogEntry, Review, VisibilityChange} +import models.admin.{ProjectLog, ProjectLogEntry, ProjectVisibilityChange, Review, VersionVisibilityChange} import models.api.ProjectApiKey import models.project._ import models.statistic.{ProjectView, VersionDownload} @@ -32,7 +32,7 @@ trait OreModelConfig extends ModelService with OreDBOs { // Begin schemas - val UserSchema = new UserSchema(this) + val UserSchema: ModelSchema[User] = new UserSchema(this) .withChildren[Project](classOf[Project], _.userId) .withChildren[ProjectRole](classOf[ProjectRole], _.userId) .withChildren[OrganizationRole](classOf[OrganizationRole], _.userId) @@ -66,9 +66,10 @@ trait OreModelConfig extends ModelService with OreDBOs { val ProjectRolesSchema = new ModelSchema[ProjectRole](this, classOf[ProjectRole], TableQuery[ProjectRoleTable]) - val VisibilityChangeSchema = new ModelSchema[VisibilityChange](this, classOf[VisibilityChange], TableQuery[VisibilityChangeTable]) + val ProjectVisibilityChangeSchema = new ModelSchema[ProjectVisibilityChange](this, classOf[ProjectVisibilityChange], TableQuery[ProjectVisibilityChangeTable]) + val VersionVisibilityChangeSchema = new ModelSchema[VersionVisibilityChange](this, classOf[VersionVisibilityChange], TableQuery[VersionVisibilityChangeTable]) - val ProjectSchema = new ProjectSchema(this, Users) + val ProjectSchema: ModelSchema[Project] = new ProjectSchema(this, Users) .withChildren[Channel](classOf[Channel], _.projectId) .withChildren[Version](classOf[Version], _.projectId) .withChildren[Page](classOf[Page], _.projectId) @@ -76,7 +77,7 @@ trait OreModelConfig extends ModelService with OreDBOs { .withChildren[ProjectRole](classOf[ProjectRole], _.projectId) .withChildren[ProjectView](classOf[ProjectView], _.modelId) .withChildren[ProjectApiKey](classOf[ProjectApiKey], _.projectId) - .withChildren[VisibilityChange](classOf[VisibilityChange], _.projectId) + .withChildren[ProjectVisibilityChange](classOf[ProjectVisibilityChange], _.projectId) .withAssociation[ProjectWatchersTable, User]( association = this.projectWatchers, selfReference = _.projectId, @@ -96,7 +97,7 @@ trait OreModelConfig extends ModelService with OreDBOs { val ProjectSettingsSchema = new ModelSchema[ProjectSettings](this, classOf[ProjectSettings], TableQuery[ProjectSettingsTable]) - val ProjectLogSchema = new ModelSchema[ProjectLog](this, classOf[ProjectLog], TableQuery[ProjectLogTable]) + val ProjectLogSchema: ModelSchema[ProjectLog] = new ModelSchema[ProjectLog](this, classOf[ProjectLog], TableQuery[ProjectLogTable]) .withChildren[ProjectLogEntry](classOf[ProjectLogEntry], _.logId) val ProjectLogEntrySchema = new ModelSchema[ProjectLogEntry]( @@ -109,9 +110,10 @@ trait OreModelConfig extends ModelService with OreDBOs { val ReviewSchema = new ModelSchema[Review](this, classOf[Review], TableQuery[ReviewTable]) - val VersionSchema = new VersionSchema(this) + val VersionSchema: ModelSchema[Version] = new VersionSchema(this) .withChildren[VersionDownload](classOf[VersionDownload], _.modelId) .withChildren[Review](classOf[Review], _.versionId) + .withChildren[VersionVisibilityChange](classOf[VersionVisibilityChange], _.versionId) val DownloadWarningSchema = new ModelSchema[DownloadWarning]( this, classOf[DownloadWarning], TableQuery[DownloadWarningsTable]) @@ -122,7 +124,7 @@ trait OreModelConfig extends ModelService with OreDBOs { case object DownloadSchema extends ModelSchema[VersionDownload]( this, classOf[VersionDownload], TableQuery[VersionDownloadsTable]) with StatSchema[VersionDownload] - val ChannelSchema = new ModelSchema[Channel](this, classOf[Channel], TableQuery[ChannelTable]) + val ChannelSchema: ModelSchema[Channel] = new ModelSchema[Channel](this, classOf[Channel], TableQuery[ChannelTable]) .withChildren[Version](classOf[Version], _.channelId) val TagSchema = new ModelSchema[ProjectTag](this, classOf[ProjectTag], TableQuery[TagTable]) @@ -131,7 +133,7 @@ trait OreModelConfig extends ModelService with OreDBOs { val NotificationSchema = new ModelSchema[Notification](this, classOf[Notification], TableQuery[NotificationTable]) - val OrganizationSchema = new ModelSchema[Organization](this, classOf[Organization], TableQuery[OrganizationTable]) + val OrganizationSchema: ModelSchema[Organization] = new ModelSchema[Organization](this, classOf[Organization], TableQuery[OrganizationTable]) .withChildren[Project](classOf[Project], _.userId) .withChildren[OrganizationRole](classOf[OrganizationRole], _.organizationId) .withAssociation[OrganizationMembersTable, User]( diff --git a/app/db/impl/service/OreModelService.scala b/app/db/impl/service/OreModelService.scala index 99577143c..e099d557d 100644 --- a/app/db/impl/service/OreModelService.scala +++ b/app/db/impl/service/OreModelService.scala @@ -30,16 +30,16 @@ class OreModelService @Inject()(override val env: OreEnv, val Logger = play.api.Logger("Database") // Implement ModelService - override lazy val registry = new ModelRegistry {} + override lazy val registry: ModelRegistry = new ModelRegistry {} override lazy val processor = new OreModelProcessor( this, Users, Projects, Organizations, this.config, this.forums, this.auth) - override lazy val driver = OrePostgresDriver + override lazy val driver: OrePostgresDriver.type = OrePostgresDriver override lazy val DB = db.get[JdbcProfile] override lazy val DefaultTimeout: Duration = this.config.app.get[Int]("db.default-timeout").seconds import registry.{registerModelBase, registerSchema} - override def start() = { + override def start(): Unit = { val time = System.currentTimeMillis() // Initialize database access objects @@ -70,8 +70,9 @@ class OreModelService @Inject()(override val env: OreEnv, registerSchema(OrganizationSchema) registerSchema(OrganizationRoleSchema) registerSchema(ProjectApiKeySchema) - registerSchema(VisibilityChangeSchema) registerSchema(UserActionLogSchema) + registerSchema(ProjectVisibilityChangeSchema) + registerSchema(VersionVisibilityChangeSchema) Logger.info( "Database initialized:\n" + diff --git a/app/db/impl/table/ModelKeys.scala b/app/db/impl/table/ModelKeys.scala index 4d94b70b1..0b0c16aee 100755 --- a/app/db/impl/table/ModelKeys.scala +++ b/app/db/impl/table/ModelKeys.scala @@ -2,9 +2,9 @@ package db.impl.table import db.Named import db.impl.OrePostgresDriver.api._ -import db.impl.model.common.{Describable, Downloadable} +import db.impl.model.common.{Describable, Downloadable, Hideable, VisibilityChange} import db.table.key._ -import models.admin.{ProjectLogEntry, Review, VisibilityChange} +import models.admin.{ProjectLogEntry, Review} import models.project.VisibilityTypes.Visibility import models.project._ import models.statistic.StatEntry @@ -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. @@ -24,7 +25,7 @@ object ModelKeys { val Name = new StringKey[Named](_.name, _.name) val Downloads = new IntKey[Downloadable](_.downloads, _.downloadCount) val Description = new StringKey[Describable](_.description, _.description.orNull) - val Visibility = new MappedTypeKey[Project, Visibility](_.visibility, _.visibility) + val Visibility = new MappedTypeKey[Hideable, Visibility](_.visibility, _.visibility) // Project val OwnerId = new IntKey[Project](_.userId, _.ownerId) @@ -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) @@ -77,6 +79,7 @@ object ModelKeys { val ApprovedAt = new TimestampKey[Version](_.approvedAt, _.approvedAt.orNull) val ChannelId = new IntKey[Version](_.channelId, _.channelId) val TagIds = new Key[Version, List[Int]](_.tagIds, _.tagIds) + val IsNonReviewedVersion = new BooleanKey[Version](_.isNonReviewed, _.isNonReviewed) // Tags val TagVersionIds = new Key[models.project.Tag, List[Int]](_.versionIds, _.versionIds) diff --git a/app/db/impl/table/common/VisibilityChangeColumns.scala b/app/db/impl/table/common/VisibilityChangeColumns.scala new file mode 100644 index 000000000..7e5985fe1 --- /dev/null +++ b/app/db/impl/table/common/VisibilityChangeColumns.scala @@ -0,0 +1,16 @@ +package db.impl.table.common + +import java.sql.Timestamp + +import db.impl.OrePostgresDriver.api._ +import db.impl.model.common.VisibilityChange +import db.table.ModelTable + +trait VisibilityChangeColumns[M <: VisibilityChange] extends ModelTable[M] { + + def createdBy = column[Int]("created_by") + def comment = column[String]("comment") + def resolvedAt = column[Timestamp]("resolved_at") + def resolvedBy = column[Int]("resolved_by") + def visibility = column[Int]("visibility") +} diff --git a/app/db/table/ModelAssociation.scala b/app/db/table/ModelAssociation.scala index 04702dfd8..c6b1d493b 100644 --- a/app/db/table/ModelAssociation.scala +++ b/app/db/table/ModelAssociation.scala @@ -1,5 +1,7 @@ package db.table +import scala.concurrent.Future + import com.google.common.base.Preconditions._ import db.impl.OrePostgresDriver.api._ import db.{Model, ModelService} @@ -32,7 +34,7 @@ class ModelAssociation[AssocTable <: AssociativeTable] * @param model2 Second model * @return Future results */ - def assoc(model1: Model, model2: Model) = { + def assoc(model1: Model, model2: Model): Future[Int] = { val modelPair = orderModels(model1, model2) this.service.DB.db.run(this.assocTable += (modelPair._1.id.get, modelPair._2.id.get)) } @@ -43,7 +45,7 @@ class ModelAssociation[AssocTable <: AssociativeTable] * @param model1 First model * @param model2 Second model */ - def disassoc(model1: Model, model2: Model) = { + def disassoc(model1: Model, model2: Model): Future[Int] = { val modelPair = orderModels(model1, model2) this.service.DB.db.run { this.assocTable.filter(t => ref1(t) === modelPair._1.id.get && ref2(t) === modelPair._2.id.get).delete diff --git a/app/db/table/key/Key.scala b/app/db/table/key/Key.scala index 9b531dd1a..ac7b64891 100644 --- a/app/db/table/key/Key.scala +++ b/app/db/table/key/Key.scala @@ -1,5 +1,7 @@ package db.table.key +import scala.concurrent.Future + import db.Model import db.impl.OrePostgresDriver.api._ import slick.jdbc.JdbcType @@ -11,6 +13,6 @@ import slick.jdbc.JdbcType */ class Key[M <: Model, A](val ref: M#T => Rep[A], val getter: M => A)(implicit mapper: JdbcType[A]) { - def update(model: M) = model.service.set(model, this.ref, getter(model)) + def update(model: M): Future[Int] = model.service.set(model, this.ref, getter(model)) } diff --git a/app/db/table/key/MappedTypeKey.scala b/app/db/table/key/MappedTypeKey.scala index c80a71f75..76fb6e74c 100644 --- a/app/db/table/key/MappedTypeKey.scala +++ b/app/db/table/key/MappedTypeKey.scala @@ -1,5 +1,7 @@ package db.table.key +import scala.concurrent.Future + import db.Model import db.impl.OrePostgresDriver.api._ import db.table.MappedType @@ -8,6 +10,6 @@ class MappedTypeKey[M <: Model, A <: MappedType[A]](override val ref: M#T => Rep override val getter: M => A) extends Key[M, A](ref, getter)(mapper = null) { - override def update(model: M) = model.service.setMappedType(model, this.ref, this.getter(model)) + override def update(model: M): Future[Int] = model.service.setMappedType(model, this.ref, this.getter(model)) } diff --git a/app/discourse/OreDiscourseApi.scala b/app/discourse/OreDiscourseApi.scala index 944e412f1..f867c3457 100644 --- a/app/discourse/OreDiscourseApi.scala +++ b/app/discourse/OreDiscourseApi.scala @@ -5,7 +5,7 @@ import java.nio.file.Path import akka.actor.Scheduler import com.google.common.base.Preconditions.{checkArgument, checkNotNull} import db.impl.access.ProjectBase -import models.project.{Project, Version} +import models.project.{Project, Version, VisibilityTypes} import models.user.User import org.spongepowered.play.discourse.DiscourseApi import util.StringUtils._ @@ -27,8 +27,12 @@ trait OreDiscourseApi extends DiscourseApi { var projects: ProjectBase = _ var isEnabled = true - /** The category where projects are posted to */ - val categorySlug: String + /** Username of admin account to move topics to secured categories */ + val admin: String + /** The category where project topics are posted to */ + val categoryDefault: Int + /** The category where deleted project topics are moved to */ + val categoryDeleted: Int /** Path to project topic template */ val topicTemplatePath: Path /** Path to version release template */ @@ -78,7 +82,7 @@ trait OreDiscourseApi extends DiscourseApi { poster = project.ownerName, title = title, content = content, - categorySlug = this.categorySlug + categoryId = Some(this.categoryDefault) ).andThen { case Success(errorsOrTopic) => errorsOrTopic match { case Left(errors) => @@ -103,7 +107,7 @@ trait OreDiscourseApi extends DiscourseApi { project.setTopicId(topic.topicId) project.setPostId(topic.postId) - Logger.info( + Logger.debug( s"New project topic:\n" + s"Project: ${project.url}\n" + s"Topic ID: ${project.topicId}\n" + @@ -112,8 +116,8 @@ trait OreDiscourseApi extends DiscourseApi { resultPromise.success(true) } case Failure(_) => - // Discourse never received our request! Try again later. - Logger.info(s"Could not create project topic for project ${project.url}. Rescheduling...") + // Something went wrong. Turn on debug mode to gez debug messages from play discourse for further investigations. + Logger.warn(s"Could not create project topic for project ${project.url}. Rescheduling...") resultPromise.success(false) } @@ -145,7 +149,7 @@ trait OreDiscourseApi extends DiscourseApi { // A promise for our final result val resultPromise: Promise[Boolean] = Promise() - def logErrors(errors: List[String]) = { + def logErrors(errors: List[String]): Unit = { val message = "Request to update project topic was successful but Discourse responded with errors:\n" + s"Project: ${project.url}\n" + s"Topic ID: $topicId\n" + @@ -156,7 +160,7 @@ trait OreDiscourseApi extends DiscourseApi { } def fail(message: String) = { - Logger.info(s"Couldn't update project topic for project ${project.url}: " + message) + Logger.warn(s"Couldn't update project topic for project ${project.url}: " + message) resultPromise.success(false) } @@ -164,7 +168,8 @@ trait OreDiscourseApi extends DiscourseApi { updateTopic( username = ownerName, topicId = topicId, - title = title + title = Some(title), + categoryId = None ).andThen { case Success(errors) => if (errors.nonEmpty) { @@ -184,7 +189,7 @@ trait OreDiscourseApi extends DiscourseApi { resultPromise.success(false) } else { // Title and content updated! - Logger.info(s"Project topic updated for ${project.url}.") + Logger.debug(s"Project topic updated for ${project.url}.") project.setTopicDirty(false) resultPromise.success(true) } @@ -251,6 +256,27 @@ trait OreDiscourseApi extends DiscourseApi { } } + def changeTopicVisibility(project: Project, isVisible: Boolean)(implicit ec: ExecutionContext): Future[Boolean] = { + if (!this.isEnabled) + return Future.successful(true) + + checkArgument(project.id.isDefined, "undefined project", "") + checkArgument(project.topicId != -1, "undefined topic id", "") + + val resultPromise: Promise[Boolean] = Promise() + updateTopic(this.admin, project.topicId, None, Some(if (isVisible) this.categoryDefault else this.categoryDeleted)).foreach { list => + if(list.isEmpty) { + Logger.debug(s"Successfully updated topic category for project: ${project.url}.") + resultPromise.success(true) + } else { + Logger.warn(s"Couldn't hide topic for project: ${project.url}. Message: " + list.mkString(" | ")) + resultPromise.success(false) + } + } + + resultPromise.future + } + /** * Delete's a [[Project]]'s forum topic. * @@ -263,10 +289,10 @@ trait OreDiscourseApi extends DiscourseApi { checkArgument(project.id.isDefined, "undefined project", "") checkArgument(project.topicId != -1, "undefined topic id", "") - def logFailure() = Logger.info(s"Couldn't delete topic for project: ${project.url}. Rescheduling...") + def logFailure(): Unit = Logger.warn(s"Couldn't delete topic for project: ${project.url}. Rescheduling...") val resultPromise: Promise[Boolean] = Promise() - deleteTopic(project.ownerName, project.topicId).andThen { + deleteTopic(this.admin, project.topicId).andThen { case Success(result) => if(!result) { logFailure() @@ -274,7 +300,7 @@ trait OreDiscourseApi extends DiscourseApi { } else { project.setTopicId(-1) project.setPostId(-1) - Logger.info(s"Successfully deleted project topic for: ${project.url}.") + Logger.debug(s"Successfully deleted project topic for: ${project.url}.") resultPromise.success(true) } case Failure(e) => @@ -310,10 +336,10 @@ trait OreDiscourseApi extends DiscourseApi { class Templates { /** Creates a new title for a project topic. */ - def projectTitle(project: Project) = project.name + project.description.map(d => s" - $d").getOrElse("") + def projectTitle(project: Project): String = project.name + project.description.map(d => s" - $d").getOrElse("") /** Generates the content for a project topic. */ - def projectTopic(project: Project)(implicit ec: ExecutionContext) = readAndFormatFile( + def projectTopic(project: Project)(implicit ec: ExecutionContext): String = readAndFormatFile( OreDiscourseApi.this.topicTemplatePath, project.name, OreDiscourseApi.this.baseUrl + '/' + project.url, @@ -321,8 +347,8 @@ trait OreDiscourseApi extends DiscourseApi { ) /** Generates the content for a version release post. */ - def versionRelease(project: Project, version: Version, content: Option[String]) = { - implicit val p = project + def versionRelease(project: Project, version: Version, content: Option[String]): String = { + implicit val p: Project = project readAndFormatFile( OreDiscourseApi.this.versionReleasePostTemplatePath, project.name, diff --git a/app/discourse/RecoveryTask.scala b/app/discourse/RecoveryTask.scala index bc2972291..bc4c63f5e 100644 --- a/app/discourse/RecoveryTask.scala +++ b/app/discourse/RecoveryTask.scala @@ -7,6 +7,8 @@ import db.impl.access.ProjectBase import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration +import play.api.Logger + /** * Task to periodically retry failed Discourse requests. */ @@ -15,30 +17,30 @@ class RecoveryTask(scheduler: Scheduler, api: OreDiscourseApi, projects: ProjectBase)(implicit ec: ExecutionContext) extends Runnable { - val Logger = this.api.Logger + val Logger: Logger = this.api.Logger /** * Starts the recovery task to be run at the specified interval. */ - def start() = { + def start(): Unit = { this.scheduler.schedule(this.retryRate, this.retryRate, this) Logger.info(s"Discourse recovery task started. First run in ${this.retryRate.toString}.") } - override def run() = { - Logger.info("Running Discourse recovery task...") + override def run(): Unit = { + Logger.debug("Running Discourse recovery task...") this.projects.filter(_.topicId === -1).foreach { toCreate => - Logger.info(s"Creating ${toCreate.size} topics...") + Logger.debug(s"Creating ${toCreate.size} topics...") toCreate.foreach(this.api.createProjectTopic) } this.projects.filter(_.isTopicDirty).foreach { toUpdate => - Logger.info(s"Updating ${toUpdate.size} topics...") + Logger.debug(s"Updating ${toUpdate.size} topics...") toUpdate.foreach(this.api.updateProjectTopic) } - Logger.info("Done") + Logger.debug("Done") // TODO: We need to keep deleted projects in case the topic cannot be deleted } diff --git a/app/discourse/SpongeForums.scala b/app/discourse/SpongeForums.scala index 0cb961fc0..837d708b6 100644 --- a/app/discourse/SpongeForums.scala +++ b/app/discourse/SpongeForums.scala @@ -2,7 +2,7 @@ package discourse import java.nio.file.Path -import akka.actor.ActorSystem +import akka.actor.{ActorSystem, Scheduler} import javax.inject.{Inject, Singleton} import ore.{OreConfig, OreEnv} import play.api.libs.ws.WSClient @@ -30,11 +30,12 @@ class SpongeForums @Inject()(env: OreEnv, override val url: String = this.conf.get[String]("baseUrl") override val baseUrl: String = this.config.app.get[String]("baseUrl") - override val categorySlug: String = this.conf.get[String]("embed.categorySlug") + override val categoryDefault: Int = this.conf.get[Int]("categoryDefault") + override val categoryDeleted: Int = this.conf.get[Int]("categoryDeleted") override val topicTemplatePath: Path = this.env.conf.resolve("discourse/project_topic.md") override val versionReleasePostTemplatePath: Path = this.env.conf.resolve("discourse/version_post.md") - override val scheduler = this.actorSystem.scheduler + override val scheduler: Scheduler = this.actorSystem.scheduler override val bootstrapExecutionContext: ExecutionContext = this.actorSystem.dispatcher - override val retryRate = this.conf.get[FiniteDuration]("embed.retryRate") + override val retryRate: FiniteDuration = this.conf.get[FiniteDuration]("retryRate") } diff --git a/app/form/OreForms.scala b/app/form/OreForms.scala index 56eec11bd..c91b0d29b 100755 --- a/app/form/OreForms.scala +++ b/app/form/OreForms.scala @@ -19,7 +19,7 @@ import ore.rest.ProjectApiKeyTypes.ProjectApiKeyType import play.api.data.Forms._ import play.api.data.format.Formatter import play.api.data.validation.{Constraint, Invalid, Valid, ValidationError} -import play.api.data.{Form, FormError} +import play.api.data.{FieldMapping, Form, FormError, Mapping} import scala.concurrent.ExecutionContext import scala.util.Try @@ -30,7 +30,7 @@ import scala.util.Try //noinspection ConvertibleToMethodValue class OreForms @Inject()(implicit config: OreConfig, factory: ProjectFactory, service: ModelService) { - val url = text verifying("error.url.invalid", text => { + val url: Mapping[String] = text verifying("error.url.invalid", text => { if (text.isEmpty) true else { @@ -215,8 +215,8 @@ class OreForms @Inject()(implicit config: OreConfig, factory: ProjectFactory, se */ lazy val VersionDescription = Form(single("content" -> text)) - val projectApiKeyType = of[ProjectApiKeyType](new Formatter[ProjectApiKeyType] { - def bind(key: String, data: Map[String, String]) = + val projectApiKeyType: FieldMapping[ProjectApiKeyType] = of[ProjectApiKeyType](new Formatter[ProjectApiKeyType] { + def bind(key: String, data: Map[String, String]): Either[Seq[FormError], ProjectApiKeyType] = data.get(key) .flatMap(id => Try(id.toInt).toOption.map(ProjectApiKeyTypes(_).asInstanceOf[ProjectApiKeyType])) .toRight(Seq(FormError(key, "error.required", Nil))) @@ -227,8 +227,8 @@ class OreForms @Inject()(implicit config: OreConfig, factory: ProjectFactory, se def required(key: String) = Seq(FormError(key, "error.required", Nil)) - def projectApiKey(implicit ec: ExecutionContext) = of[ProjectApiKey](new Formatter[ProjectApiKey] { - def bind(key: String, data: Map[String, String]) = { + def projectApiKey(implicit ec: ExecutionContext): FieldMapping[ProjectApiKey] = of[ProjectApiKey](new Formatter[ProjectApiKey] { + def bind(key: String, data: Map[String, String]): Either[Seq[FormError], ProjectApiKey] = { data.get(key). flatMap(id => Try(id.toInt).toOption.flatMap(evilAwaitpProjectApiKey(_))) .toRight(required(key)) @@ -245,8 +245,8 @@ class OreForms @Inject()(implicit config: OreConfig, factory: ProjectFactory, se def ProjectApiKeyRevoke(implicit ec: ExecutionContext) = Form(single("id" -> projectApiKey)) - def channel(implicit request: ProjectRequest[_], ec: ExecutionContext) = of[Channel](new Formatter[Channel] { - def bind(key: String, data: Map[String, String]) = { + def channel(implicit request: ProjectRequest[_], ec: ExecutionContext): FieldMapping[Channel] = of[Channel](new Formatter[Channel] { + def bind(key: String, data: Map[String, String]): Either[Seq[FormError], Channel] = { data.get(key) .flatMap(evilAwaitChannel(_)) .toRight(Seq(FormError(key, "api.deploy.channelNotFound", Nil))) diff --git a/app/form/organization/OrganizationAvatarUpdate.scala b/app/form/organization/OrganizationAvatarUpdate.scala index cd2737d36..ee6988845 100644 --- a/app/form/organization/OrganizationAvatarUpdate.scala +++ b/app/form/organization/OrganizationAvatarUpdate.scala @@ -11,6 +11,6 @@ case class OrganizationAvatarUpdate(method: String, url: Option[String]) { /** * Returns true if this update was a file upload. */ - val isFileUpload = this.method.equals("by-file") + val isFileUpload: Boolean = this.method.equals("by-file") } diff --git a/app/form/organization/OrganizationMembersUpdate.scala b/app/form/organization/OrganizationMembersUpdate.scala index bcfd25967..83c9d261c 100644 --- a/app/form/organization/OrganizationMembersUpdate.scala +++ b/app/form/organization/OrganizationMembersUpdate.scala @@ -10,6 +10,10 @@ import play.api.i18n.{Lang, MessagesApi} import scala.concurrent.{ExecutionContext, Future} +import db.impl.{OrganizationMembersTable, OrganizationRoleTable} +import ore.organization.OrganizationMember +import ore.user.MembershipDossier + /** * Saves new and old [[OrganizationRole]]s. * @@ -23,24 +27,33 @@ case class OrganizationMembersUpdate(override val users: List[Int], userUps: List[String], roleUps: List[String]) extends TOrganizationRoleSetBuilder { - implicit val lang = Lang.defaultLang - //noinspection ComparingUnrelatedTypes - def saveTo(organization: Organization)(implicit cache: AsyncCacheApi, ex: ExecutionContext, messages: MessagesApi, users: UserBase) = { + def saveTo(organization: Organization)(implicit cache: AsyncCacheApi, ex: ExecutionContext, messages: MessagesApi, users: UserBase): Unit = { if (!organization.isDefined) throw new RuntimeException("tried to update members on undefined organization") // Add new roles - val dossier = organization.memberships + val dossier: MembershipDossier { + type MembersTable = OrganizationMembersTable + + type MemberType = OrganizationMember + + type RoleTable = OrganizationRoleTable + + type ModelType = Organization + + type RoleType = OrganizationRole + } = organization.memberships val orgId = organization.id.get 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 dc3b47c67..3705f5f88 100644 --- a/app/mail/EmailFactory.scala +++ b/app/mail/EmailFactory.scala @@ -15,12 +15,10 @@ final class EmailFactory @Inject()(override val messagesApi: MessagesApi, val PgpUpdated = "email.pgpUpdate" val AccountUnlocked = "email.accountUnlock" - implicit val users = this.service.getModelBase(classOf[UserBase]) - implicit val lang = Lang.defaultLang - + implicit val users: UserBase = this.service.getModelBase(classOf[UserBase]) 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/mail/Mailer.scala b/app/mail/Mailer.scala index ffa3327a7..261ad6676 100644 --- a/app/mail/Mailer.scala +++ b/app/mail/Mailer.scala @@ -47,12 +47,12 @@ trait Mailer extends Runnable { private var session: Session = _ - private def log(msg: String) = if (!this.suppressLogger) Logger.info(msg) + private def log(msg: String): Unit = if (!this.suppressLogger) Logger.debug(msg) /** * Configures, initializes, and starts this Mailer. */ - def start()(implicit ec: ExecutionContext) = { + def start()(implicit ec: ExecutionContext): Unit = { Security.addProvider(new Provider) val props = System.getProperties for (prop <- this.properties.keys) @@ -67,7 +67,7 @@ trait Mailer extends Runnable { * * @param email Email to send */ - def send(email: Email) = { + def send(email: Email): Unit = { log("Sending email to " + email.recipient + "...") val message = new MimeMessage(this.session) message.setFrom(this.email) @@ -87,12 +87,12 @@ trait Mailer extends Runnable { * * @param email Email to push */ - def push(email: Email) = this.queue :+= email + def push(email: Email): Unit = this.queue :+= email /** * Sends all queued [[Email]]s. */ - def run() = { + def run(): Unit = { if (queue.nonEmpty) { log(s"Sending ${this.queue.size} queued emails...") this.queue.foreach(send) @@ -108,15 +108,15 @@ final class SpongeMailer @Inject()(config: Configuration, actorSystem: ActorSyst private val conf = config.get[Configuration]("mail") - override val username = this.conf.get[String]("username") - override val email = InternetAddress.parse(this.conf.get[String]("email"))(0) - override val password = this.conf.get[String]("password") - override val smtpHost = this.conf.get[String]("smtp.host") - override val smtpPort = this.conf.get[Int]("smtp.port") - override val transportProtocol = this.conf.get[String]("transport.protocol") - override val interval = this.conf.get[FiniteDuration]("interval") - override val scheduler = this.actorSystem.scheduler - override val properties = this.conf.get[Map[String, String]]("properties") + override val username: String = this.conf.get[String]("username") + override val email: InternetAddress = InternetAddress.parse(this.conf.get[String]("email"))(0) + override val password: String = this.conf.get[String]("password") + override val smtpHost: String = this.conf.get[String]("smtp.host") + override val smtpPort: Int = this.conf.get[Int]("smtp.port") + override val transportProtocol: String = this.conf.get[String]("transport.protocol") + override val interval: FiniteDuration = this.conf.get[FiniteDuration]("interval") + override val scheduler: Scheduler = this.actorSystem.scheduler + override val properties: Map[String, String] = this.conf.get[Map[String, String]]("properties") start() diff --git a/app/models/admin/LoggedActionView.scala b/app/models/admin/LoggedActionView.scala new file mode 100644 index 000000000..4fea54b32 --- /dev/null +++ b/app/models/admin/LoggedActionView.scala @@ -0,0 +1,66 @@ +package models.admin + +import java.sql.Timestamp + +import com.github.tminglei.slickpg.InetString +import db.impl.{LoggedActionTable, LoggedActionViewTable} +import db.impl.model.OreModel +import models.user.{LoggedAction, LoggedActionContext} +import ore.user.UserOwned + +case class LoggedProject(pId: Option[Int], pPluginId: Option[String], pSlug: Option[String], pOwnerName: Option[String]) +case class LoggedProjectVersion(pvId: Option[Int], pvVersionString: Option[String]) +case class LoggedProjectPage(ppId: Option[Int], ppSlug: Option[String]) +case class LoggedSubject(sId: Option[Int], sName: Option[String]) + +case class LoggedActionViewModel(override val id: Option[Int] = None, + override val createdAt: Option[Timestamp] = None, + private val _userId: Int, + private val _address: InetString, + private val _action: LoggedAction, + private val _actionContext: LoggedActionContext, + private val _actionContextId: Int, + private val _newState: String, + private val _oldState: String, + private val _uId: Int, + private val _uName: String, + private val _loggedProject: LoggedProject, + private val _loggedProjectVerison: LoggedProjectVersion, + private val _loggedProjectPage: LoggedProjectPage, + private val _loggedSubject: LoggedSubject, + private val _filterProject: Option[Int], + private val _filterVersion: Option[Int], + private val _filterPage: Option[Int], + private val _filterSubject: Option[Int], + private val _filterAction: Option[Int]) extends OreModel(id, createdAt) with UserOwned { + + override type T = LoggedActionViewTable + override type M = LoggedActionViewModel + + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): LoggedActionViewModel = this.copy(createdAt = theTime) + override def userId: Int = _userId + + def address: InetString = _address + def action: LoggedAction = _action + def oldState: String = _oldState + def newState: String = _newState + def contextId: Int = _actionContextId + def actionType: LoggedActionContext = _action.context + def uId: Int = _uId + def uName: String = _uName + def pId: Option[Int] = _loggedProject.pId + def pPluginId: Option[String] = _loggedProject.pPluginId + def pSlug: Option[String] = _loggedProject.pSlug + def pOwnerName: Option[String] = _loggedProject.pOwnerName + def pvId: Option[Int] = _loggedProjectVerison.pvId + def pvVersionString: Option[String] = _loggedProjectVerison.pvVersionString + def ppId: Option[Int] = _loggedProjectPage.ppId + def ppSlug: Option[String] = _loggedProjectPage.ppSlug + def sId: Option[Int] = _loggedSubject.sId + def sName: Option[String] = _loggedSubject.sName + def filterProject: Option[Int] = _filterProject + def filterVersion: Option[Int] = _filterVersion + def filterPage: Option[Int] = _filterPage + def filterSubject: Option[Int] = _filterSubject + def filterAction: Option[Int] = _filterAction +} diff --git a/app/models/admin/ProjectLog.scala b/app/models/admin/ProjectLog.scala index c9732d4c4..d398c955d 100644 --- a/app/models/admin/ProjectLog.scala +++ b/app/models/admin/ProjectLog.scala @@ -57,6 +57,6 @@ case class ProjectLog(override val id: Option[Int] = None, } } - def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + def copyWith(id: Option[Int], theTime: Option[Timestamp]): ProjectLog = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/admin/ProjectLogEntry.scala b/app/models/admin/ProjectLogEntry.scala index 17b1573fe..a95a581e7 100644 --- a/app/models/admin/ProjectLogEntry.scala +++ b/app/models/admin/ProjectLogEntry.scala @@ -2,6 +2,8 @@ package models.admin import java.sql.Timestamp +import scala.concurrent.Future + import db.impl.ProjectLogEntryTable import db.impl.model.OreModel import db.impl.table.ModelKeys._ @@ -41,7 +43,7 @@ case class ProjectLogEntry(override val id: Option[Int] = None, * * @param occurrences Amount of occurrences */ - def setOoccurrences(occurrences: Int) = Defined { + def setOoccurrences(occurrences: Int): Future[Int] = Defined { this._occurrences = occurrences update(Occurrences) } @@ -58,11 +60,11 @@ case class ProjectLogEntry(override val id: Option[Int] = None, * * @param lastOccurrence Last occurrence timestamp */ - def setLastOccurrence(lastOccurrence: Timestamp) = Defined { + def setLastOccurrence(lastOccurrence: Timestamp): Future[Int] = Defined { this._lastOccurrence = lastOccurrence update(LastOccurrence) } - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): ProjectLogEntry = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/admin/ProjectVisibilityChange.scala b/app/models/admin/ProjectVisibilityChange.scala new file mode 100644 index 000000000..488e145aa --- /dev/null +++ b/app/models/admin/ProjectVisibilityChange.scala @@ -0,0 +1,66 @@ +package models.admin + +import java.sql.Timestamp + +import db.Model +import db.impl.model.OreModel +import db.impl.table.ModelKeys._ +import models.project.Page +import models.user.User +import util.functional.OptionT +import util.instances.future._ +import play.twirl.api.Html +import scala.concurrent.{ExecutionContext, Future} + +import db.impl.ProjectVisibilityChangeTable +import db.impl.model.common.VisibilityChange + +case class ProjectVisibilityChange(override val id: Option[Int] = None, + override val createdAt: Option[Timestamp] = None, + createdBy: Option[Int] = None, + projectId: Int = -1, + comment: String, + var resolvedAt: Option[Timestamp] = None, + var resolvedBy: Option[Int] = None, + visibility: Int = 1) extends OreModel(id, createdAt) with VisibilityChange { + /** Self referential type */ + override type M = ProjectVisibilityChange + /** The model's table */ + override type T = ProjectVisibilityChangeTable + + /** Render the comment as Html */ + def renderComment(): Html = Page.Render(comment) + + def created(implicit ec: ExecutionContext): OptionT[Future, User] = { + OptionT.fromOption[Future](createdBy).flatMap(userBase.get(_)) + } + + /** + * Set the resolvedAt time + */ + def setResolvedAt(time: Timestamp): Future[Int] = { + this.resolvedAt = Some(time) + update(ResolvedAtVC) + } + + /** + * Set the resolvedBy user + */ + def setResolvedBy(user: User): Future[Int] = { + this.resolvedBy = user.id + update(ResolvedByVC) + } + def setResolvedById(userId: Int): Future[Int] = { + this.resolvedBy = Some(userId) + update(ResolvedByVC) + } + + /** + * Returns a copy of this model with an updated ID and timestamp. + * + * @param id ID to set + * @param theTime Timestamp + * @return Copy of model + */ + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): Model = this.copy(id = id, createdAt = createdAt) +} diff --git a/app/models/admin/Review.scala b/app/models/admin/Review.scala index bd08aea41..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]]. @@ -62,8 +63,8 @@ case class Review(override val id: Option[Int] = None, /** * Helper function to encode to json */ - implicit val messageWrites = new Writes[Message] { - def writes(message: Message) = Json.obj( + implicit val messageWrites: Writes[Message] = new Writes[Message] { + def writes(message: Message): JsObject = Json.obj( "message" -> message.message, "time" -> message.time, "action" -> message.action @@ -133,9 +134,9 @@ 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) = StringUtils.prettifyDateAndTime(new Timestamp(time)) - def isTakeover() = action.equalsIgnoreCase("takeover") - def isStop() = action.equalsIgnoreCase("stop") + 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) } @@ -150,4 +151,4 @@ object Review { // TODO make simple + check order Ordering.by(_.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime) } -} \ No newline at end of file +} diff --git a/app/models/admin/VisibilityChange.scala b/app/models/admin/VersionVisibilityChange.scala similarity index 78% rename from app/models/admin/VisibilityChange.scala rename to app/models/admin/VersionVisibilityChange.scala index 4dbea7d92..d044a0d39 100644 --- a/app/models/admin/VisibilityChange.scala +++ b/app/models/admin/VersionVisibilityChange.scala @@ -2,37 +2,35 @@ package models.admin import java.sql.Timestamp +import scala.concurrent.{ExecutionContext, Future} + import db.Model -import db.impl.VisibilityChangeTable +import db.impl.VersionVisibilityChangeTable import db.impl.model.OreModel +import db.impl.model.common.VisibilityChange import db.impl.table.ModelKeys._ import models.project.Page import models.user.User +import play.twirl.api.Html import util.functional.OptionT import util.instances.future._ -import play.twirl.api.Html -import scala.concurrent.{ExecutionContext, Future} - -case class VisibilityChange(override val id: Option[Int] = None, +case class VersionVisibilityChange(override val id: Option[Int] = None, override val createdAt: Option[Timestamp] = None, createdBy: Option[Int] = None, projectId: Int = -1, comment: String, var resolvedAt: Option[Timestamp] = None, var resolvedBy: Option[Int] = None, - visibility: Int = 1) extends OreModel(id, createdAt) { + visibility: Int = 1) extends OreModel(id, createdAt) with VisibilityChange { /** Self referential type */ - override type M = VisibilityChange + override type M = VersionVisibilityChange /** The model's table */ - override type T = VisibilityChangeTable + override type T = VersionVisibilityChangeTable /** Render the comment as Html */ def renderComment(): Html = Page.Render(comment) - /** Check if the change has been dealt with */ - def isResolved: Boolean = !resolvedAt.isEmpty - def created(implicit ec: ExecutionContext): OptionT[Future, User] = { OptionT.fromOption[Future](createdBy).flatMap(userBase.get(_)) } @@ -41,7 +39,7 @@ case class VisibilityChange(override val id: Option[Int] = None, * Set the resolvedAt time * @param time */ - def setResolvedAt(time: Timestamp) = { + def setResolvedAt(time: Timestamp): Future[Int] = { this.resolvedAt = Some(time) update(ResolvedAtVC) } @@ -50,11 +48,11 @@ case class VisibilityChange(override val id: Option[Int] = None, * Set the resolvedBy user * @param user */ - def setResolvedBy(user: User) = { + def setResolvedBy(user: User): Future[Int] = { this.resolvedBy = user.id update(ResolvedByVC) } - def setResolvedById(userId: Int) = { + def setResolvedById(userId: Int): Future[Int] = { this.resolvedBy = Some(userId) update(ResolvedByVC) } diff --git a/app/models/api/ProjectApiKey.scala b/app/models/api/ProjectApiKey.scala index c0e969703..4b5975fdc 100644 --- a/app/models/api/ProjectApiKey.scala +++ b/app/models/api/ProjectApiKey.scala @@ -17,6 +17,6 @@ case class ProjectApiKey(override val id: Option[Int] = None, override type T = ProjectApiKeyTable override type M = ProjectApiKey - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): ProjectApiKey = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/project/Channel.scala b/app/models/project/Channel.scala index e33888c72..921aad74e 100644 --- a/app/models/project/Channel.scala +++ b/app/models/project/Channel.scala @@ -5,7 +5,9 @@ import java.sql.Timestamp import scala.concurrent.Future import com.google.common.base.Preconditions._ + import db.Named +import db.access.ModelAccess import db.impl.ChannelTable import db.impl.model.OreModel import db.impl.table.ModelKeys @@ -53,7 +55,7 @@ case class Channel(override val id: Option[Int] = None, * * @param _name New channel name */ - def setName(_name: String) = Defined { + def setName(_name: String): Future[Int] = Defined { checkNotNull(_name, "null name", "") checkArgument(this.config.isValidChannelName(_name), "invalid name", "") this._name = _name @@ -72,7 +74,7 @@ case class Channel(override val id: Option[Int] = None, * * @param _color Color of channel */ - def setColor(_color: Color) = Defined { + def setColor(_color: Color): Future[Int] = Defined { checkNotNull(_color, "null color", "") this._color = _color update(ModelKeys.Color) @@ -82,7 +84,7 @@ case class Channel(override val id: Option[Int] = None, def isNonReviewed: Boolean = this._isNonReviewed - def setNonReviewed(isNonReviewed: Boolean) = { + def setNonReviewed(isNonReviewed: Boolean): Future[AnyVal] = { this._isNonReviewed = isNonReviewed if (isDefined) update(IsNonReviewed) @@ -94,12 +96,12 @@ case class Channel(override val id: Option[Int] = None, * * @return All versions */ - def versions = this.schema.getChildren[Version](classOf[Version], this) + def versions: ModelAccess[Version] = this.schema.getChildren[Version](classOf[Version], this) - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) - override def compare(that: Channel) = this._name compare that._name - override def hashCode() = this.id.get.hashCode - override def equals(o: Any) = o.isInstanceOf[Channel] && o.asInstanceOf[Channel].id.get == this.id.get + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): Channel = this.copy(id = id, createdAt = theTime) + override def compare(that: Channel): Int = this._name compare that._name + override def hashCode(): Int = this.id.get.hashCode + override def equals(o: Any): Boolean = o.isInstanceOf[Channel] && o.asInstanceOf[Channel].id.get == this.id.get } diff --git a/app/models/project/DownloadWarning.scala b/app/models/project/DownloadWarning.scala index 1bad9976a..2ffb53eb6 100644 --- a/app/models/project/DownloadWarning.scala +++ b/app/models/project/DownloadWarning.scala @@ -42,7 +42,7 @@ case class DownloadWarning(override val id: Option[Int] = None, def isConfirmed: Boolean = this._isConfirmed - def setConfirmed(confirmed: Boolean = true) = Defined { + def setConfirmed(confirmed: Boolean = true): Future[Int] = Defined { this._isConfirmed = confirmed update(IsConfirmed) } @@ -71,7 +71,7 @@ case class DownloadWarning(override val id: Option[Int] = None, * * @param download Download warning was for */ - def setDownload(download: UnsafeDownload) = Defined { + def setDownload(download: UnsafeDownload): Future[Int] = Defined { checkNotNull(download, "null download", "") checkArgument(download.isDefined, "undefined download", "") this._downloadId = download.id.get @@ -88,7 +88,7 @@ case class DownloadWarning(override val id: Option[Int] = None, bakery.bake(COOKIE, this.token) } - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): DownloadWarning = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/project/Flag.scala b/app/models/project/Flag.scala index 358b1e07f..8f65ad152 100644 --- a/app/models/project/Flag.scala +++ b/app/models/project/Flag.scala @@ -65,6 +65,6 @@ case class Flag(override val id: Option[Int], } } - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): Flag = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/project/Page.scala b/app/models/project/Page.scala index 72a60e1b2..d1288d59f 100644 --- a/app/models/project/Page.scala +++ b/app/models/project/Page.scala @@ -147,7 +147,7 @@ case class Page(override val id: Option[Int] = None, = this.service.access[Page](classOf[Page], ModelFilter[Page](_.parentId === this.id.get)) def url(implicit project: Project, parentPage: Option[Page]) : String = project.url + "/pages/" + this.fullSlug(parentPage) - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): Page = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/project/Project.scala b/app/models/project/Project.scala index f384a73a6..44e96f66c 100755 --- a/app/models/project/Project.scala +++ b/app/models/project/Project.scala @@ -8,16 +8,17 @@ import _root_.util.StringUtils._ import _root_.util.instances.future._ import _root_.util.functional.OptionT import com.google.common.base.Preconditions._ -import db.access.ModelAccess + +import db.access.{ModelAccess, ModelAssociationAccess} import db.impl.OrePostgresDriver.api._ import db.impl._ import db.impl.model.OreModel -import db.impl.model.common.{Describable, Downloadable, Hideable} +import db.impl.model.common.{Describable, Downloadable, Hideable, VisibilityChange} import db.impl.schema.ProjectSchema import db.impl.table.ModelKeys import db.impl.table.ModelKeys._ import db.{ModelService, Named} -import models.admin.{ProjectLog, VisibilityChange} +import models.admin.{ProjectLog, ProjectVisibilityChange} import models.api.ProjectApiKey import models.project.VisibilityTypes.{Public, Visibility} import models.statistic.ProjectView @@ -35,9 +36,10 @@ import play.api.libs.json._ import play.twirl.api.Html import slick.lifted import slick.lifted.{Rep, TableQuery} - import scala.concurrent.{ExecutionContext, Future} +import play.api.i18n.Messages + /** * Represents an Ore package. * @@ -97,11 +99,22 @@ case class Project(override val id: Option[Int] = None, override type M = Project override type T = ProjectTable override type S = ProjectSchema + override type ModelVisibilityChange = ProjectVisibilityChange /** * Contains all information for [[User]] memberships. */ - override val memberships = new MembershipDossier { + override val memberships: MembershipDossier { + type MembersTable = ProjectMembersTable + + type MemberType = ProjectMember + + type RoleTable = ProjectRoleTable + + type ModelType = Project + + type RoleType = ProjectRole +} = new MembershipDossier { type ModelType = Project type RoleType = ProjectRole @@ -201,7 +214,7 @@ case class Project(override val id: Option[Int] = None, * * @return Users watching project */ - def watchers = this.schema.getAssociation[ProjectWatchersTable, User](classOf[ProjectWatchersTable], this) + def watchers: ModelAssociationAccess[ProjectWatchersTable, User] = this.schema.getAssociation[ProjectWatchersTable, User](classOf[ProjectWatchersTable], this) /** * Returns the name of this Project. @@ -312,6 +325,9 @@ case class Project(override val id: Option[Int] = None, } + override def visibilityChanges: ModelAccess[ProjectVisibilityChange] = + this.schema.getChildren[ProjectVisibilityChange](classOf[ProjectVisibilityChange], this) + /** * Returns true if this Project is visible. * @@ -319,14 +335,7 @@ case class Project(override val id: Option[Int] = None, */ override def visibility: Visibility = this._visibility - def isDeleted = visibility == VisibilityTypes.SoftDelete - - /** - * Sets whether this project is visible. - * - * @param visibility True if visible - */ - def setVisibility(visibility: Visibility, comment: String, creator: Int)(implicit ec: ExecutionContext) = { + override def setVisibility(visibility: Visibility, comment: String, creator: Int)(implicit ec: ExecutionContext): Future[ProjectVisibilityChange] = { this._visibility = visibility if (isDefined) update(ModelKeys.Visibility) @@ -336,22 +345,11 @@ case class Project(override val id: Option[Int] = None, 0 } cnt.flatMap { _ => - val change = VisibilityChange(None, Some(Timestamp.from(Instant.now())), Some(creator), this.id.get, comment, None, None, visibility.id) - this.service.access[VisibilityChange](classOf[VisibilityChange]).add(change) + val change = ProjectVisibilityChange(None, Some(Timestamp.from(Instant.now())), Some(creator), this.id.get, comment, None, None, visibility.id) + this.service.access[ProjectVisibilityChange](classOf[ProjectVisibilityChange]).add(change) } } - /** - * Get VisibilityChanges - */ - def visibilityChanges = this.schema.getChildren[VisibilityChange](classOf[VisibilityChange], this) - def visibilityChangesByDate(implicit ec: ExecutionContext) = visibilityChanges.all.map(_.toSeq.sortWith(byCreationDate)) - def byCreationDate(first: VisibilityChange, second: VisibilityChange) = first.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime < second.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime - def lastVisibilityChange(implicit ec: ExecutionContext): OptionT[Future, VisibilityChange] = - OptionT(visibilityChanges.all.map(_.toSeq.filter(cr => !cr.isResolved).sortWith(byCreationDate).headOption)) - def lastChangeRequest(implicit ec: ExecutionContext): OptionT[Future, VisibilityChange] = - OptionT(visibilityChanges.all.map(_.toSeq.filter(cr => cr.visibility == VisibilityTypes.NeedsChanges.id).sortWith(byCreationDate).lastOption)) - /** * Returns the last time this [[Project]] was updated. * @@ -376,7 +374,7 @@ case class Project(override val id: Option[Int] = None, * * @return Users who have starred this project */ - def stars = Defined(this.schema.getAssociation[ProjectStarsTable, User](classOf[ProjectStarsTable], this)) + def stars: ModelAssociationAccess[ProjectStarsTable, User] = Defined(this.schema.getAssociation[ProjectStarsTable, User](classOf[ProjectStarsTable], this)) /** * Returns the amount of stars this [[Project]] has. @@ -391,7 +389,7 @@ case class Project(override val id: Option[Int] = None, * @param user User to set starred state of * @param starred True if should star */ - def setStarredBy(user: User, starred: Boolean)(implicit ec: ExecutionContext) = Defined { + def setStarredBy(user: User, starred: Boolean)(implicit ec: ExecutionContext): Future[Future[Int]] = Defined { checkNotNull(user, "null user", "") checkArgument(user.isDefined, "undefined user", "") for { @@ -415,7 +413,7 @@ case class Project(override val id: Option[Int] = None, * * @return Unique project views */ - def views = this.schema.getChildren[ProjectView](classOf[ProjectView], this) + def views: ModelAccess[ProjectView] = this.schema.getChildren[ProjectView](classOf[ProjectView], this) /** * Returns the amount of views this project has. @@ -454,7 +452,7 @@ case class Project(override val id: Option[Int] = None, * * @return Flags on project */ - def flags = this.schema.getChildren[Flag](classOf[Flag], this) + def flags: ModelAccess[Flag] = this.schema.getChildren[Flag](classOf[Flag], this) /** * Submits a flag on this project for the specified user. @@ -476,14 +474,14 @@ case class Project(override val id: Option[Int] = None, * * @return Channels in project */ - def channels = this.schema.getChildren[Channel](classOf[Channel], this) + def channels: ModelAccess[Channel] = this.schema.getChildren[Channel](classOf[Channel], this) /** * Returns all versions in this project. * * @return Versions in project */ - def versions = this.schema.getChildren[Version](classOf[Version], this) + def versions: ModelAccess[Version] = this.schema.getChildren[Version](classOf[Version], this) /** * Returns this Project's recommended version. @@ -499,11 +497,39 @@ case class Project(override val id: Option[Int] = None, * @param _version Version to set * @return Result */ - def setRecommendedVersion(_version: Version) = { + def setRecommendedVersion(_version: Version): Future[AnyVal] = { checkNotNull(_version, "null version", "") checkArgument(_version.isDefined, "undefined version", "") this.recommendedVersionId = _version.id - if (isDefined) update(RecommendedVersionId) + if (isDefined) update(RecommendedVersionId) else Future.unit + } + + /** + * Get a collection of tags that represent a project through its versions + */ + def tags(implicit ec: ExecutionContext, service: ModelService): Future[Seq[Tag]] = { + schema(service) + // get all the versions for the project + this.service.access(classOf[Version]).filter(_.projectId === id.get).flatMap { versions => + val tagIds = versions.flatMap(_.tagIds).distinct + // get all the tags for all the versions + this.service.access(classOf[Tag]).filter(t => t.id inSet tagIds).map { list => + list.distinct + // get the latest tag from the versions + .groupBy(_.name) + .map { case (_, tags) => + tags.maxBy { tag => + versions + .filter(_.tagIds.contains(tag.id.get)) + .filter(!_.isDeleted) + // get the latest version + .map(_.createdAt.get.toInstant.toEpochMilli) + .max + } + } + .toSeq + } + } } /** @@ -511,7 +537,7 @@ case class Project(override val id: Option[Int] = None, * * @return Pages in project */ - def pages = this.schema.getChildren[Page](classOf[Page], this) + def pages: ModelAccess[Page] = this.schema.getChildren[Page](classOf[Page], this) /** * Returns this Project's home page. @@ -576,7 +602,7 @@ case class Project(override val id: Option[Int] = None, * * @param _topicId ID to set */ - def setTopicId(_topicId: Int) = Defined { + def setTopicId(_topicId: Int): Future[Int] = Defined { this._topicId = _topicId update(TopicId) } @@ -593,7 +619,7 @@ case class Project(override val id: Option[Int] = None, * * @param _postId Forum post ID */ - def setPostId(_postId: Int) = Defined { + def setPostId(_postId: Int): Future[Int] = Defined { this._postId = _postId update(PostId) } @@ -611,18 +637,18 @@ case class Project(override val id: Option[Int] = None, * * @param topicDirty True if topic is dirty */ - def setTopicDirty(topicDirty: Boolean) = Defined { + def setTopicDirty(topicDirty: Boolean): Future[Int] = Defined { this._isTopicDirty = topicDirty update(IsTopicDirty) } def apiKeys: ModelAccess[ProjectApiKey] = this.schema.getChildren[ProjectApiKey](classOf[ProjectApiKey], this) - override def projectId = Defined(this.id.get) - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) + override def projectId: Int = Defined(this.id.get) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): Project = this.copy(id = id, createdAt = theTime, _lastUpdated = theTime.orNull) - override def hashCode() = this.id.get.hashCode - override def equals(o: Any) = o.isInstanceOf[Project] && o.asInstanceOf[Project].id.get == this.id.get + override def hashCode(): Int = this.id.get.hashCode + override def equals(o: Any): Boolean = o.isInstanceOf[Project] && o.asInstanceOf[Project].id.get == this.id.get /** * Set a message and update the database @@ -648,13 +674,13 @@ case class Project(override val id: Option[Int] = None, * @param message * @return */ - def addNote(message: Note) = { + def addNote(message: Note): Future[Int] = { /** * Helper function to encode to json */ - implicit val noteWrites = new Writes[Note] { - def writes(note: Note) = Json.obj( + implicit val noteWrites: Writes[Note] = new Writes[Note] { + def writes(note: Note): JsObject = Json.obj( "message" -> note.message, "user" -> note.user, "time" -> note.time @@ -694,7 +720,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) = 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) } @@ -727,27 +753,27 @@ object Project { private var _name: String = _ private var _visibility: Visibility = _ - def pluginId(pluginId: String) = { + def pluginId(pluginId: String): Builder = { this._pluginId = pluginId this } - def ownerName(ownerName: String) = { + def ownerName(ownerName: String): Builder = { this._ownerName = ownerName this } - def ownerId(ownerId: Int) = { + def ownerId(ownerId: Int): Builder = { this._ownerId = ownerId this } - def name(name: String) = { + def name(name: String): Builder = { this._name = name this } - def visibility(visibility: Visibility) = { + def visibility(visibility: Visibility): Builder = { this._visibility = visibility this } diff --git a/app/models/project/ProjectSettings.scala b/app/models/project/ProjectSettings.scala index 6abf584ab..38333c8be 100644 --- a/app/models/project/ProjectSettings.scala +++ b/app/models/project/ProjectSettings.scala @@ -14,7 +14,7 @@ import models.user.Notification import models.user.role.ProjectRole import ore.permission.role.RoleTypes import ore.project.io.ProjectFiles -import ore.project.{Categories, ProjectOwned} +import ore.project.{Categories, ProjectMember, ProjectOwned} import ore.user.notification.NotificationTypes import play.api.Logger import play.api.cache.AsyncCacheApi @@ -24,6 +24,8 @@ import util.StringUtils._ import scala.concurrent.{ExecutionContext, Future} +import ore.user.MembershipDossier + /** * Represents a [[Project]]'s settings. * @@ -47,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.defaultLang override type M = ProjectSettings override type T = ProjectSettingsTable @@ -144,8 +145,8 @@ case class ProjectSettings(override val id: Option[Int] = None, */ //noinspection ComparingUnrelatedTypes def save(project: Project, formData: ProjectSettingsForm)(implicit cache: AsyncCacheApi, messages: MessagesApi, fileManager: ProjectFiles, ec: ExecutionContext): Future[_] = { - Logger.info("Saving project settings") - Logger.info(formData.toString) + Logger.debug("Saving project settings") + Logger.debug(formData.toString) project.setCategory(Categories.withName(formData.categoryName)) project.setDescription(nullIfEmpty(formData.description)) @@ -176,7 +177,17 @@ case class ProjectSettings(override val id: Option[Int] = None, // Handle member changes if (project.isDefined) { // Add new roles - val dossier = project.memberships + val dossier: MembershipDossier { + type MembersTable = ProjectMembersTable + + type MemberType = ProjectMember + + type RoleTable = ProjectRoleTable + + type ModelType = Project + + type RoleType = ProjectRole + } = project.memberships Future.sequence(formData.build().map { role => dossier.addRole(role.copy(projectId = project.id.get)) }).flatMap { roles => @@ -186,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 @@ -221,6 +232,6 @@ case class ProjectSettings(override val id: Option[Int] = None, } private lazy val updateMemberShip = Compiled(memberShipUpdate _) - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): ProjectSettings = this.copy(id = id, createdAt = theTime) -} \ No newline at end of file +} diff --git a/app/models/project/Tag.scala b/app/models/project/Tag.scala index ce7ef1000..cf89e8f01 100644 --- a/app/models/project/Tag.scala +++ b/app/models/project/Tag.scala @@ -2,14 +2,17 @@ package models.project import java.sql.Timestamp -import db.Named +import db.impl.OrePostgresDriver.api._ import db.impl.model.OreModel import db.impl.table.ModelKeys._ import db.impl.{OrePostgresDriver, TagTable} import db.table.MappedType +import db.{ModelService, Named} import models.project.TagColors.TagColor import slick.jdbc.JdbcType +import scala.concurrent.{ExecutionContext, Future} + case class Tag(override val id: Option[Int] = None, private var _versionIds: List[Int], name: String, @@ -23,7 +26,7 @@ case class Tag(override val id: Option[Int] = None, def versionIds: List[Int] = this._versionIds - def addVersionId(versionId: Int) = { + def addVersionId(versionId: Int): Unit = { this._versionIds = this._versionIds :+ versionId if (isDefined) { update(TagVersionIds) @@ -31,14 +34,36 @@ case class Tag(override val id: Option[Int] = None, } def copyWith(id: Option[Int], theTime: Option[Timestamp]): Tag = this.copy(id = id) + + /** + * Used to convert a ghost tag to a normal tag + * @author phase + */ + def getFilledTag(service: ModelService)(implicit ex: ExecutionContext): Future[Tag] = { + val access = service.access(classOf[Tag]) + for { + tagsWithVersion <- access.filter(t => t.name === this.name && t.data === this.data) + tag <- if (tagsWithVersion.isEmpty) { + access.add(this) + } else { + Future.successful(tagsWithVersion.head) + } + } yield tag + } + } object TagColors extends Enumeration { // Tag colors - val Sponge = TagColor(1, "#F7Cf0D", "#000000") - val Forge = TagColor(2, "#910020", "#FFFFFF") - val Unstable = TagColor(3, "#FFDAB9", "#000000") + val Sponge = TagColor(1, "#F7Cf0D", "#333333") + val Forge = TagColor(2, "#dfa86a", "#FFFFFF") + val Unstable = TagColor(3, "#FFDAB9", "#333333") + val SpongeForge = TagColor(4, "#910020", "#FFFFFF") + val SpongeVanilla = TagColor(5, "#50C888", "#FFFFFF") + val SpongeCommon = TagColor(6, "#5d5dff", "#FFFFFF") + val Lantern = TagColor(7, "#4EC1B4", "#FFFFFF") + val Mixin = TagColor(8, "#FFA500", "#333333") def withId(id: Int): TagColor = { this.apply(id).asInstanceOf[TagColor] diff --git a/app/models/project/UnsafeDownload.scala b/app/models/project/UnsafeDownload.scala index c7bdf4aee..7dbc5074e 100644 --- a/app/models/project/UnsafeDownload.scala +++ b/app/models/project/UnsafeDownload.scala @@ -25,6 +25,6 @@ case class UnsafeDownload(override val id: Option[Int] = None, override type M = UnsafeDownload override type T = UnsafeDownloadsTable - def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + def copyWith(id: Option[Int], theTime: Option[Timestamp]): UnsafeDownload = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/project/Version.scala b/app/models/project/Version.scala index d0d13a65d..2e34c04f8 100755 --- a/app/models/project/Version.scala +++ b/app/models/project/Version.scala @@ -4,15 +4,16 @@ import java.sql.Timestamp import java.time.Instant import com.google.common.base.Preconditions.{checkArgument, checkNotNull} + import db.ModelService import db.access.ModelAccess import db.impl.OrePostgresDriver.api._ import db.impl.model.OreModel -import db.impl.model.common.{Describable, Downloadable} +import db.impl.model.common.{Describable, Downloadable, Hideable} import db.impl.schema.VersionSchema import db.impl.table.ModelKeys._ import db.impl.{ReviewTable, VersionTable} -import models.admin.Review +import models.admin.{Review, VersionVisibilityChange} import models.statistic.VersionDownload import models.user.User import ore.permission.scope.ProjectScope @@ -24,6 +25,9 @@ import util.functional.OptionT import scala.concurrent.{ExecutionContext, Future} +import db.impl.table.ModelKeys +import models.project.VisibilityTypes.{Public, Visibility} + /** * Represents a single version of a Project. * @@ -53,16 +57,20 @@ case class Version(override val id: Option[Int] = None, private var _reviewerId: Int = -1, private var _approvedAt: Option[Timestamp] = None, private var _tagIds: List[Int] = List(), + private var _visibility: Visibility = Public, fileName: String, - signatureFileName: String) + signatureFileName: String, + private var _isNonReviewed: Boolean = false) extends OreModel(id, createdAt) with ProjectScope with Describable - with Downloadable { + with Downloadable + with Hideable { override type M = Version override type T = VersionTable override type S = VersionSchema + override type ModelVisibilityChange = VersionVisibilityChange /** * Returns the name of this Channel. @@ -157,23 +165,30 @@ case class Version(override val id: Option[Int] = None, def reviewer(implicit ec: ExecutionContext): OptionT[Future, User] = this.userBase.get(this._reviewerId) - def setReviewer(reviewer: User) = Defined { + def setReviewer(reviewer: User): Future[Int] = Defined { this._reviewerId = reviewer.id.get update(ReviewerId) } - def setReviewerId(reviewer: Int) = Defined { + def setReviewerId(reviewer: Int): Future[Int] = Defined { this._reviewerId = reviewer update(ReviewerId) } def approvedAt: Option[Timestamp] = this._approvedAt - def setApprovedAt(approvedAt: Timestamp) = Defined { + def setApprovedAt(approvedAt: Timestamp): Future[Int] = Defined { this._approvedAt = Option(approvedAt) update(ApprovedAt) } + def isNonReviewed: Boolean = this._isNonReviewed + + def setIsNonReviewed(ignoreReview: Boolean): Unit = { + this._isNonReviewed = ignoreReview + update(IsNonReviewedVersion) + } + def tagIds: List[Int] = this._tagIds def setTagIds(tags: List[Int]) = { @@ -181,7 +196,7 @@ case class Version(override val id: Option[Int] = None, if(isDefined) update(TagIds) } - def addTag(tag: Tag) = { + def addTag(tag: Tag): Unit = { this._tagIds = this._tagIds :+ tag.id.get if (isDefined) { update(TagIds) @@ -190,16 +205,9 @@ case class Version(override val id: Option[Int] = None, def tags(implicit ec: ExecutionContext, service: ModelService = null): Future[List[Tag]] = { schema(service) - this.service.access(classOf[Tag]).filter(_.id inSetBind tagIds).map { list => - list.toSet.toList - } + this.service.access(classOf[Tag]).filter(_.id inSetBind tagIds).map (_.distinct.toList) } - - def isSpongePlugin(implicit ec: ExecutionContext): Future[Boolean] = tags.map(_.map(_.name).contains("Sponge")) - - def isForgeMod(implicit ec: ExecutionContext): Future[Boolean] = tags.map(_.map(_.name).contains("Forge")) - /** * Returns this Versions plugin dependencies. * @@ -208,7 +216,7 @@ case class Version(override val id: Option[Int] = None, def dependencies: List[Dependency] = { for (depend <- this.dependencyIds) yield { val data = depend.split(":") - Dependency(data(0), data(1)) + Dependency(data(0), if (data.length > 1) data(1) else "") } } @@ -219,7 +227,7 @@ case class Version(override val id: Option[Int] = None, * @return True if has dependency on ID */ //noinspection ComparingUnrelatedTypes - def hasDependency(pluginId: String) = this.dependencies.exists(_.pluginId.equals(pluginId)) + def hasDependency(pluginId: String): Boolean = this.dependencies.exists(_.pluginId.equals(pluginId)) /** * Returns the amount of unique downloads this Version has. @@ -231,17 +239,37 @@ case class Version(override val id: Option[Int] = None, /** * Adds a download to the amount of unique downloads this Version has. */ - def addDownload() = Defined { + def addDownload(): Future[Int] = Defined { this._downloads += 1 update(Downloads) } + override def visibilityChanges: ModelAccess[VersionVisibilityChange] = + this.schema.getChildren[VersionVisibilityChange](classOf[VersionVisibilityChange], this) + + override def visibility: Visibility = this._visibility + + override def setVisibility(visibility: Visibility, comment: String, creator: Int)(implicit ec: ExecutionContext): Future[VersionVisibilityChange] = { + this._visibility = visibility + if (isDefined) update(ModelKeys.Visibility) + + val cnt = lastVisibilityChange.map { vc => + vc.setResolvedAt(Timestamp.from(Instant.now())) + vc.setResolvedById(creator) + 0 + } + cnt.value.flatMap { _ => + val change = VersionVisibilityChange(None, Some(Timestamp.from(Instant.now())), Some(creator), this.id.get, comment, None, None, visibility.id) + this.service.access[VersionVisibilityChange](classOf[VersionVisibilityChange]).add(change) + } + } + /** * Returns [[ModelAccess]] to the recorded unique downloads. * * @return Recorded downloads */ - def downloadEntries = this.schema.getChildren[VersionDownload](classOf[VersionDownload], this) + def downloadEntries: ModelAccess[VersionDownload] = this.schema.getChildren[VersionDownload](classOf[VersionDownload], this) /** * Returns a human readable file size for this Version. @@ -270,12 +298,12 @@ case class Version(override val id: Option[Int] = None, } } - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) - override def hashCode() = this.id.hashCode - override def equals(o: Any) = o.isInstanceOf[Version] && o.asInstanceOf[Version].id.get == this.id.get + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): Version = this.copy(id = id, createdAt = theTime) + override def hashCode(): Int = this.id.hashCode + override def equals(o: Any): Boolean = o.isInstanceOf[Version] && o.asInstanceOf[Version].id.get == this.id.get - def byCreationDate(first: Review, second: Review) = first.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime < second.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime - def reviewEntries = this.schema.getChildren[Review](classOf[Review], this) + def byCreationDate(first: Review, second: Review): Boolean = first.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime < second.createdAt.getOrElse(Timestamp.from(Instant.MIN)).getTime + def reviewEntries: ModelAccess[Review] = this.schema.getChildren[Review](classOf[Review], this) def unfinishedReviews(implicit ec: ExecutionContext): Future[Seq[Review]] = reviewEntries.all.map(_.toSeq.filter(rev => rev.createdAt.isDefined && rev.endedAt.isEmpty).sortWith(byCreationDate)) def mostRecentUnfinishedReview(implicit ec: ExecutionContext): OptionT[Future, Review] = OptionT(unfinishedReviews.map(_.headOption)) def mostRecentReviews(implicit ec: ExecutionContext): Future[Seq[Review]] = reviewEntries.toSeq.map(_.sortWith(byCreationDate)) @@ -304,57 +332,57 @@ object Version { private var _signatureFileName: String = _ private var _tagIds: List[Int] = List() - def versionString(versionString: String) = { + def versionString(versionString: String): Builder = { this._versionString = versionString this } - def dependencyIds(dependencyIds: List[String]) = { + def dependencyIds(dependencyIds: List[String]): Builder = { this._dependencyIds = dependencyIds this } - def description(description: String) = { + def description(description: String): Builder = { this._description = description this } - def projectId(projectId: Int) = { + def projectId(projectId: Int): Builder = { this._projectId = projectId this } - def fileSize(fileSize: Long) = { + def fileSize(fileSize: Long): Builder = { this._fileSize = fileSize this } - def hash(hash: String) = { + def hash(hash: String): Builder = { this._hash = hash this } - def authorId(authorId: Int) = { + def authorId(authorId: Int): Builder = { this._authorId = authorId this } - def fileName(fileName: String) = { + def fileName(fileName: String): Builder = { this._fileName = fileName this } - def signatureFileName(signatureFileName: String) = { + def signatureFileName(signatureFileName: String): Builder = { this._signatureFileName = signatureFileName this } - def tagIds(tagIds: List[Int]) = { + def tagIds(tagIds: List[Int]): Builder = { this._tagIds = tagIds this } - def build() = { + def build(): Version = { checkArgument(this._fileSize != -1, "invalid file size", "") this.service.processor.process(Version( versionString = checkNotNull(this._versionString, "name null", ""), diff --git a/app/models/statistic/ProjectView.scala b/app/models/statistic/ProjectView.scala index 97e8eb212..24a801838 100644 --- a/app/models/statistic/ProjectView.scala +++ b/app/models/statistic/ProjectView.scala @@ -14,6 +14,8 @@ import util.instances.future._ import scala.concurrent.{ExecutionContext, Future} +import controllers.sugar.Requests + /** * Represents a unique view on a Project. * @@ -37,7 +39,7 @@ case class ProjectView(override val id: Option[Int] = None, override type T = ProjectViewsTable override def projectId: Int = this.modelId - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): ProjectView = this.copy(id = id, createdAt = theTime) } @@ -51,7 +53,7 @@ object ProjectView { * @return New ProjectView */ def bindFromRequest(request: ProjectRequest[_])(implicit ec: ExecutionContext, users: UserBase): Future[ProjectView] = { - implicit val r = request.request + implicit val r: Requests.OreRequest[_] = request.request checkNotNull(request, "null request", "") checkNotNull(users, "null user base", "") users.current.subflatMap(_.id).value.map { userId => diff --git a/app/models/statistic/StatEntry.scala b/app/models/statistic/StatEntry.scala index 16b3afb1a..108751465 100644 --- a/app/models/statistic/StatEntry.scala +++ b/app/models/statistic/StatEntry.scala @@ -47,7 +47,7 @@ abstract class StatEntry[Subject <: Model](override val id: Option[Int] = None, OptionT.fromOption[Future](this._userId).flatMap(this.userBase.get(_)) } - def userId = _userId + def userId: Option[Int] = _userId /** * Sets the User associated with this entry, if any. diff --git a/app/models/statistic/VersionDownload.scala b/app/models/statistic/VersionDownload.scala index a8da2434a..e60d8a53e 100644 --- a/app/models/statistic/VersionDownload.scala +++ b/app/models/statistic/VersionDownload.scala @@ -34,7 +34,7 @@ case class VersionDownload(override val id: Option[Int] = None, override type M = VersionDownload override type T = VersionDownloadsTable - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): VersionDownload = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/user/LoggedAction.scala b/app/models/user/LoggedAction.scala index 9a553a4cf..22dce6687 100644 --- a/app/models/user/LoggedAction.scala +++ b/app/models/user/LoggedAction.scala @@ -2,6 +2,8 @@ package models.user import java.sql.Timestamp +import scala.collection.immutable + import com.github.tminglei.slickpg.InetString import controllers.sugar.Requests.AuthRequest import db.ModelService @@ -31,20 +33,23 @@ case class LoggedActionModel(override val id: Option[Int] = None, def address: InetString = _address def action: LoggedAction = _action - def oldState = _oldState - def newState = _newState - + def oldState: String = _oldState + def newState: String = _newState + def contextId: Int = _actionContextId + def actionType: LoggedActionContext = _action.context } sealed abstract class LoggedActionContext(val value: Int) extends IntEnumEntry object LoggedActionContext extends IntEnum[LoggedActionContext] { - case object Project extends LoggedActionContext(0) - case object Version extends LoggedActionContext(1) - case object ProjectPage extends LoggedActionContext(2) + case object Project extends LoggedActionContext(0) + case object Version extends LoggedActionContext(1) + case object ProjectPage extends LoggedActionContext(2) + case object User extends LoggedActionContext(3) + case object Organization extends LoggedActionContext(4) - val values = findValues + val values: immutable.IndexedSeq[LoggedActionContext] = findValues } @@ -60,14 +65,19 @@ case object LoggedAction extends IntEnum[LoggedAction] { case object ProjectMemberRemoved extends LoggedAction(5, "ProjectMemberRemoved", LoggedActionContext.Project, "A Member was removed from the project") case object ProjectIconChanged extends LoggedAction(6, "ProjectIconChanged", LoggedActionContext.Project, "The project icon was changed") case object ProjectPageEdited extends LoggedAction(7, "ProjectPageEdited", LoggedActionContext.ProjectPage, "A project page got edited") + case object ProjectFlagResolved extends LoggedAction(13, "ProjectFlagResolved", LoggedActionContext.Project, "The flag was resolved") case object VersionDeleted extends LoggedAction(8, "VersionDeleted", LoggedActionContext.Version, "The version was deleted") case object VersionUploaded extends LoggedAction(9, "VersionUploaded", LoggedActionContext.Version, "A new version was uploaded") case object VersionApproved extends LoggedAction(10, "VersionApproved", LoggedActionContext.Version, "The version was approved") case object VersionAsRecommended extends LoggedAction(11, "VersionAsRecommended", LoggedActionContext.Version, "The version was set as recommended version") case object VersionDescriptionEdited extends LoggedAction(12, "VersionDescriptionEdited", LoggedActionContext.Version, "The version description was edited") + case object VersionNonReviewChanged extends LoggedAction(17, "VersionNonReviewChanged", LoggedActionContext.Version, "If the review queue skip was changed") - val values = findValues + case object UserTaglineChanged extends LoggedAction(14, "UserTaglineChanged", LoggedActionContext.User, "The user tagline changed") + case object UserPgpKeySaved extends LoggedAction(15, "UserPgpKeySaved", LoggedActionContext.User, "The user saved a PGP Public Key") + case object UserPgpKeyRemoved extends LoggedAction(16, "UserPgpKeyRemoved", LoggedActionContext.User, "The user removed a PGP Public Key") + val values: immutable.IndexedSeq[LoggedAction] = findValues } diff --git a/app/models/user/Notification.scala b/app/models/user/Notification.scala index 3e15f7792..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 @@ -57,7 +60,7 @@ case class Notification(override val id: Option[Int] = None, * * @param read True if has been read */ - def setRead(read: Boolean) = Defined { + def setRead(read: Boolean): Future[Int] = Defined { this.read = read update(Read) } diff --git a/app/models/user/Organization.scala b/app/models/user/Organization.scala index 87557da6d..43335ddca 100644 --- a/app/models/user/Organization.scala +++ b/app/models/user/Organization.scala @@ -19,6 +19,8 @@ import slick.lifted.{Compiled, Rep, TableQuery} import scala.concurrent.{ExecutionContext, Future} +import util.functional.OptionT + /** * Represents an Ore Organization. An organization is like a [[User]] in the * sense that it shares many qualities with Users and also has a companion @@ -46,7 +48,17 @@ case class Organization(override val id: Option[Int] = None, /** * Contains all information for [[User]] memberships. */ - override val memberships = new MembershipDossier { + override val memberships: MembershipDossier { + type MembersTable = OrganizationMembersTable + + type MemberType = OrganizationMember + + type RoleTable = OrganizationRoleTable + + type ModelType = Organization + + type RoleType = OrganizationRole +} = new MembershipDossier { type ModelType = Organization type RoleType = OrganizationRole @@ -123,7 +135,7 @@ case class Organization(override val id: Option[Int] = None, * * @return This Organization as a User */ - def toUser(implicit ec: ExecutionContext) = this.service.getModelBase(classOf[UserBase]).withName(this.username) + def toUser(implicit ec: ExecutionContext): OptionT[Future, User] = this.service.getModelBase(classOf[UserBase]).withName(this.username) override val name: String = this.username override def url: String = this.username @@ -147,4 +159,4 @@ object Organization { r } } -} \ No newline at end of file +} diff --git a/app/models/user/Session.scala b/app/models/user/Session.scala index e5cb41781..f8e60e57b 100644 --- a/app/models/user/Session.scala +++ b/app/models/user/Session.scala @@ -7,7 +7,9 @@ import db.impl.access.UserBase import db.impl.model.OreModel import db.{Expirable, Model} -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Future} + +import util.functional.OptionT /** * Represents a persistant [[User]] session. @@ -33,7 +35,7 @@ case class Session(override val id: Option[Int] = None, * @param users UserBase instance * @return User session belongs to */ - def user(implicit users: UserBase, ec: ExecutionContext) = users.withName(this.username) + def user(implicit users: UserBase, ec: ExecutionContext): OptionT[Future, User] = users.withName(this.username) override def copyWith(id: Option[Int], theTime: Option[Timestamp]): Model = this.copy(id = id, createdAt = theTime) diff --git a/app/models/user/SignOn.scala b/app/models/user/SignOn.scala index fc5253be2..5daaad130 100644 --- a/app/models/user/SignOn.scala +++ b/app/models/user/SignOn.scala @@ -2,6 +2,8 @@ package models.user import java.sql.Timestamp +import scala.concurrent.Future + import db.impl.SignOnTable import db.impl.model.OreModel import db.impl.table.ModelKeys._ @@ -24,11 +26,11 @@ case class SignOn(override val id: Option[Int] = None, def isCompleted: Boolean = this._isCompleted - def setCompleted(completed: Boolean = true) = Defined { + def setCompleted(completed: Boolean = true): Future[Int] = Defined { this._isCompleted = completed update(IsCompleted) } - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): SignOn = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/user/User.scala b/app/models/user/User.scala index 2f429a182..23c8d8f61 100644 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -4,7 +4,7 @@ import java.sql.Timestamp import com.google.common.base.Preconditions._ import db.{ModelFilter, Named} -import db.access.ModelAccess +import db.access.{ModelAccess, ModelAssociationAccess} import db.impl.OrePostgresDriver.api._ import db.impl._ import db.impl.access.{OrganizationBase, UserBase} @@ -19,7 +19,6 @@ import ore.permission.role._ import ore.permission.scope._ import ore.user.Prompts.Prompt import ore.user.UserOwned -import org.spongepowered.play.discourse.model.DiscourseUser import play.api.mvc.Request import security.pgp.PGPPublicKeyInfo import security.spauth.SpongeUser @@ -31,6 +30,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 +54,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 @@ -145,7 +147,7 @@ case class User(override val id: Option[Int] = None, * * @param _lastPgpPubKeyUpdate Last time this User updated their public key */ - def setLastPgpPubKeyUpdate(_lastPgpPubKeyUpdate: Timestamp) = Defined { + def setLastPgpPubKeyUpdate(_lastPgpPubKeyUpdate: Timestamp): Future[Int] = Defined { this._lastPgpPubKeyUpdate = Option(_lastPgpPubKeyUpdate) update(LastPGPPubKeyUpdate) } @@ -248,6 +250,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. * @@ -357,25 +379,6 @@ case class User(override val id: Option[Int] = None, }.exists(identity) } - /** - * Fills the mutable field in this User with the specified User's - * non-missing mutable fields. - * - * @param user User to fill with - */ - def fill(user: DiscourseUser): Unit = { - if (user != null) { - this.setUsername(user.username) - user.createdAt.foreach(this.setJoinDate) - user.email.foreach(this.setEmail) - user.fullName.foreach(this.setFullName) - user.avatarTemplate.foreach(this.setAvatarUrl) - this.setGlobalRoles(user.groups - .flatMap(group => RoleTypes.values.find(_.roleId == group.id).map(_.asInstanceOf[RoleType])) - .toSet[RoleType]) - } - } - /** * Fills this User with the information SpongeUser provides. * @@ -385,6 +388,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") @@ -395,31 +399,12 @@ case class User(override val id: Option[Int] = None, } } - /** - * Pulls information from the forums and updates this User. - * - * @return This user - */ - def pullForumData()(implicit ec: ExecutionContext): Future[Unit] = { - // Exceptions are ignored - OptionT(this.forums.fetchUser(this.name).recover{case _: Exception => None}).cata((), fill) - } - - /** - * Pulls information from the forums and updates this User. - * - * @return This user - */ - def pullSpongeData()(implicit ec: ExecutionContext): Future[Unit] = { - this.auth.getUser(this.name).cata((), fill) - } - /** * Returns all [[Project]]s owned by this user. * * @return Projects owned by user */ - def projects = this.schema.getChildren[Project](classOf[Project], this) + def projects: ModelAccess[Project] = this.schema.getChildren[Project](classOf[Project], this) /** * Returns the Project with the specified name that this User owns. @@ -434,21 +419,21 @@ case class User(override val id: Option[Int] = None, * * @return ProjectRoles */ - def projectRoles = this.schema.getChildren[ProjectRole](classOf[ProjectRole], this) + def projectRoles: ModelAccess[ProjectRole] = this.schema.getChildren[ProjectRole](classOf[ProjectRole], this) /** * Returns the [[Organization]]s that this User owns. * * @return Organizations user owns */ - def ownedOrganizations = this.schema.getChildren[Organization](classOf[Organization], this) + def ownedOrganizations: ModelAccess[Organization] = this.schema.getChildren[Organization](classOf[Organization], this) /** * Returns the [[Organization]]s that this User belongs to. * * @return Organizations user belongs to */ - def organizations + def organizations: ModelAssociationAccess[OrganizationMembersTable, Organization] = this.schema.getAssociation[OrganizationMembersTable, Organization](classOf[OrganizationMembersTable], this) /** @@ -456,7 +441,7 @@ case class User(override val id: Option[Int] = None, * * @return OrganizationRoles */ - def organizationRoles = this.schema.getChildren[OrganizationRole](classOf[OrganizationRole], this) + def organizationRoles: ModelAccess[OrganizationRole] = this.schema.getChildren[OrganizationRole](classOf[OrganizationRole], this) /** * Returns true if this User is also an organization. @@ -486,7 +471,7 @@ case class User(override val id: Option[Int] = None, * * @return Projects user is watching */ - def watching = this.schema.getAssociation[ProjectWatchersTable, Project](classOf[ProjectWatchersTable], this) + def watching: ModelAssociationAccess[ProjectWatchersTable, Project] = this.schema.getAssociation[ProjectWatchersTable, Project](classOf[ProjectWatchersTable], this) /** * Sets the "watching" status on the specified project. @@ -494,7 +479,7 @@ case class User(override val id: Option[Int] = None, * @param project Project to update status on * @param watching True if watching */ - def setWatching(project: Project, watching: Boolean)(implicit ec: ExecutionContext) = { + def setWatching(project: Project, watching: Boolean)(implicit ec: ExecutionContext): Future[Any] = { checkNotNull(project, "null project", "") checkArgument(project.isDefined, "undefined project", "") val contains = this.watching.contains(project) @@ -509,7 +494,7 @@ case class User(override val id: Option[Int] = None, * * @return Flags submitted by user */ - def flags = this.schema.getChildren[Flag](classOf[Flag], this) + def flags: ModelAccess[Flag] = this.schema.getChildren[Flag](classOf[Flag], this) /** * Returns true if the User has an unresolved [[Flag]] on the specified @@ -529,7 +514,7 @@ case class User(override val id: Option[Int] = None, * * @return User notifications */ - def notifications = this.schema.getChildren[Notification](classOf[Notification], this) + def notifications: ModelAccess[Notification] = this.schema.getChildren[Notification](classOf[Notification], this) /** * Sends a [[Notification]] to this user. @@ -537,7 +522,7 @@ case class User(override val id: Option[Int] = None, * @param notification Notification to send * @return Future result */ - def sendNotification(notification: Notification)(implicit ec: ExecutionContext) = { + def sendNotification(notification: Notification)(implicit ec: ExecutionContext): Future[Notification] = { checkNotNull(notification, "null notification", "") this.config.debug("Sending notification: " + notification, -1) this.service.access[Notification](classOf[Notification]).add(notification.copy(userId = this.id.get)) @@ -584,29 +569,16 @@ case class User(override val id: Option[Int] = None, if (isDefined) update(ReadPrompts) } - def name = this.username - def url = this.username - override val scope = GlobalScope - override def userId = this.id.get - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(createdAt = theTime) + def name: String = this.username + def url: String = this.username + override val scope: GlobalScope.type = GlobalScope + override def userId: Int = this.id.get + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): User = this.copy(createdAt = theTime) } object User { - /** - * Creates a new [[User]] from the specified [[DiscourseUser]]. - * - * @param toConvert User to convert - * @return Ore User - */ - @deprecated("use fromSponge instead", "Oct 14, 2016, 1:45 PM PDT") - def fromDiscourse(toConvert: DiscourseUser)(implicit ec: ExecutionContext): User = { - val user = User() - user.fill(toConvert) - user.copy(id = Some(toConvert.id)) - } - /** * Create a new [[User]] from the specified [[SpongeUser]]. * diff --git a/app/models/user/role/ProjectRole.scala b/app/models/user/role/ProjectRole.scala index cfb825162..6abb8994e 100644 --- a/app/models/user/role/ProjectRole.scala +++ b/app/models/user/role/ProjectRole.scala @@ -42,6 +42,6 @@ case class ProjectRole(override val id: Option[Int] = None, ) override def subject(implicit ec: ExecutionContext): Future[Visitable] = this.project - override def copyWith(id: Option[Int], theTime: Option[Timestamp]) = this.copy(id = id, createdAt = theTime) + override def copyWith(id: Option[Int], theTime: Option[Timestamp]): ProjectRole = this.copy(id = id, createdAt = theTime) } diff --git a/app/models/user/role/RoleModel.scala b/app/models/user/role/RoleModel.scala index a9236ca07..c428f7471 100644 --- a/app/models/user/role/RoleModel.scala +++ b/app/models/user/role/RoleModel.scala @@ -47,7 +47,7 @@ abstract class RoleModel(override val id: Option[Int], * * @param accepted True if role accepted */ - def setAccepted(accepted: Boolean) = Defined { + def setAccepted(accepted: Boolean): Future[Int] = Defined { this._isAccepted = accepted update(IsAccepted) } @@ -59,7 +59,7 @@ abstract class RoleModel(override val id: Option[Int], */ def isAccepted: Boolean = this._isAccepted - override def roleType = this._roleType + override def roleType: RoleType = this._roleType /** * Sets this role's [[RoleType]]. diff --git a/app/models/viewhelper/HeaderData.scala b/app/models/viewhelper/HeaderData.scala index 414313871..15e0ced3f 100644 --- a/app/models/viewhelper/HeaderData.scala +++ b/app/models/viewhelper/HeaderData.scala @@ -4,7 +4,7 @@ import controllers.sugar.Requests.ProjectRequest import db.ModelService import db.impl.OrePostgresDriver.api._ import db.impl.access.OrganizationBase -import db.impl.{ProjectTableMain, SessionTable, UserTable, VersionTable} +import db.impl._ import models.project.VisibilityTypes import models.user.User import ore.permission._ @@ -32,11 +32,11 @@ case class HeaderData(currentUser: Option[User] = None, ) { // Just some helpers in templates: - def isAuthenticated = currentUser.isDefined + def isAuthenticated: Boolean = currentUser.isDefined - def hasUser = currentUser.isDefined + def hasUser: Boolean = currentUser.isDefined - def isCurrentUser(userId: Int) = currentUser.flatMap(_.id).contains(userId) + def isCurrentUser(userId: Int): Boolean = currentUser.flatMap(_.id).contains(userId) def globalPerm(perm: Permission): Boolean = globalPermissions.getOrElse(perm, false) @@ -57,6 +57,7 @@ object HeaderData { ViewLogs -> false, HideProjects -> false, HardRemoveProject -> false, + HardRemoveVersion -> false, UserAdmin -> false, HideProjects -> false) @@ -115,13 +116,25 @@ object HeaderData { } + private def flagQueue()(implicit ec: ExecutionContext, db: JdbcBackend#DatabaseDef) : Future[Boolean] = { + val tableFlags = TableQuery[FlagTable] + + val query = for { + v <- tableFlags if v.isResolved === false + } yield { + v + } + + db.run(query.exists.result) + } + private def getHeaderData(user: User)(implicit ec: ExecutionContext, db: JdbcBackend#DatabaseDef) = { perms(Some(user)).flatMap { perms => ( user.hasNotice, user.notifications.filterNot(_.read).map(_.nonEmpty), - user.flags.filterNot(_.isResolved).map(_.nonEmpty), + flagQueue(), projectApproval(user), if (perms(ReviewProjects)) reviewQueue() else Future.successful(false) ).parMapN { (hasNotice, unreadNotif, unresolvedFlags, hasProjectApprovals, hasReviewQueue) => @@ -150,10 +163,11 @@ object HeaderData { user can ViewLogs in GlobalScope map ((ViewLogs, _)), user can HideProjects in GlobalScope map ((HideProjects, _)), user can HardRemoveProject in GlobalScope map ((HardRemoveProject, _)), + user can HardRemoveVersion in GlobalScope map ((HardRemoveProject, _)), user can UserAdmin in GlobalScope map ((UserAdmin, _)), ).parMapN { - case (reviewFlags, reviewVisibility, reviewProjects, viewStats, viewHealth, viewLogs, hideProjects, hardRemoveProject, userAdmin) => - val perms = Seq(reviewFlags, reviewVisibility, reviewProjects, viewStats, viewHealth, viewLogs, hideProjects, hardRemoveProject, userAdmin) + case (reviewFlags, reviewVisibility, reviewProjects, viewStats, viewHealth, viewLogs, hideProjects, hardRemoveProject, hardRemoveVersion, userAdmin) => + val perms = Seq(reviewFlags, reviewVisibility, reviewProjects, viewStats, viewHealth, viewLogs, hideProjects, hardRemoveProject, hardRemoveVersion, userAdmin) perms.toMap } } diff --git a/app/models/viewhelper/JoinableData.scala b/app/models/viewhelper/JoinableData.scala index fb0900070..a56c868a4 100644 --- a/app/models/viewhelper/JoinableData.scala +++ b/app/models/viewhelper/JoinableData.scala @@ -14,9 +14,9 @@ trait JoinableData[R <: RoleModel, M <: Member[R], T <: Joinable[M]] { val ownerRole: R val members: Seq[(R, User)] - def roleClass = ownerRole.getClass.asInstanceOf[Class[_ <: Role]] + def roleClass: Class[_ <: Role] = ownerRole.getClass.asInstanceOf[Class[_ <: Role]] - def filteredMembers(implicit request: OreRequest[_]) = { + def filteredMembers(implicit request: OreRequest[_]): Seq[(R, User)] = { if (request.data.globalPerm(EditSettings) || // has EditSettings show all request.currentUser.map(_.id.get).contains(joinable.ownerId) // Current User is owner ) members diff --git a/app/models/viewhelper/OrganizationData.scala b/app/models/viewhelper/OrganizationData.scala index abc7a10e3..d6884a965 100644 --- a/app/models/viewhelper/OrganizationData.scala +++ b/app/models/viewhelper/OrganizationData.scala @@ -1,7 +1,11 @@ package models.viewhelper import db.ModelService -import models.user.role.OrganizationRole +import db.impl.OrePostgresDriver.api._ +import db.impl._ +import db.impl.access.UserBase +import models.project.Project +import models.user.role.{OrganizationRole, ProjectRole} import models.user.{Organization, User} import ore.organization.OrganizationMember import ore.permission._ @@ -9,13 +13,14 @@ import play.api.cache.AsyncCacheApi import slick.jdbc.JdbcBackend import scala.concurrent.{ExecutionContext, Future} - +import slick.lifted.TableQuery import util.functional.OptionT import util.instances.future._ case class OrganizationData(joinable: Organization, ownerRole: OrganizationRole, members: Seq[(OrganizationRole, User)], // TODO sorted/reverse + projectRoles: Seq[(ProjectRole, Project)] ) extends JoinableData[OrganizationRole, OrganizationMember, Organization] { @@ -26,19 +31,31 @@ case class OrganizationData(joinable: Organization, object OrganizationData { val noPerms: Map[Permission, Boolean] = Map(EditSettings -> false) - def cacheKey(orga: Organization) = "organization" + orga.id.get + def cacheKey(orga: Organization): String = "organization" + orga.id.get def of[A](orga: Organization)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext, service: ModelService): Future[OrganizationData] = { - implicit val users = orga.userBase + implicit val users: UserBase = orga.userBase for { role <- orga.owner.headRole members <- orga.memberships.members memberRoles <- Future.sequence(members.map(_.headRole)) memberUser <- Future.sequence(memberRoles.map(_.user)) + projectRoles <- db.run(queryProjectRoles(orga.id.get).result) } yield { val members = memberRoles zip memberUser - OrganizationData(orga, role, members.toSeq) + OrganizationData(orga, role, members.toSeq, projectRoles) + } + } + + private def queryProjectRoles(userId: Int) = { + val tableProjectRole = TableQuery[ProjectRoleTable] + val tableProject = TableQuery[ProjectTableMain] + + for { + (role, project) <- tableProjectRole.join(tableProject).on(_.projectId === _.id) if role.userId === userId + } yield { + (role, project) } } diff --git a/app/models/viewhelper/ProjectData.scala b/app/models/viewhelper/ProjectData.scala index 70885603e..5081d9433 100644 --- a/app/models/viewhelper/ProjectData.scala +++ b/app/models/viewhelper/ProjectData.scala @@ -3,7 +3,7 @@ package models.viewhelper import controllers.sugar.Requests.OreRequest import db.impl.OrePostgresDriver.api._ import db.impl.{ProjectRoleTable, UserTable} -import models.admin.VisibilityChange +import models.admin.ProjectVisibilityChange import models.project._ import models.user.User import models.user.role.ProjectRole @@ -15,6 +15,8 @@ import slick.lifted.TableQuery import scala.concurrent.{ExecutionContext, Future} +import db.impl.access.UserBase +import play.twirl.api.Html import util.syntax._ import util.instances.future._ @@ -24,30 +26,31 @@ import util.instances.future._ case class ProjectData(joinable: Project, projectOwner: User, ownerRole: ProjectRole, - versions: Int, // project.versions.size + publicVersions: Int, // project.versions.count(_.visibility === VisibilityTypes.Public) settings: ProjectSettings, members: Seq[(ProjectRole, User)], projectLogSize: Int, flags: Seq[(Flag, String, Option[String])], // (Flag, user.name, resolvedBy) noteCount: Int, // getNotes.size - lastVisibilityChange: Option[VisibilityChange], - lastVisibilityChangeUser: String // users.get(project.lastVisibilityChange.get.createdBy.get).map(_.username).getOrElse("Unknown") + lastVisibilityChange: Option[ProjectVisibilityChange], + lastVisibilityChangeUser: String, // users.get(project.lastVisibilityChange.get.createdBy.get).map(_.username).getOrElse("Unknown") + recommendedVersion: Option[Version] ) extends JoinableData[ProjectRole, ProjectMember, Project] { - def flagCount = flags.size + def flagCount: Int = flags.size def project: Project = joinable - def visibility = project.visibility + def visibility: VisibilityTypes.Visibility = project.visibility def fullSlug = s"""/${project.ownerName}/${project.slug}""" - def renderVisibilityChange = lastVisibilityChange.map(_.renderComment()) + def renderVisibilityChange: Option[Html] = lastVisibilityChange.map(_.renderComment()) } object ProjectData { - def cacheKey(project: Project) = "project" + project.id.get + def cacheKey(project: Project): String = "project" + project.id.get def of[A](request: OreRequest[A], project: PendingProject)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext): ProjectData = { @@ -63,6 +66,7 @@ object ProjectData { val logSize = 0 val lastVisibilityChange = None val lastVisibilityChangeUser = "-" + val recommendedVersion = None val data = new ProjectData(project.underlying, projectOwner, @@ -74,14 +78,15 @@ object ProjectData { Seq.empty, 0, lastVisibilityChange, - lastVisibilityChangeUser) + lastVisibilityChangeUser, + recommendedVersion) data } def of[A](project: Project)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext): Future[ProjectData] = { - implicit val userBase = project.userBase + implicit val userBase: UserBase = project.userBase val flagsFut = project.flags.all val flagUsersFut = flagsFut.flatMap(flags => Future.sequence(flags.map(_.user))) @@ -96,16 +101,17 @@ object ProjectData { project.settings, project.owner.user, project.owner.headRole, - project.versions.size, + project.versions.count(_.visibility === VisibilityTypes.Public), members(project), project.logger.flatMap(_.entries.size), flagsFut, flagUsersFut, flagResolvedFut, lastVisibilityChangeFut, - lastVisibilityChangeUserFut + lastVisibilityChangeUserFut, + project.recommendedVersion ).parMapN { - case (settings, projectOwner, ownerRole, versions, members, logSize, flags, flagUsers, flagResolved, lastVisibilityChange, lastVisibilityChangeUser) => + case (settings, projectOwner, ownerRole, versions, members, logSize, flags, flagUsers, flagResolved, lastVisibilityChange, lastVisibilityChangeUser, recommendedVersion) => val noteCount = project.getNotes().size val flagData = flags zip flagUsers zip flagResolved map { case ((fl, user), resolved) => (fl, user.name, resolved.map(_.username)) @@ -122,7 +128,8 @@ object ProjectData { flagData.toSeq, noteCount, lastVisibilityChange, - lastVisibilityChangeUser) + lastVisibilityChangeUser, + Some(recommendedVersion)) } } @@ -144,4 +151,4 @@ object ProjectData { -} \ No newline at end of file +} diff --git a/app/models/viewhelper/ScopedOrganizationData.scala b/app/models/viewhelper/ScopedOrganizationData.scala index 03d919107..01d8ff543 100644 --- a/app/models/viewhelper/ScopedOrganizationData.scala +++ b/app/models/viewhelper/ScopedOrganizationData.scala @@ -8,6 +8,7 @@ import slick.jdbc.JdbcBackend import scala.concurrent.{ExecutionContext, Future} +import db.impl.access.UserBase import util.functional.OptionT import util.instances.future._ @@ -21,7 +22,7 @@ object ScopedOrganizationData { def of[A](currentUser: Option[User], orga: Organization)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext, service: ModelService): Future[ScopedOrganizationData] = { - implicit val users = orga.userBase + implicit val users: UserBase = orga.userBase if (currentUser.isEmpty) Future.successful(noScope) else { for { @@ -38,4 +39,4 @@ object ScopedOrganizationData { service: ModelService): OptionT[Future, ScopedOrganizationData] = { OptionT.fromOption[Future](orga).semiFlatMap(of(currentUser, _)) } -} \ No newline at end of file +} diff --git a/app/models/viewhelper/ScopedProjectData.scala b/app/models/viewhelper/ScopedProjectData.scala index 5a5e45576..d24ba2305 100644 --- a/app/models/viewhelper/ScopedProjectData.scala +++ b/app/models/viewhelper/ScopedProjectData.scala @@ -29,10 +29,11 @@ object ScopedProjectData { user can EditSettings in project map ((EditSettings, _)), user can EditChannels in project map ((EditChannels, _)), user can EditVersions in project map ((EditVersions, _)), + user can UploadVersions in project map ((UploadVersions, _)), Future.sequence(VisibilityTypes.values.map(_.permission).map(p => user can p in project map ((p, _)))) ).parMapN { - case (canPostAsOwnerOrga, uProjectFlags, starred, watching, editPages, editSettings, editChannels, editVersions, visibilities) => - val perms = visibilities + editPages + editSettings + editChannels + editVersions + case (canPostAsOwnerOrga, uProjectFlags, starred, watching, editPages, editSettings, editChannels, editVersions, uploadVersions, visibilities) => + val perms = visibilities + editPages + editSettings + editChannels + editVersions + uploadVersions ScopedProjectData(canPostAsOwnerOrga, uProjectFlags, starred, watching, perms.toMap) } } getOrElse Future.successful(noScope) @@ -51,4 +52,4 @@ case class ScopedProjectData(canPostAsOwnerOrga: Boolean = false, def perms(perm: Permission): Boolean = permissions.getOrElse(perm, false) -} \ No newline at end of file +} diff --git a/app/models/viewhelper/UserData.scala b/app/models/viewhelper/UserData.scala index c28e124c6..856741675 100644 --- a/app/models/viewhelper/UserData.scala +++ b/app/models/viewhelper/UserData.scala @@ -13,6 +13,7 @@ import slick.lifted.TableQuery import db.impl.OrePostgresDriver.api._ import scala.concurrent.{ExecutionContext, Future} +import play.api.mvc.Call import util.syntax._ // TODO separate Scoped UserData @@ -25,14 +26,14 @@ case class UserData(headerData: HeaderData, userPerm: Map[Permission, Boolean], orgaPerm: Map[Permission, Boolean]) { - def global = headerData + def global: HeaderData = headerData - def hasUser = global.hasUser - def currentUser = global.currentUser + def hasUser: Boolean = global.hasUser + def currentUser: Option[User] = global.currentUser - def isCurrent = currentUser.contains(user) + def isCurrent: Boolean = currentUser.contains(user) - def pgpFormCall = { + def pgpFormCall: Call = { user.pgpPubKey.map { _ => routes.Users.verify(Some(routes.Users.deletePgpPublicKey(user.name, None, None).path)) } getOrElse { @@ -40,7 +41,7 @@ case class UserData(headerData: HeaderData, } } - def pgpFormClass = user.pgpPubKey.map(_ => "pgp-delete").getOrElse("") + def pgpFormClass: String = user.pgpPubKey.map(_ => "pgp-delete").getOrElse("") } diff --git a/app/models/viewhelper/VersionData.scala b/app/models/viewhelper/VersionData.scala index 7dd2eccbd..39bfbc600 100644 --- a/app/models/viewhelper/VersionData.scala +++ b/app/models/viewhelper/VersionData.scala @@ -2,36 +2,37 @@ package models.viewhelper import controllers.sugar.Requests.ProjectRequest import db.ModelService +import db.impl.access.ProjectBase import models.project.{Channel, Project, Version} +import ore.Platforms import ore.project.Dependency -import ore.project.Dependency._ import play.api.cache.AsyncCacheApi import slick.jdbc.JdbcBackend +import util.instances.future._ +import util.syntax._ import scala.concurrent.{ExecutionContext, Future} -import util.syntax._ -import util.instances.future._ - case class VersionData(p: ProjectData, v: Version, c: Channel, approvedBy: Option[String], // Reviewer if present dependencies: Seq[(Dependency, Option[Project])]) { - def isRecommended = p.project.recommendedVersionId == v.id + def isRecommended: Boolean = p.project.recommendedVersionId == v.id def fullSlug = s"""${p.fullSlug}/versions/${v.versionString}""" - - def filteredDependencies = { - dependencies.filterNot(_._1.pluginId == SpongeApiId) - .filterNot(_._1.pluginId == MinecraftId) - .filterNot(_._1.pluginId == ForgeId) + /** + * Filters out platforms from the dependencies + * @return filtered dependencies + */ + def filteredDependencies: Seq[(Dependency, Option[Project])] = { + dependencies.filterNot(d => Platforms.values.map(_.dependencyId).contains(d._1.pluginId)) } } object VersionData { def of[A](request: ProjectRequest[A], version: Version)(implicit cache: AsyncCacheApi, db: JdbcBackend#DatabaseDef, ec: ExecutionContext, service: ModelService): Future[VersionData] = { - implicit val base = version.projectBase + implicit val base: ProjectBase = version.projectBase val depsFut = Future.sequence(version.dependencies.map(dep => dep.project.value.map((dep, _)))) (version.channel, version.reviewer.map(_.name).value, depsFut).parMapN { diff --git a/app/ore/Cacheable.scala b/app/ore/Cacheable.scala index ecd984a68..812170b06 100644 --- a/app/ore/Cacheable.scala +++ b/app/ore/Cacheable.scala @@ -19,11 +19,11 @@ trait Cacheable { /** * Caches this. */ - def cache() = this.cacheApi.set(this.key, this) + def cache(): Unit = this.cacheApi.set(this.key, this) /** * Removes this from the Cache. */ - def free() = this.cacheApi.remove(this.key) + def free(): Unit = this.cacheApi.remove(this.key) } diff --git a/app/ore/OreConfig.scala b/app/ore/OreConfig.scala index d4386dda0..073200602 100644 --- a/app/ore/OreConfig.scala +++ b/app/ore/OreConfig.scala @@ -16,18 +16,18 @@ import util.StringUtils._ final class OreConfig @Inject()(config: Configuration) { // Sub-configs - lazy val root = this.config - lazy val app = this.config.get[Configuration]("application") - lazy val play = this.config.get[Configuration]("play") - lazy val ore = this.config.get[Configuration]("ore") - lazy val channels = this.ore.get[Configuration]("channels") - lazy val pages = this.ore.get[Configuration]("pages") - lazy val projects = this.ore.get[Configuration]("projects") - lazy val users = this.ore.get[Configuration]("users") - lazy val orgs = this.ore.get[Configuration]("orgs") - lazy val forums = this.root.get[Configuration]("discourse") - lazy val sponge = this.root.get[Configuration]("sponge") - lazy val security = this.root.get[Configuration]("security") + lazy val root: Configuration = this.config + lazy val app: Configuration = this.config.get[Configuration]("application") + lazy val play: Configuration = this.config.get[Configuration]("play") + lazy val ore: Configuration = this.config.get[Configuration]("ore") + lazy val channels: Configuration = this.ore.get[Configuration]("channels") + lazy val pages: Configuration = this.ore.get[Configuration]("pages") + lazy val projects: Configuration = this.ore.get[Configuration]("projects") + lazy val users: Configuration = this.ore.get[Configuration]("users") + lazy val orgs: Configuration = this.ore.get[Configuration]("orgs") + lazy val forums: Configuration = this.root.get[Configuration]("discourse") + lazy val sponge: Configuration = this.root.get[Configuration]("sponge") + lazy val security: Configuration = this.root.get[Configuration]("security") /** * The default color used for Channels. @@ -73,19 +73,19 @@ final class OreConfig @Inject()(config: Configuration) { def getSuggestedNameForVersion(version: String): String = Option(new ComparableVersion(version).getFirstString).getOrElse(this.defaultChannelName) - lazy val debugLevel = this.ore.get[Int]("debug-level") + lazy val debugLevel: Int = this.ore.get[Int]("debug-level") /** Returns true if the application is running in debug mode. */ def isDebug: Boolean = this.ore.get[Boolean]("debug") /** Sends a debug message if in debug mode */ - def debug(msg: Any, level: Int = 1) = { + def debug(msg: Any, level: Int = 1): Unit = { if (isDebug && (level == this.debugLevel || level == -1)) Logger.debug(msg.toString) } /** Asserts that the application is in debug mode. */ - def checkDebug() = { + def checkDebug(): Unit = { if(!isDebug) throw new UnsupportedOperationException("this function is supported in debug mode only") } diff --git a/app/ore/OreEnv.scala b/app/ore/OreEnv.scala index f6c04759d..ef1200f85 100644 --- a/app/ore/OreEnv.scala +++ b/app/ore/OreEnv.scala @@ -1,6 +1,6 @@ package ore -import java.nio.file.Paths +import java.nio.file.{Path, Paths} import javax.inject.Inject import play.api.Environment @@ -10,11 +10,11 @@ import play.api.Environment */ final class OreEnv @Inject()(val env: Environment, config: OreConfig) { - lazy val root = this.env.rootPath.toPath - lazy val public = this.root.resolve("public") - lazy val conf = this.root.resolve("conf") - lazy val uploads = Paths.get(this.config.app.get[String]("uploadsDir")) - lazy val plugins = this.uploads.resolve("plugins") - lazy val tmp = this.uploads.resolve("tmp") + lazy val root: Path = this.env.rootPath.toPath + lazy val public: Path = this.root.resolve("public") + lazy val conf: Path = this.root.resolve("conf") + lazy val uploads: Path = Paths.get(this.config.app.get[String]("uploadsDir")) + lazy val plugins: Path = this.uploads.resolve("plugins") + lazy val tmp: Path = this.uploads.resolve("tmp") } diff --git a/app/ore/Platforms.scala b/app/ore/Platforms.scala index a221fe2c9..d9c9f1279 100755 --- a/app/ore/Platforms.scala +++ b/app/ore/Platforms.scala @@ -1,13 +1,98 @@ -package ore - -import scala.language.implicitConversions - -object Platforms extends Enumeration { - - val Sponge = Platform(0, "Sponge") - val Forge = Platform(1, "Forge") - - case class Platform(override val id: Int, name: String) extends super.Val(id, name) - implicit def convert(v: Value): Platform = v.asInstanceOf[Platform] - -} +package ore + +import models.project.{Tag, TagColors} +import models.project.TagColors.TagColor +import ore.project.Dependency + +import scala.language.implicitConversions + +/** + * The Platform a plugin/mod runs on + * + * @author phase + */ +object Platforms extends Enumeration { + + val Sponge = Platform(0, "Sponge", SpongeCategory, 0, "spongeapi", + TagColors.Sponge, "https://spongepowered.org/downloads") + + val SpongeForge = Platform(2, "SpongeForge", SpongeCategory, 2, "spongeforge", + TagColors.SpongeForge, "https://www.spongepowered.org/downloads/spongeforge") + + val SpongeVanilla = Platform(3, "SpongeVanilla", SpongeCategory, 2, "spongevanilla", + TagColors.SpongeVanilla, "https://www.spongepowered.org/downloads/spongevanilla") + + val SpongeCommon = Platform(4, "SpongeCommon", SpongeCategory, 1, "sponge", + TagColors.SpongeCommon, "https://www.spongepowered.org/downloads") + + val Lantern = Platform(5, "Lantern", SpongeCategory, 2, "lantern", + TagColors.Lantern, "https://www.lanternpowered.org/") + + val Forge = Platform(1, "Forge", ForgeCategory, 0, "forge", + TagColors.Forge, "https://files.minecraftforge.net/") + + case class Platform(override val id: Int, + name: String, + platformCategory: PlatformCategory, + priority: Int, + dependencyId: String, + tagColor: TagColor, + url: String + ) extends super.Val(id, name) { + + def toGhostTag(version: String): Tag = Tag(None, List(), name, version, tagColor) + + } + + implicit def convert(v: Value): Platform = v.asInstanceOf[Platform] + + def getPlatforms(dependencyIds: Seq[String]): Seq[Platform] = { + Platforms.values + .filter(p => dependencyIds.contains(p.dependencyId)) + .groupBy(_.platformCategory) + .flatMap(_._2.groupBy(_.priority).maxBy(_._1)._2) + .map(_.asInstanceOf[Platform]) + .toSeq + } + + def getPlatformGhostTags(dependencies: Seq[Dependency]): Seq[Tag] = { + Platforms.values + .filter(p => dependencies.map(_.pluginId).contains(p.dependencyId)) + .groupBy(_.platformCategory) + .flatMap(_._2.groupBy(_.priority).maxBy(_._1)._2) + .map(p => p.toGhostTag(dependencies.find(_.pluginId == p.dependencyId).get.version)) + .toSeq + } + +} + +/** + * The category of a platform. + * Examples would be + * + * Sponge <- SpongeAPI, SpongeForge, SpongeVanilla + * Forge <- Forge (maybe Rift if that doesn't die?) + * Bukkit <- Bukkit, Spigot, Paper + * Canary <- Canary, Neptune + * + * @author phase + */ +sealed trait PlatformCategory { + val name: String + + def getPlatforms: Seq[Platforms.Value] = { + Platforms.values.filter(p => p.platformCategory == this).toSeq + } +} + +case object SpongeCategory extends PlatformCategory { + override val name = "Sponge Plugins" +} + +case object ForgeCategory extends PlatformCategory { + override val name = "Forge Mods" +} + +object PlatformCategory { + def getPlatformCategories: Seq[PlatformCategory] = Seq(SpongeCategory, ForgeCategory) +} diff --git a/app/ore/StatTracker.scala b/app/ore/StatTracker.scala index 77edf5aa2..7cba16173 100755 --- a/app/ore/StatTracker.scala +++ b/app/ore/StatTracker.scala @@ -80,7 +80,7 @@ object StatTracker { * @param request Request with cookie * @return New or existing cookie */ - def currentCookie(implicit request: RequestHeader) + def currentCookie(implicit request: RequestHeader): String = request.cookies.get(COOKIE_NAME).map(_.value).getOrElse(UUID.randomUUID.toString) /** @@ -90,7 +90,7 @@ object StatTracker { * @param request Request to get address of * @return Remote address */ - def remoteAddress(implicit request: RequestHeader) = { + def remoteAddress(implicit request: RequestHeader): String = { request.headers.get("X-Forwarded-For") match { case None => request.remoteAddress case Some(header) => header.split(',').headOption.map(_.trim).getOrElse(request.remoteAddress) @@ -100,9 +100,10 @@ object StatTracker { } class OreStatTracker @Inject()(service: ModelService, override val bakery: Bakery) extends StatTracker { - override val users = this.service.getModelBase(classOf[UserBase]) - override val projects = this.service.getModelBase(classOf[ProjectBase]) - override val viewSchema = this.service.getSchemaByModel(classOf[ProjectView]).asInstanceOf[StatSchema[ProjectView]] - override val downloadSchema = this.service.getSchemaByModel(classOf[VersionDownload]) + override val users : UserBase = this.service.getModelBase(classOf[UserBase]) + override val projects : ProjectBase = this.service.getModelBase(classOf[ProjectBase]) + override val viewSchema : StatSchema[ProjectView] = this.service.getSchemaByModel(classOf[ProjectView]).asInstanceOf[StatSchema[ProjectView]] + + override val downloadSchema: StatSchema[VersionDownload] = this.service.getSchemaByModel(classOf[VersionDownload]) .asInstanceOf[StatSchema[VersionDownload]] } diff --git a/app/ore/permission/Permission.scala b/app/ore/permission/Permission.scala index 5aa417b51..645353d41 100644 --- a/app/ore/permission/Permission.scala +++ b/app/ore/permission/Permission.scala @@ -5,24 +5,27 @@ import ore.permission.role._ /** * Represents a permission for a user to do something in the application. */ -sealed trait Permission { def trust: Trust } -case object EditChannels extends Permission { val trust = Standard } -case object EditPages extends Permission { val trust = Limited } -case object EditSettings extends Permission { val trust = Lifted } -case object EditVersions extends Permission { val trust = Standard } -case object HideProjects extends Permission { val trust = Standard } -case object HardRemoveProject extends Permission { val trust = Absolute } -case object ReviewFlags extends Permission { val trust = Standard } -case object ReviewProjects extends Permission { val trust = Standard } -case object ReviewVisibility extends Permission { val trust = Standard } -case object ViewHealth extends Permission { val trust = Standard } -case object ViewLogs extends Permission { val trust = Lifted } -case object UserAdmin extends Permission { val trust = Lifted } -case object ResetOre extends Permission { val trust = Absolute } -case object SeedOre extends Permission { val trust = Absolute } -case object MigrateOre extends Permission { val trust = Absolute } -case object CreateProject extends Permission { val trust = Lifted } -case object PostAsOrganization extends Permission { val trust = Standard } -case object EditApiKeys extends Permission { val trust = Lifted } -case object ViewActivity extends Permission { val trust = Standard } -case object ViewStats extends Permission { val trust = Standard } +sealed abstract case class Permission(trust: Trust) +object ResetOre extends Permission(Absolute) +object SeedOre extends Permission(Absolute) +object MigrateOre extends Permission(Absolute) +object HardRemoveProject extends Permission(Absolute) +object HardRemoveVersion extends Permission(Absolute) +object CreateProject extends Permission(Absolute) +object ViewIp extends Permission(Absolute) +object EditSettings extends Permission(Lifted) +object ViewLogs extends Permission(Lifted) +object UserAdmin extends Permission(Lifted) +object EditApiKeys extends Permission(Lifted) +object UploadVersions extends Permission(Publish) +object ReviewFlags extends Permission(Moderation) +object ReviewProjects extends Permission(Moderation) +object HideProjects extends Permission(Moderation) +object EditChannels extends Permission(Moderation) +object ReviewVisibility extends Permission(Moderation) +object ViewHealth extends Permission(Moderation) +object PostAsOrganization extends Permission(Moderation) +object ViewActivity extends Permission(Moderation) +object ViewStats extends Permission(Moderation) +object EditPages extends Permission(Limited) +object EditVersions extends Permission(Limited) diff --git a/app/ore/permission/role/RoleTypes.scala b/app/ore/permission/role/RoleTypes.scala index 63396b9f8..4f3a9873b 100644 --- a/app/ore/permission/role/RoleTypes.scala +++ b/app/ore/permission/role/RoleTypes.scala @@ -15,19 +15,19 @@ object RoleTypes extends Enumeration { // Global - val Admin = new RoleType( 0, "Ore_Admin", 61, classOf[GlobalRole], Absolute, "Ore Admin", Red) - val Mod = new RoleType( 1, "Ore_Moderator", 62, classOf[GlobalRole], Lifted, "Ore Moderator", Aqua) - val SpongeLeader = new RoleType( 2, "Sponge_Leader", 44, classOf[GlobalRole], Default, "Sponge Leader", Amber) - val TeamLeader = new RoleType( 3, "Team_Leader", 58, classOf[GlobalRole], Default, "Team Leader", Amber) - val CommunityLeader = new RoleType( 4, "Community_Leader", 59, classOf[GlobalRole], Default, "Community Leader", Amber) - val Staff = new RoleType( 5, "Sponge_Staff", 3, classOf[GlobalRole], Default, "Sponge Staff", Amber) - val SpongeDev = new RoleType( 6, "Sponge_Developer", 41, classOf[GlobalRole], Default, "Sponge Developer", Green) - val OreDev = new RoleType(27, "Ore_Developer", 66, classOf[GlobalRole], Default, "Ore Developer", Orange) - val WebDev = new RoleType( 7, "Web_Developer", 45, classOf[GlobalRole], Default, "Web Developer", Blue) - val Scribe = new RoleType( 8, "Sponge_Documenter", 51, classOf[GlobalRole], Default, "Sponge Documenter", Aqua) - val Support = new RoleType( 9, "Sponge_Support", 43, classOf[GlobalRole], Default, "Sponge Support", Aqua) - val Contributor = new RoleType(10, "Sponge_Contributor",49, classOf[GlobalRole], Default, "Sponge Contributor", Green) - val Adviser = new RoleType(11, "Sponge_Adviser", 48, classOf[GlobalRole], Default, "Sponge Adviser", Aqua) + val Admin = new RoleType( 0, "Ore_Admin", 61, classOf[GlobalRole], Absolute, "Ore Admin", Red) + val Mod = new RoleType( 1, "Ore_Mod", 62, classOf[GlobalRole], Moderation, "Ore Moderator", Aqua) + val SpongeLeader = new RoleType( 2, "Sponge_Leader", 44, classOf[GlobalRole], Default, "Sponge Leader", Amber) + val TeamLeader = new RoleType( 3, "Team_Leader", 58, classOf[GlobalRole], Default, "Team Leader", Amber) + val CommunityLeader = new RoleType( 4, "Community_Leader", 59, classOf[GlobalRole], Default, "Community Leader", Amber) + val Staff = new RoleType( 5, "Sponge_Staff", 3, classOf[GlobalRole], Default, "Sponge Staff", Amber) + val SpongeDev = new RoleType( 6, "Sponge_Developer", 41, classOf[GlobalRole], Default, "Sponge Developer", Green) + val OreDev = new RoleType(27, "Ore_Dev", 66, classOf[GlobalRole], Default, "Ore Developer", Orange) + val WebDev = new RoleType( 7, "Web_dev", 45, classOf[GlobalRole], Default, "Web Developer", Blue) + val Documenter = new RoleType( 8, "Documenter", 51, classOf[GlobalRole], Default, "Documenter", Aqua) + val Support = new RoleType( 9, "Support", 43, classOf[GlobalRole], Default, "Support", Aqua) + val Contributor = new RoleType(10, "Contributor", 49, classOf[GlobalRole], Default, "Contributor", Green) + val Advisor = new RoleType(11, "Advisor", 48, classOf[GlobalRole], Default, "Advisor", Aqua) val StoneDonor = new DonorType(12, "Stone_Donor", 57, "Stone Donor", Gray) val QuartzDonor = new DonorType(13, "Quartz_Donor", 54, "Quartz Donor", Quartz) @@ -38,7 +38,7 @@ object RoleTypes extends Enumeration { // Project val ProjectOwner = new RoleType(17, "Project_Owner", -1, classOf[ProjectRole], Absolute, "Owner", Transparent, isAssignable = false) - val ProjectDev = new RoleType(18, "Project_Developer",-2, classOf[ProjectRole], Standard, "Developer",Transparent) + val ProjectDev = new RoleType(18, "Project_Developer",-2, classOf[ProjectRole], Publish, "Developer",Transparent) val ProjectEditor = new RoleType(19, "Project_Editor", -3, classOf[ProjectRole], Limited, "Editor", Transparent) val ProjectSupport = new RoleType(20, "Project_Support", -4, classOf[ProjectRole], Default, "Support", Transparent) @@ -47,7 +47,7 @@ object RoleTypes extends Enumeration { val Organization = new RoleType(21, "Organization", 64, classOf[OrganizationRole], Absolute, "Organization", Purple, isAssignable = false) val OrganizationOwner = new RoleType(22, "Organization_Owner", -5, classOf[OrganizationRole], Absolute, "Owner", Purple, isAssignable = false) val OrganizationAdmin = new RoleType(26, "Organization_Admin", -9, classOf[OrganizationRole], Lifted, "Admin", Purple) - val OrganizationDev = new RoleType(23, "Organization_Developer", -6, classOf[OrganizationRole], Standard, "Developer", Transparent) + val OrganizationDev = new RoleType(23, "Organization_Developer", -6, classOf[OrganizationRole], Publish, "Developer", Transparent) val OrganizationEditor = new RoleType(24, "Organization_Editor", -7, classOf[OrganizationRole], Limited, "Editor", Transparent) val OrganizationSupport = new RoleType(25, "Organization_Support", -8, classOf[OrganizationRole], Default, "Support", Transparent) diff --git a/app/ore/permission/role/Trust.scala b/app/ore/permission/role/Trust.scala index fba857582..ba6a079c5 100644 --- a/app/ore/permission/role/Trust.scala +++ b/app/ore/permission/role/Trust.scala @@ -5,7 +5,7 @@ package ore.permission.role */ sealed trait Trust extends Ordered[Trust] { def level: Int - override def compare(that: Trust) = this.level - that.level + override def compare(that: Trust): Int = this.level - that.level } /** @@ -23,14 +23,19 @@ case object Limited extends Trust { override val level = 1 } * User has a standard amount of trust and may perform moderator-like actions * within the site. */ -case object Standard extends Trust { override val level = 2 } +case object Moderation extends Trust { override val level = 2 } /** - * User that can perform any action but they are not on top. + * Users who can publish versions */ -case object Lifted extends Trust { override val level = 3 } +case object Publish extends Trust { override val level = 3 } + +/** + * User that can perform almost any action but they are not on top. + */ +case object Lifted extends Trust { override val level = 4 } /** * User is absolutely trusted and may perform any action. */ -case object Absolute extends Trust { override val level = 4 } +case object Absolute extends Trust { override val level = 5 } diff --git a/app/ore/project/Dependency.scala b/app/ore/project/Dependency.scala index 50d98ccdd..24a1c20b5 100755 --- a/app/ore/project/Dependency.scala +++ b/app/ore/project/Dependency.scala @@ -22,22 +22,3 @@ case class Dependency(pluginId: String, version: String) { def project(implicit projects: ProjectBase, ec: ExecutionContext): OptionT[Future, Project] = projects.withPluginId(this.pluginId) } - -object Dependency { - - /** - * The Sponge API dependency ID - */ - val SpongeApiId: String = "spongeapi" - - /** - * The Minecraft dependency ID - */ - val MinecraftId: String = "minecraft" - - /** - * The Forge dependency ID - */ - val ForgeId: String = "forge" - -} diff --git a/app/ore/project/NotifyWatchersTask.scala b/app/ore/project/NotifyWatchersTask.scala index 9105ca09f..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.defaultLang - - def run() = { + 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/ProjectSortingStrategies.scala b/app/ore/project/ProjectSortingStrategies.scala index 60e97b863..de447bbc0 100644 --- a/app/ore/project/ProjectSortingStrategies.scala +++ b/app/ore/project/ProjectSortingStrategies.scala @@ -1,6 +1,9 @@ package ore.project +import java.sql.Timestamp + import db.impl.OrePostgresDriver.api._ +import db.impl.ProjectTable import models.project.Project import slick.lifted.ColumnOrdered @@ -15,7 +18,7 @@ object ProjectSortingStrategies { ) /** The default strategy. */ - val Default = RecentlyUpdated + val Default: RecentlyUpdated.type = RecentlyUpdated /** * Returns the strategy with the specified ID. @@ -40,31 +43,31 @@ object ProjectSortingStrategies { } case object MostStars extends ProjectSortingStrategy { - def fn = _.stars.desc + def fn: ProjectTable => ColumnOrdered[Int] = _.stars.desc def title = "Most stars" def id = 0 } case object MostDownloads extends ProjectSortingStrategy { - def fn = _.downloads.desc + def fn: ProjectTable => ColumnOrdered[Int] = _.downloads.desc def title = "Most downloads" def id = 1 } case object MostViews extends ProjectSortingStrategy { - def fn = _.views.desc + def fn: ProjectTable => ColumnOrdered[Int] = _.views.desc def title = "Most views" def id = 2 } case object Newest extends ProjectSortingStrategy { - def fn = _.createdAt.desc + def fn: ProjectTable => ColumnOrdered[Timestamp] = _.createdAt.desc def title = "Newest" def id = 3 } case object RecentlyUpdated extends ProjectSortingStrategy { - def fn = _.lastUpdated.desc + def fn: ProjectTable => ColumnOrdered[Timestamp] = _.lastUpdated.desc def title = "Recently updated" def id = 4 } diff --git a/app/ore/project/ProjectTask.scala b/app/ore/project/ProjectTask.scala index 9c990c211..d075014b7 100644 --- a/app/ore/project/ProjectTask.scala +++ b/app/ore/project/ProjectTask.scala @@ -20,13 +20,13 @@ import ore.OreConfig class ProjectTask @Inject()(models: ModelService, actorSystem: ActorSystem, config: OreConfig)(implicit ec: ExecutionContext) extends Runnable { val Logger = play.api.Logger("ProjectTask") - val interval = this.config.projects.get[FiniteDuration]("check-interval") + val interval: FiniteDuration = this.config.projects.get[FiniteDuration]("check-interval") val draftExpire: Long = this.config.projects.getOptional[Long]("draft-expire").getOrElse(86400000) /** * Starts the task. */ - def start() = { + def start(): Unit = { this.actorSystem.scheduler.schedule(this.interval, this.interval, this) Logger.info(s"Initialized. First run in ${this.interval.toString}.") } @@ -34,7 +34,7 @@ class ProjectTask @Inject()(models: ModelService, actorSystem: ActorSystem, conf /** * Task runner */ - def run() = { + def run(): Unit = { val actions = this.models.getSchema(classOf[ProjectSchema]) val newFilter: ModelFilter[Project] = ModelFilter[Project](_.visibility === VisibilityTypes.New) @@ -44,10 +44,10 @@ class ProjectTask @Inject()(models: ModelService, actorSystem: ActorSystem, conf val dayAgo = System.currentTimeMillis() - draftExpire projects.foreach(project => { - Logger.info(s"Found project: ${project.ownerName}/${project.slug}") + Logger.debug(s"Found project: ${project.ownerName}/${project.slug}") val createdAt = project.createdAt.getOrElse(Timestamp.from(Instant.now())).getTime if (createdAt < dayAgo) { - Logger.info(s"Changed ${project.ownerName}/${project.slug} from New to Public") + Logger.debug(s"Changed ${project.ownerName}/${project.slug} from New to Public") project.setVisibility(VisibilityTypes.Public, "Changed by task", project.ownerId) } }) diff --git a/app/ore/project/factory/PendingProject.scala b/app/ore/project/factory/PendingProject.scala index 482b99c87..9e3be0a68 100755 --- a/app/ore/project/factory/PendingProject.scala +++ b/app/ore/project/factory/PendingProject.scala @@ -37,10 +37,15 @@ case class PendingProject(projects: ProjectBase, * The first [[PendingVersion]] for this PendingProject. */ val pendingVersion: PendingVersion = { - val version = this.factory.startVersion(this.file, this.underlying, this.settings, this.channelName) - val model = version.underlying - version.cache() - version + val result = this.factory.startVersion(this.file, this.underlying, this.settings, this.channelName) + result match { + case Right (version) => + val model = version.underlying + version.cache() + version + // TODO: not this crap + case Left (errorMessage) => throw new IllegalArgumentException(errorMessage) + } } def complete()(implicit ec: ExecutionContext): Future[(Project, Version)] = { @@ -51,10 +56,8 @@ case class PendingProject(projects: ProjectBase, this.pendingVersion.project = newProject this.factory.createVersion(this.pendingVersion) } - } yield { - newProject.setRecommendedVersion(newVersion._1) - (newProject, newVersion._1) - } + _ <- newProject.setRecommendedVersion(newVersion._1) + } yield (newProject, newVersion._1) } def cancel()(implicit ec: ExecutionContext) = { diff --git a/app/ore/project/factory/PendingVersion.scala b/app/ore/project/factory/PendingVersion.scala index fc53b5567..ea28d234b 100644 --- a/app/ore/project/factory/PendingVersion.scala +++ b/app/ore/project/factory/PendingVersion.scala @@ -2,11 +2,10 @@ package ore.project.factory import db.impl.access.ProjectBase import models.project._ -import ore.Cacheable import ore.Colors.Color -import ore.project.Dependency import ore.project.factory.TagAlias.ProjectTag import ore.project.io.PluginFile +import ore.{Cacheable, Platforms} import play.api.cache.SyncCacheApi import scala.concurrent.{ExecutionContext, Future} @@ -51,17 +50,7 @@ case class PendingVersion(projects: ProjectBase, override def key: String = this.project.url + '/' + this.underlying.versionString def dependenciesAsGhostTags: Seq[Tag] = { - var ghostFlags: Seq[Tag] = Seq() - for (dependency <- this.underlying.dependencies) { - if (factory.dependencyVersionRegex.pattern.matcher(dependency.version).matches()) { - if (dependency.pluginId.equalsIgnoreCase(Dependency.SpongeApiId)) { - ghostFlags = ghostFlags :+ Tag(None, List(), "Sponge", dependency.version, TagColors.Sponge) - } - if (dependency.pluginId.equalsIgnoreCase(Dependency.ForgeId)) { - ghostFlags = ghostFlags :+ Tag(None, List(), "Forge", dependency.version, TagColors.Forge) - } - } - } - ghostFlags + Platforms.getPlatformGhostTags(this.underlying.dependencies) } + } diff --git a/app/ore/project/factory/ProjectFactory.scala b/app/ore/project/factory/ProjectFactory.scala index bf0fa1ecc..2e148e88e 100755 --- a/app/ore/project/factory/ProjectFactory.scala +++ b/app/ore/project/factory/ProjectFactory.scala @@ -8,33 +8,32 @@ import com.google.common.base.Preconditions._ import db.ModelService import db.impl.OrePostgresDriver.api._ import db.impl.access.{ProjectBase, UserBase} +import db.impl.{ProjectMembersTable, ProjectRoleTable} import discourse.OreDiscourseApi import javax.inject.Inject -import models.project.TagColors.TagColor import models.project._ import models.user.role.ProjectRole import models.user.{Notification, User} import ore.Colors.Color -import ore.OreConfig import ore.permission.role.RoleTypes -import ore.project.Dependency.{ForgeId, SpongeApiId} -import ore.project.NotifyWatchersTask import ore.project.factory.TagAlias.ProjectTag -import ore.project.io.{InvalidPluginFileException, PluginFile, PluginUpload, ProjectFiles} +import ore.project.io._ +import ore.project.{NotifyWatchersTask, ProjectMember} +import ore.user.MembershipDossier import ore.user.notification.NotificationTypes -import org.spongepowered.plugin.meta.PluginMetadata +import ore.{OreConfig, OreEnv, Platforms} 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} +import util.functional.EitherT import util.instances.future._ import util.syntax._ -import scala.collection.JavaConverters._ -import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.Duration +import scala.concurrent.{ExecutionContext, Future} import scala.util.Try +import scala.util.matching.Regex /** * Manages the project and version creation pipeline. @@ -49,15 +48,13 @@ trait ProjectFactory { val cacheApi: SyncCacheApi val actorSystem: ActorSystem val pgp: PGPVerifier = new PGPVerifier - val dependencyVersionRegex = "^[0-9a-zA-Z\\.\\,\\[\\]\\(\\)-]+$".r + val dependencyVersionRegex: Regex = "^[0-9a-zA-Z\\.\\,\\[\\]\\(\\)-]+$".r - implicit val messages: MessagesApi implicit val config: OreConfig implicit val forums: OreDiscourseApi - implicit val env = this.fileManager.env - implicit val lang = Lang.defaultLang + implicit val env: OreEnv = this.fileManager.env - var isPgpEnabled = this.config.security.get[Boolean]("requirePgp") + var isPgpEnabled: Boolean = this.config.security.get[Boolean]("requirePgp") /** * Processes incoming [[PluginUpload]] data, verifies it, and loads a new @@ -65,9 +62,9 @@ trait ProjectFactory { * * @param uploadData Upload data of request * @param owner Upload owner - * @return Loaded PluginFile + * @return Loaded PluginFile */ - def processPluginUpload(uploadData: PluginUpload, owner: User): PluginFile = { + def processPluginUpload(uploadData: PluginUpload, owner: User)(implicit messages: Messages): Either[String, PluginFile] = { val pluginFileName = uploadData.pluginFileName var signatureFileName = uploadData.signatureFileName @@ -102,32 +99,44 @@ trait ProjectFactory { // create and load a new PluginFile instance for further processing val plugin = new PluginFile(pluginPath, sigPath, owner) - plugin.loadMeta() - plugin + val result = plugin.loadMeta() + result match { + case Right(_) => Right(plugin) + case Left(errorMessage) => Left(errorMessage) + } } def processSubsequentPluginUpload(uploadData: PluginUpload, owner: User, - project: Project)(implicit ec: ExecutionContext): EitherT[Future, String, PendingVersion] = { - val plugin = this.processPluginUpload(uploadData, owner) - if (!plugin.meta.get.getId.equals(project.pluginId)) - EitherT.leftT("error.version.invalidPluginId") - else { - EitherT( - for { - (channels, settings) <- (project.channels.all, project.settings).parTupled - version = this.startVersion(plugin, project, settings, channels.head.name) - modelExists <- version.underlying.exists - } yield { - if(modelExists && this.config.projects.get[Boolean]("file-validate")) - Left("error.version.duplicate") - else { - version.cache() - Right(version) + project: Project)(implicit ec: ExecutionContext, messages: Messages): EitherT[Future, String, PendingVersion] = { + this.processPluginUpload(uploadData, owner) match { + case Right(plugin) if !plugin.data.flatMap(_.id).contains(project.pluginId) => + EitherT.leftT("error.version.invalidPluginId") + case Right(plugin) => + EitherT( + for { + (channels, settings) <- (project.channels.all, project.settings).parTupled + version = this.startVersion(plugin, project, settings, channels.head.name) + modelExists <- version match { + case Right(v) => v.underlying.exists + case Left(_) => Future.successful(false) + } + } yield { + version match { + case Right(v) => if (modelExists && this.config.projects.get[Boolean]("file-validate")) + Left("error.version.duplicate") + else { + v.cache() + Right(v) + } + case Left(m) => Left(m) + } + } - } - ) + ) + case Left(errorMessage) => EitherT.leftT[Future, PendingVersion](errorMessage) } + } /** @@ -154,7 +163,7 @@ trait ProjectFactory { * Starts the construction process of a [[Project]]. * * @param plugin First version file - * @return PendingProject instance + * @return PendingProject instance */ def startProject(plugin: PluginFile): PendingProject = { val metaData = checkMeta(plugin) @@ -162,10 +171,10 @@ trait ProjectFactory { // Start a new pending project val project = Project.Builder(this.service) - .pluginId(metaData.getId) + .pluginId(metaData.id.get) .ownerName(owner.name) .ownerId(owner.id.get) - .name(metaData.getName) + .name(metaData.get[String]("name").getOrElse("name not found")) .visibility(VisibilityTypes.New) .build() @@ -175,7 +184,7 @@ trait ProjectFactory { underlying = project, file = plugin, config = this.config, - channelName = this.config.getSuggestedNameForVersion(metaData.getVersion), + channelName = this.config.getSuggestedNameForVersion(metaData.version.get), cacheApi = this.cacheApi) pendingProject } @@ -183,23 +192,21 @@ trait ProjectFactory { /** * Starts the construction process of a [[Version]]. * - * @param plugin Plugin file - * @param project Parent project - * @return PendingVersion instance + * @param plugin Plugin file + * @param project Parent project + * @return PendingVersion instance */ - def startVersion(plugin: PluginFile, project: Project, settings: ProjectSettings, channelName: String): PendingVersion = { + def startVersion(plugin: PluginFile, project: Project, settings: ProjectSettings, channelName: String): Either[String, PendingVersion] = { val metaData = checkMeta(plugin) - if (!metaData.getId.equals(project.pluginId)) - throw InvalidPluginFileException("error.plugin.invalidPluginId") + if (!metaData.id.contains(project.pluginId)) + return Left("error.plugin.invalidPluginId") // Create new pending version - val depends = for (depend <- metaData.collectRequiredDependencies().asScala) yield - depend.getId + ":" + depend.getVersion val path = plugin.path val version = Version.Builder(this.service) - .versionString(metaData.getVersion) - .dependencyIds(depends.toList) - .description(metaData.getDescription) + .versionString(metaData.version.get) + .dependencyIds(metaData.dependencies.map(d => d.pluginId + ":" + d.version).toList) + .description(metaData.get[String]("description").getOrElse("")) .projectId(project.id.getOrElse(-1)) // Version might be for an uncreated project .fileSize(path.toFile.length) .hash(plugin.md5) @@ -208,7 +215,7 @@ trait ProjectFactory { .authorId(plugin.user.id.get) .build() - PendingVersion( + Right(PendingVersion( projects = this.projects, factory = this, project = project, @@ -218,18 +225,18 @@ trait ProjectFactory { plugin = plugin, createForumPost = settings.forumSync, cacheApi = cacheApi - ) + )) } - private def checkMeta(plugin: PluginFile): PluginMetadata - = plugin.meta.getOrElse(throw new IllegalStateException("plugin metadata not loaded?")) + private def checkMeta(plugin: PluginFile): PluginFileData + = plugin.data.getOrElse(throw new IllegalStateException("plugin metadata not loaded?")) /** * Returns the PendingProject of the specified owner and name, if any. * - * @param owner Project owner - * @param slug Project slug - * @return PendingProject if present, None otherwise + * @param owner Project owner + * @param slug Project slug + * @return PendingProject if present, None otherwise */ def getPendingProject(owner: String, slug: String): Option[PendingProject] = this.cacheApi.get[PendingProject](owner + '/' + slug) @@ -238,10 +245,10 @@ trait ProjectFactory { * Returns the pending version for the specified owner, name, channel, and * version string. * - * @param owner Name of owner - * @param slug Project slug - * @param version Name of version - * @return PendingVersion, if present, None otherwise + * @param owner Name of owner + * @param slug Project slug + * @param version Name of version + * @return PendingVersion, if present, None otherwise */ def getPendingVersion(owner: String, slug: String, version: String): Option[PendingVersion] = this.cacheApi.get[PendingVersion](owner + '/' + slug + '/' + version) @@ -249,8 +256,8 @@ trait ProjectFactory { /** * Creates a new Project from the specified PendingProject * - * @param pending PendingProject - * @return New Project + * @param pending PendingProject + * @return New Project * @throws IllegalArgumentException if the project already exists */ def createProject(pending: PendingProject)(implicit ec: ExecutionContext): Future[Project] = { @@ -270,7 +277,17 @@ trait ProjectFactory { newProject.updateSettings(pending.settings) // Invite members - val dossier = newProject.memberships + val dossier: MembershipDossier { + type MembersTable = ProjectMembersTable + + type MemberType = ProjectMember + + type RoleTable = ProjectRoleTable + + type ModelType = Project + + type RoleType = ProjectRole + } = newProject.memberships val owner = newProject.owner val ownerId = owner.userId val projectId = newProject.id.get @@ -282,7 +299,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) )) } } @@ -296,10 +313,10 @@ trait ProjectFactory { /** * Creates a new release channel for the specified [[Project]]. * - * @param project Project to create channel for - * @param name Channel name - * @param color Channel color - * @return New channel + * @param project Project to create channel for + * @param name Channel name + * @param color Channel color + * @return New channel */ def createChannel(project: Project, name: String, color: Color, nonReviewed: Boolean)(implicit ec: ExecutionContext): Future[Channel] = { checkNotNull(project, "null project", "") @@ -317,8 +334,8 @@ trait ProjectFactory { /** * Creates a new version from the specified PendingVersion. * - * @param pending PendingVersion - * @return New version + * @param pending PendingVersion + * @return New version */ def createVersion(pending: PendingVersion)(implicit ec: ExecutionContext): Future[(Version, Channel, Seq[ProjectTag])] = { val project = pending.project @@ -346,13 +363,10 @@ trait ProjectFactory { ) this.service.access[Version](classOf[Version]).add(newVersion) } - spongeTag <- addTags(newVersion, SpongeApiId, "Sponge", TagColors.Sponge).value - forgeTag <- addTags(newVersion, ForgeId, "Forge", TagColors.Forge).value + tags <- addTags(pending, newVersion) } yield { - 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) @@ -362,43 +376,42 @@ trait ProjectFactory { this.forums.postVersionRelease(project, newVersion, newVersion.description) } - (newVersion, channel, tags.toSeq) + (newVersion, channel, tags) } } - private def addTags(newVersion: Version, dependencyName: String, tagName: String, tagColor: TagColor)(implicit ec: ExecutionContext): OptionT[Future, ProjectTag] = { - val dependenciesMatchingName = newVersion.dependencies.filter(_.pluginId == dependencyName) - OptionT.fromOption[Future](dependenciesMatchingName.headOption) - .filter(dep => dependencyVersionRegex.pattern.matcher(dep.version).matches()) - .semiFlatMap { dep => - for { - tagsWithVersion <- service.access(classOf[ProjectTag]) - .filter(t => t.name === tagName && t.data === dep.version) - tag <- { - if (tagsWithVersion.isEmpty) { - val tag = Tag( - _versionIds = List(newVersion.id.get), - name = tagName, - data = dep.version, - color = tagColor - ) - val newTag = service.access(classOf[ProjectTag]).add(tag) - newTag.map(newVersion.addTag) - newTag - - } else { - val tag = tagsWithVersion.head - tag.addVersionId(newVersion.id.get) - Future.successful(tag) - } - } - } yield { - newVersion.addTag(tag) - tag - } + private def addTags(pendingVersion: PendingVersion, newVersion: Version)(implicit ec: ExecutionContext): Future[Seq[ProjectTag]] = { + for { + (metadataTags, dependencyTags) <- ( + addMetadataTags(pendingVersion.plugin.data, newVersion), + addDependencyTags(newVersion) + ).parTupled + } yield { + metadataTags ++ dependencyTags } } + private def addMetadataTags(pluginFileData: Option[PluginFileData], version: Version)(implicit ec: ExecutionContext): Future[Seq[ProjectTag]] = { + Future.sequence(pluginFileData.map(_.ghostTags.map(_.getFilledTag(service))).toList.flatten).map( + _.map { tag => + tag.addVersionId(version.id.get) + version.addTag(tag) + tag + }) + } + + private def addDependencyTags(version: Version)(implicit ec: ExecutionContext): Future[Seq[ProjectTag]] = { + Future.sequence( + Platforms.getPlatformGhostTags( + // filter valid dependency versions + version.dependencies.filter(d => dependencyVersionRegex.pattern.matcher(d.version).matches()) + ).map(_.getFilledTag(service))).map( + _.map { tag => + tag.addVersionId(version.id.get) + version.addTag(tag) + tag + }) + } private def getOrCreateChannel(pending: PendingVersion, project: Project)(implicit ec: ExecutionContext) = { project.channels.find(equalsIgnoreCase(_.name, pending.channelName)) @@ -406,8 +419,6 @@ trait ProjectFactory { } private def uploadPlugin(project: Project, channel: Channel, plugin: PluginFile, version: Version): Try[Unit] = Try { - val meta = plugin.meta.get - val oldPath = plugin.path val oldSigPath = plugin.signaturePath @@ -432,6 +443,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 + extends ProjectFactory diff --git a/app/ore/project/io/DownloadTypes.scala b/app/ore/project/io/DownloadTypes.scala index b32425b21..7b2b75da4 100644 --- a/app/ore/project/io/DownloadTypes.scala +++ b/app/ore/project/io/DownloadTypes.scala @@ -5,6 +5,9 @@ import db.table.MappedType import scala.language.implicitConversions +import slick.ast.BaseTypedType +import slick.jdbc.JdbcType + /** * Represents different kind of downloads. */ @@ -26,7 +29,7 @@ object DownloadTypes extends Enumeration { val SignatureFile = DownloadType(2) case class DownloadType(i: Int) extends super.Val(i) with MappedType[DownloadType] { - override implicit val mapper = OrePostgresDriver.api.downloadTypeTypeMapper + override implicit val mapper: JdbcType[DownloadType] with BaseTypedType[DownloadType] = OrePostgresDriver.api.downloadTypeTypeMapper } implicit def convert(v: Value): DownloadType = v.asInstanceOf[DownloadType] diff --git a/app/ore/project/io/PluginFile.scala b/app/ore/project/io/PluginFile.scala index 47d023636..64b5911f4 100755 --- a/app/ore/project/io/PluginFile.scala +++ b/app/ore/project/io/PluginFile.scala @@ -1,18 +1,18 @@ package ore.project.io -import java.io.{IOException, InputStream} +import java.io._ import java.nio.file.{Files, Path} -import java.util.jar.{JarEntry, JarInputStream} +import java.util.jar.{JarEntry, JarFile, JarInputStream} import java.util.zip.{ZipEntry, ZipFile} import com.google.common.base.Preconditions._ 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.collection.mutable.ArrayBuffer import scala.util.control.Breaks._ /** @@ -22,11 +22,7 @@ import scala.util.control.Breaks._ */ class PluginFile(private var _path: Path, val signaturePath: Path, val user: User) extends UserOwned { - implicit val lang = Lang.defaultLang - - private val MetaFileName = "mcmod.info" - - private var _meta: Option[PluginMetadata] = None + private var _data: Option[PluginFileData] = None private var _md5: String = _ /** @@ -44,7 +40,7 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use * * @param path Path to move file to */ - def move(path: Path) = { + def move(path: Path): Unit = { Files.move(this.path, path) this._path = path } @@ -52,7 +48,7 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use /** * Deletes the File at this PluginFile's Path */ - def delete() = { + def delete(): Unit = { Files.delete(this._path) this._path = null } @@ -62,7 +58,7 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use * * @return PluginMetadata if present, None otherwise */ - def meta: Option[PluginMetadata] = this._meta + def data: Option[PluginFileData] = this._data /** * Returns an MD5 hash of this PluginFile. @@ -80,58 +76,62 @@ class PluginFile(private var _path: Path, val signaturePath: Path, val user: Use * * TODO: More validation on PluginMetadata results (null checks, etc) * - * @return Result of parse + * @return Plugin metadata or an error message */ @throws[InvalidPluginFileException] - def loadMeta()(implicit messages: MessagesApi): PluginMetadata = { + def loadMeta()(implicit messages: Messages): Either[String, PluginFileData] = { + val fileNames = PluginFileData.fileNames + var jarIn: JarInputStream = null try { // Find plugin JAR jarIn = new JarInputStream(newJarStream) + var data = new ArrayBuffer[DataValue[_]]() + // Find plugin meta file - var entry: JarEntry = null - var metaFound: Boolean = false - breakable { - while ({ entry = jarIn.getNextJarEntry; entry } != null) { - if (entry.getName.equals(MetaFileName)) { - metaFound = true - break - } + var entry: JarEntry = jarIn.getNextJarEntry + while (entry != null) { + if (fileNames.contains(entry.getName)) { + data ++= PluginFileData.getData(entry.getName, new BufferedReader(new InputStreamReader(jarIn))) + } + entry = jarIn.getNextJarEntry + } + + // Mainfest file isn't read in the jar stream for whatever reason + // so we need to use the java API + if (fileNames.contains(JarFile.MANIFEST_NAME)) { + val manifest = jarIn.getManifest + if (manifest != null) { + val manifestLines = new BufferedReader(new StringReader(jarIn.getManifest.getMainAttributes.asScala + .map(p => p._1.toString + ": " + p._2.toString).mkString("\n"))) + + data ++= PluginFileData.getData(JarFile.MANIFEST_NAME, manifestLines) } } - if (!metaFound) - throw InvalidPluginFileException("error.plugin.metaNotFound") - // Read the meta file - val metaList = McModInfo.DEFAULT.read(jarIn).asScala.toList - if (metaList.isEmpty) - throw InvalidPluginFileException("error.plugin.metaNotFound") + // This won't be called if a plugin uses mixins but doesn't + // have a mcmod.info, but the check below will catch that + if (data.isEmpty) + return Left(messages("error.plugin.metaNotFound")) - // Parse plugin meta info - val meta = metaList.head + val fileData = new PluginFileData(data) - // check meta - def checkMeta(value: Any, field: String) = { - if (value == null) - throw InvalidPluginFileException(messages("error.plugin.incomplete", field)) + this._data = Some(fileData) + if(!fileData.isValidPlugin) { + return Left(messages("error.plugin.incomplete", "id or version")) } - checkMeta(meta.getName, "name") - checkMeta(meta.getVersion, "version") - this._meta = Some(meta) - meta + Right(fileData) } catch { - case pe: InvalidPluginFileException => - throw pe case e: Exception => - throw InvalidPluginFileException(cause = e) + Left(e.getMessage) } finally { if (jarIn != null) jarIn.close() else - throw InvalidPluginFileException("error.plugin.unexpected") + return Left(messages("error.plugin.unexpected")) } } @@ -141,7 +141,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 +150,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 +168,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/ore/project/io/PluginFileData.scala b/app/ore/project/io/PluginFileData.scala new file mode 100644 index 000000000..d0f401905 --- /dev/null +++ b/app/ore/project/io/PluginFileData.scala @@ -0,0 +1,182 @@ +package ore.project.io + +import java.io.BufferedReader + +import models.project.{Tag, TagColors} +import ore.project.Dependency +import org.spongepowered.plugin.meta.McModInfo + +import scala.collection.JavaConverters._ +import scala.collection.mutable.ArrayBuffer + +/** + * The metadata within a [[PluginFile]] + * + * @author phase + * @param data the data within a [[PluginFile]] + */ +class PluginFileData(data: Seq[DataValue[_]]) { + + val dataValues = data.groupBy(_.key).flatMap { case (key, value) => + // combine dependency lists that may come from different files + if (value.size > 1 && value.head.isInstanceOf[DependencyDataValue]) { + val dependencies = value.flatMap(_.value.asInstanceOf[Seq[Dependency]]) + Seq(DependencyDataValue(key, dependencies)) + } else value + }.toSeq + + def id: Option[String] = { + get[String]("id") + } + + def version: Option[String] = { + get[String]("version") + } + + def authors: Seq[String] = { + get[Seq[String]]("authors").getOrElse(Seq()) + } + + def dependencies: Seq[Dependency] = { + get[Seq[Dependency]]("dependencies").getOrElse(Seq()) + } + + def get[T](key: String): Option[T] = { + dataValues.filter(_.key == key).filter(_.isInstanceOf[DataValue[T]]).map(_.asInstanceOf[DataValue[T]].value).headOption + } + + def isValidPlugin: Boolean = { + dataValues.exists(_.isInstanceOf[StringDataValue]) && + dataValues.exists(_.isInstanceOf[StringDataValue]) + } + + def ghostTags: Seq[Tag] = { + val buffer = new ArrayBuffer[Tag] + + if (containsMixins) { + val mixinTag = Tag(None, List(), "Mixin", "", TagColors.Mixin) + buffer += mixinTag + } + + println("PluginFileData#getGhostTags: " + buffer) + buffer + } + + /** + * A mod using Mixins will contain the "MixinConfigs" attribute in their MANIFEST + * + * @return + */ + def containsMixins: Boolean = { + dataValues.exists(p => p.key == "MixinConfigs" && p.isInstanceOf[StringDataValue]) + } + +} + +object PluginFileData { + val fileTypes: Seq[FileTypeHandler] = Seq(McModInfoHandler, ManifestHandler, ModTomlHandler) + + def fileNames: Seq[String] = fileTypes.map(_.fileName).distinct + + def getData(fileName: String, stream: BufferedReader): Seq[DataValue[_]] = { + fileTypes.filter(_.fileName == fileName).flatMap(_.getData(stream)) + } + +} + +/** + * A data element in a data file + * + * @param key the key for the value + * @param value the value extracted from the file + * @tparam T the type of the value + */ +sealed trait DataValue[T] { + val key: String + val value: T +} + +/** + * A data element that is a String, such as the plugin id or version + * + * @param value the value extracted from the file + */ +case class StringDataValue(key: String, value: String) + extends DataValue[String] + +/** + * A data element that is a list of strings, such as an authors list + * + * @param value the value extracted from the file + */ +case class StringListValue(key: String, value: Seq[String]) + extends DataValue[Seq[String]] + +/** + * A data element that is a list of [[Dependency]] + * + * @param value the value extracted from the file + */ +case class DependencyDataValue(key: String, value: Seq[Dependency]) + extends DataValue[Seq[Dependency]] + +sealed abstract case class FileTypeHandler(fileName: String) { + def getData(bufferedReader: BufferedReader): Seq[DataValue[_]] +} + +object McModInfoHandler extends FileTypeHandler("mcmod.info") { + override def getData(bufferedReader: BufferedReader): Seq[DataValue[_]] = { + val dataValues = new ArrayBuffer[DataValue[_]] + try { + val info = McModInfo.DEFAULT.read(bufferedReader).asScala + if (info.size < 1) return Seq() + + val metadata = info.head + + if (metadata.getId != null) + dataValues += StringDataValue("id", metadata.getId) + if (metadata.getVersion != null) + dataValues += StringDataValue("version", metadata.getVersion) + if (metadata.getName != null) + dataValues += StringDataValue("name", metadata.getName) + if (metadata.getDescription != null) + dataValues += StringDataValue("description", metadata.getDescription) + if (metadata.getUrl != null) + dataValues += StringDataValue("url", metadata.getUrl) + if (metadata.getAuthors != null) + dataValues += StringListValue("authors", metadata.getAuthors.asScala) + if (metadata.getDependencies != null) { + val dependencies = metadata.getDependencies.asScala.map(p => Dependency(p.getId, p.getVersion)).toSeq + dataValues += DependencyDataValue("dependencies", dependencies) + } + } catch { + case e: Exception => e.printStackTrace() + } + + dataValues + } +} + +object ManifestHandler extends FileTypeHandler("META-INF/MANIFEST.MF") { + override def getData(bufferedReader: BufferedReader): Seq[DataValue[_]] = { + val dataValues = new ArrayBuffer[DataValue[_]] + + val lines = Stream.continually(bufferedReader.readLine()).takeWhile(_ != null) + for (line <- lines) { + // Check for Mixins + if (line.startsWith("MixinConfigs: ")) { + val mixinConfigs = line.split(": ")(1) + dataValues += StringDataValue("MixinConfigs", mixinConfigs) + } + } + + dataValues + } +} + +object ModTomlHandler extends FileTypeHandler("mod.toml") { + override def getData(bufferedReader: BufferedReader): Seq[DataValue[_]] = { + // TODO: Get format from Forge once it has been decided on + Seq() + } +} diff --git a/app/ore/rest/OreRestfulApi.scala b/app/ore/rest/OreRestfulApi.scala index 8225fdd8a..3579618fa 100755 --- a/app/ore/rest/OreRestfulApi.scala +++ b/app/ore/rest/OreRestfulApi.scala @@ -98,7 +98,7 @@ trait OreRestfulApi { } } - def writeMembers(members: Seq[(ProjectRole, User)]) = { + def writeMembers(members: Seq[(ProjectRole, User)]): Seq[JsObject] = { val allRoles = members.groupBy(_._1.userId).mapValues(_.map(_._1.roleType)) members.map { case (_, user) => val roles = allRoles(user.id.get) @@ -160,7 +160,14 @@ trait OreRestfulApi { "tags" -> tags.map(toJson(_)), "downloads" -> v.downloadCount ) - author.fold(json)(a => json + (("author", JsString(a)))) + + lazy val jsonVisibility = obj( + "type" -> v.visibility.nameKey, + "css" -> v.visibility.cssClass + ) + + val withVisibility = if(v.visibility == VisibilityTypes.Public) json else json + ("visibility" -> jsonVisibility) + author.fold(withVisibility)(a => withVisibility + (("author", JsString(a)))) } private def queryProjectChannels(projectIds: Seq[Int]) = { @@ -176,7 +183,7 @@ trait OreRestfulApi { val tableTags = TableQuery[TagTable] val tableVersion = TableQuery[VersionTable] for { - v <- tableVersion if v.id inSetBind versions + v <- tableVersion if (v.id inSetBind versions) && v.visibility === VisibilityTypes.Public t <- tableTags if t.id === v.tagIds.any } yield { (v.id, t) @@ -224,14 +231,13 @@ trait OreRestfulApi { * @return JSON list of versions */ def getVersionList(pluginId: String, channels: Option[String], - limit: Option[Int], offset: Option[Int])(implicit ec: ExecutionContext): Future[JsValue] = { - + limit: Option[Int], offset: Option[Int], onlyPublic: Boolean)(implicit ec: ExecutionContext): Future[JsValue] = { val filtered = channels.map { chan => - queryVersions.filter { case (p, v, vId, c, uName) => + queryVersions(onlyPublic).filter { case (p, v, vId, c, uName) => // Only allow versions in the specified channels or all if none specified c.name.toLowerCase inSetBind chan.toLowerCase.split(",") } - } getOrElse queryVersions filter { + } getOrElse queryVersions(onlyPublic) filter { case (p, v, vId, c, uName) => p.pluginId.toLowerCase === pluginId.toLowerCase } sortBy { case (_, v, _, _, _) => @@ -263,7 +269,7 @@ trait OreRestfulApi { */ def getVersion(pluginId: String, name: String)(implicit ec: ExecutionContext): Future[Option[JsValue]] = { - val filtered = queryVersions.filter { case (p, v, vId, c, uName) => + val filtered = queryVersions().filter { case (p, v, vId, c, uName) => p.pluginId.toLowerCase === pluginId.toLowerCase && v.versionString.toLowerCase === name.toLowerCase } @@ -279,7 +285,7 @@ trait OreRestfulApi { } - private def queryVersions: Query[(ProjectTableMain, VersionTable, Rep[Int], ChannelTable, Rep[Option[String]]), (Project, Version, Int, Channel, Option[String]), Seq] = { + private def queryVersions(onlyPublic: Boolean = true): Query[(ProjectTableMain, VersionTable, Rep[Int], ChannelTable, Rep[Option[String]]), (Project, Version, Int, Channel, Option[String]), Seq] = { val tableProject = TableQuery[ProjectTableMain] val tableVersion = TableQuery[VersionTable] val tableChannels = TableQuery[ChannelTable] @@ -288,7 +294,7 @@ trait OreRestfulApi { for { p <- tableProject (v, u) <- tableVersion joinLeft tableUsers on (_.authorId === _.id) - c <- tableChannels if v.channelId === c.id && p.id === v.projectId + c <- tableChannels if v.channelId === c.id && p.id === v.projectId && (if(onlyPublic) v.visibility === VisibilityTypes.Public else true) } yield { (p, v, v.id, c, u.map(_.name)) } @@ -401,7 +407,7 @@ trait OreRestfulApi { */ def getTags(pluginId: String, version: String)(implicit ec: ExecutionContext): OptionT[Future, JsValue] = { this.projects.withPluginId(pluginId).flatMap { project => - project.versions.find(equalsIgnoreCase(_.versionString, version)).semiFlatMap { v => + project.versions.find(v => v.versionString.toLowerCase === version.toLowerCase && v.visibility === VisibilityTypes.Public).semiFlatMap { v => v.tags.map { tags => obj( "pluginId" -> pluginId, diff --git a/app/ore/rest/OreWrites.scala b/app/ore/rest/OreWrites.scala index a7d758f0d..09c05f942 100644 --- a/app/ore/rest/OreWrites.scala +++ b/app/ore/rest/OreWrites.scala @@ -17,8 +17,8 @@ final class OreWrites @Inject()(implicit config: OreConfig, service: ModelServic implicit val projects: ProjectBase = this.service.getModelBase(classOf[ProjectBase]) - implicit val projectApiKeyWrites = new Writes[ProjectApiKey] { - def writes(key: ProjectApiKey) = obj( + implicit val projectApiKeyWrites: Writes[ProjectApiKey] = new Writes[ProjectApiKey] { + def writes(key: ProjectApiKey): JsObject = obj( "id" -> key.id.get, "createdAt" -> key.createdAt.get, "keyType" -> obj("id" -> key.keyType.id, "name" -> key.keyType.name), @@ -27,8 +27,8 @@ final class OreWrites @Inject()(implicit config: OreConfig, service: ModelServic ) } - implicit val pageWrites = new Writes[Page] { - def writes(page: Page) = obj( + implicit val pageWrites: Writes[Page] = new Writes[Page] { + def writes(page: Page): JsObject = obj( "id" -> page.id.get, "createdAt" -> page.createdAt.get.toString, "parentId" -> page.parentId, @@ -37,8 +37,8 @@ final class OreWrites @Inject()(implicit config: OreConfig, service: ModelServic ) } - implicit val channelWrites = new Writes[Channel] { - def writes(channel: Channel) = obj("name" -> channel.name, "color" -> channel.color.hex, "nonReviewed" -> channel.isNonReviewed) + implicit val channelWrites: Writes[Channel] = new Writes[Channel] { + def writes(channel: Channel): JsObject = obj("name" -> channel.name, "color" -> channel.color.hex, "nonReviewed" -> channel.isNonReviewed) } /* @@ -54,7 +54,7 @@ final class OreWrites @Inject()(implicit config: OreConfig, service: ModelServic } */ - implicit val tagWrites = new Writes[Tag] { + implicit val tagWrites: Writes[Tag] = new Writes[Tag] { override def writes(tag: Tag): JsValue = { obj( "id" -> tag.id, @@ -66,7 +66,7 @@ final class OreWrites @Inject()(implicit config: OreConfig, service: ModelServic } } - implicit val tagColorWrites = new Writes[TagColors.TagColor] { + implicit val tagColorWrites: Writes[TagColors.TagColor] = new Writes[TagColors.TagColor] { override def writes(tagColor: TagColors.TagColor): JsValue = { obj( "id" -> tagColor.id, @@ -146,8 +146,8 @@ final class OreWrites @Inject()(implicit config: OreConfig, service: ModelServic } */ - implicit val pgpPublicKeyInfoWrites = new Writes[PGPPublicKeyInfo] { - def writes(key: PGPPublicKeyInfo) = { + implicit val pgpPublicKeyInfoWrites: Writes[PGPPublicKeyInfo] = new Writes[PGPPublicKeyInfo] { + def writes(key: PGPPublicKeyInfo): JsObject = { obj( "raw" -> key.raw, "userName" -> key.userName, diff --git a/app/ore/rest/ProjectApiKeyTypes.scala b/app/ore/rest/ProjectApiKeyTypes.scala index 60d55fb81..fa9cf1716 100644 --- a/app/ore/rest/ProjectApiKeyTypes.scala +++ b/app/ore/rest/ProjectApiKeyTypes.scala @@ -5,12 +5,15 @@ import db.table.MappedType import scala.language.implicitConversions +import slick.ast.BaseTypedType +import slick.jdbc.JdbcType + object ProjectApiKeyTypes extends Enumeration { val Deployment = ProjectApiKeyType(0, "deployment") case class ProjectApiKeyType(i: Int, name: String) extends super.Val(i, name) with MappedType[ProjectApiKeyType] { - implicit val mapper = OrePostgresDriver.api.projectApiKeyTypeTypeMapper + implicit val mapper: JdbcType[ProjectApiKeyType] with BaseTypedType[ProjectApiKeyType] = OrePostgresDriver.api.projectApiKeyTypeTypeMapper } implicit def convert(value: Value): ProjectApiKeyType = value.asInstanceOf[ProjectApiKeyType] diff --git a/app/ore/user/MembershipDossier.scala b/app/ore/user/MembershipDossier.scala index 4c9c67337..c005c73a0 100644 --- a/app/ore/user/MembershipDossier.scala +++ b/app/ore/user/MembershipDossier.scala @@ -69,7 +69,7 @@ trait MembershipDossier { * * @param role Role to add */ - def addRole(role: RoleType)(implicit ec: ExecutionContext) = { + def addRole(role: RoleType)(implicit ec: ExecutionContext): Future[RoleType] = { for { user <- role.user exists <- this.roles.exists(_.userId === user.id.get) @@ -99,7 +99,7 @@ trait MembershipDossier { * * @param role Role to remove */ - def removeRole(role: RoleType)(implicit ec: ExecutionContext) = { + def removeRole(role: RoleType)(implicit ec: ExecutionContext): Future[Unit] = { for { _ <- this.roleAccess.remove(role) user <- role.user @@ -114,7 +114,7 @@ trait MembershipDossier { * @param user User to remove * @return */ - def removeMember(user: User)(implicit ec: ExecutionContext) = { + def removeMember(user: User)(implicit ec: ExecutionContext): Future[Int] = { clearRoles(user) flatMap { _ => this.association.remove(user) } diff --git a/app/ore/user/UserSyncTask.scala b/app/ore/user/UserSyncTask.scala deleted file mode 100644 index d748b4e3f..000000000 --- a/app/ore/user/UserSyncTask.scala +++ /dev/null @@ -1,48 +0,0 @@ -package ore.user - -import akka.actor.ActorSystem -import db.ModelService -import db.impl.access.UserBase -import javax.inject.{Inject, Singleton} -import ore.OreConfig - -import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.duration._ - -/** - * Task that is responsible for keeping Ore users synchronized with external - * site data. - * - * @param models ModelService instance - */ -@Singleton -final class UserSyncTask @Inject()(models: ModelService, actorSystem: ActorSystem, config: OreConfig)(implicit ec: ExecutionContext) extends Runnable { - - val Logger = play.api.Logger("UserSync") - val interval = this.config.users.get[Long]("syncRate").millis - - /** - * Starts the task. - */ - def start() = { - this.actorSystem.scheduler.schedule(this.interval, this.interval, this) - Logger.info(s"Initialized. First run in ${this.interval.toString}.") - } - - /** - * Synchronizes all users with external site data. - */ - def run() = { - this.models.getModelBase(classOf[UserBase]).all.map { users => - Logger.info(s"Synchronizing ${users.size} users with external site data...") - Future.sequence(users.map { user => - user.pullForumData() - user.pullSpongeData() - }).map { _ => - Logger.info("Done") - } - } - - } - -} diff --git a/app/security/pgp/PGPPublicKeyInfo.scala b/app/security/pgp/PGPPublicKeyInfo.scala index 4bbb9ecbc..aaa27b162 100644 --- a/app/security/pgp/PGPPublicKeyInfo.scala +++ b/app/security/pgp/PGPPublicKeyInfo.scala @@ -36,7 +36,7 @@ object PGPPublicKeyInfo { * @return [[PGPPublicKeyInfo]] instance */ def decode(raw: String): Option[PGPPublicKeyInfo] = { - Logger.info(s"Decoding public key:\n$raw\n") + Logger.debug(s"Decoding public key:\n$raw\n") var in: InputStream = null try { in = PGPUtil.getDecoderStream(new ByteArrayInputStream(raw.getBytes)) @@ -46,7 +46,7 @@ object PGPPublicKeyInfo { var masterKey: PGPPublicKeyInfo = null while (keyRingIter.hasNext) { keyRingNum += 1 - Logger.info("Key ring: " + keyRingNum) + Logger.debug("Key ring: " + keyRingNum) val keyRing = keyRingIter.next() val keyIter = keyRing.iterator() var keyNum = 0 @@ -64,32 +64,32 @@ object PGPPublicKeyInfo { else None - Logger.info("Key: " + keyNum) - Logger.info("ID: " + hexId) - Logger.info("Created at: " + createdAt) - Logger.info("Revoked: " + isRevoked) - Logger.info("Encryption: " + isEncryption) - Logger.info("Master: " + isMaster) - Logger.info("Expiration: " + expirationDate.getOrElse("None")) + Logger.debug("Key: " + keyNum) + Logger.debug("ID: " + hexId) + Logger.debug("Created at: " + createdAt) + Logger.debug("Revoked: " + isRevoked) + Logger.debug("Encryption: " + isEncryption) + Logger.debug("Master: " + isMaster) + Logger.debug("Expiration: " + expirationDate.getOrElse("None")) - Logger.info("Users:") + Logger.debug("Users:") val userIter = key.getUserIDs var firstUser: String = null var userNum = 0 while (userIter.hasNext) { val user = userIter.next() - Logger.info("\t" + user) + Logger.debug("\t" + user) if (userNum == 0) firstUser = user.toString userNum += 1 } - Logger.info("Signatures:") + Logger.debug("Signatures:") val sigIter = key.getSignatures while (sigIter.hasNext) { val sig: PGPSignature = sigIter.next().asInstanceOf[PGPSignature] - Logger.info("\tCreated at: " + sig.getCreationTime) - Logger.info("\tCertification: " + sig.isCertification) + Logger.debug("\tCreated at: " + sig.getCreationTime) + Logger.debug("\tCertification: " + sig.isCertification) } if (isMaster) { @@ -100,8 +100,8 @@ object PGPPublicKeyInfo { val userName = firstUser.substring(0, emailIndexStart).trim() val email = firstUser.substring(emailIndexStart + 1, emailIndexEnd) - Logger.info("User name: " + userName) - Logger.info("Email: " + email) + Logger.debug("User name: " + userName) + Logger.debug("Email: " + email) if (isRevoked) throw new IllegalStateException("Key is revoked?") diff --git a/app/security/pgp/PGPVerifier.scala b/app/security/pgp/PGPVerifier.scala index 312e339f5..1265c87c9 100644 --- a/app/security/pgp/PGPVerifier.scala +++ b/app/security/pgp/PGPVerifier.scala @@ -12,12 +12,14 @@ import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProv import scala.util.Try +import play.api.Logger + /** * Verifies data within the PGP ecosystem. */ class PGPVerifier { - val Logger = PGPPublicKeyInfo.Logger + val Logger: Logger = PGPPublicKeyInfo.Logger /** * Verifies the specified document [[InputStream]] against the specified @@ -29,7 +31,7 @@ class PGPVerifier { * @return True if verified, false otherwise */ def verifyDetachedSignature(doc: Array[Byte], sigIn: InputStream, keyIn: InputStream): Boolean = { - Logger.info("Processing signature...") + Logger.debug("Processing signature...") var result = false try { val in = PGPUtil.getDecoderStream(sigIn) @@ -40,7 +42,7 @@ class PGPVerifier { var currentObject = getNextObject if (currentObject == null) { - Logger.info(" No PGP data found.") + Logger.debug(" No PGP data found.") return false } @@ -48,19 +50,19 @@ class PGPVerifier { currentObject match { case signatureList: PGPSignatureList => if (signatureList.isEmpty) { - Logger.info(" Empty signature list.") + Logger.debug(" Empty signature list.") return false } sigList = signatureList case e => - Logger.info("Unknown packet : " + e.getClass) + Logger.debug("Unknown packet : " + e.getClass) } - Logger.info("Processed packet : " + currentObject.toString) + Logger.debug("Processed packet : " + currentObject.toString) currentObject = getNextObject } if (sigList == null) { - Logger.info(" No signature found.") + Logger.debug(" No signature found.") return false } @@ -68,7 +70,7 @@ class PGPVerifier { val keyRings = new JcaPGPPublicKeyRingCollection(keyIn) val pubKey = keyRings.getPublicKey(sig.getKeyID) if (pubKey == null) { - Logger.info(" Invalid signature for public key.") + Logger.debug(" Invalid signature for public key.") return false } @@ -84,7 +86,7 @@ class PGPVerifier { keyIn.close() } - Logger.info(if (result) "" else "") + Logger.debug(if (result) "" else "") result } @@ -148,7 +150,7 @@ class PGPVerifier { factory = new JcaPGPObjectFactory(compressedData.getDataStream) case onePassSigList: PGPOnePassSignatureList => if (onePassSigList.isEmpty) { - Logger.info("Empty one pass signature list.") + Logger.debug("Empty one pass signature list.") return false } sig = onePassSigList.get(0) @@ -158,24 +160,24 @@ class PGPVerifier { dataIn.close() case signatureList: PGPSignatureList => if (signatureList.isEmpty) { - Logger.info("Empty signature list.") + Logger.debug("Empty signature list.") return false } sigList = signatureList case _ => } - Logger.info("Processed packet: " + currentObject.toString) + Logger.debug("Processed packet: " + currentObject.toString) currentObject = doNextObject() } in.close() if (sig == null || data == null || sigList == null) { - Logger.info("Incomplete packet data.") + Logger.debug("Incomplete packet data.") return false } - Logger.info("Signature Key ID: " + java.lang.Long.toHexString(sig.getKeyID)) + Logger.debug("Signature Key ID: " + java.lang.Long.toHexString(sig.getKeyID)) // Verify against public key val keyRings = new JcaPGPPublicKeyRingCollection(keyIn) @@ -187,7 +189,7 @@ class PGPVerifier { sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), pubKey) sig.update(data) val result = sig.verify(sigList.get(0)) - Logger.info("Verified: " + result) + Logger.debug("Verified: " + result) if (result) { out.write(data) out.close() diff --git a/app/security/spauth/SingleSignOnConsumer.scala b/app/security/spauth/SingleSignOnConsumer.scala index 6fbbd16e1..404916fb2 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. @@ -82,7 +87,7 @@ trait SingleSignOnConsumer { * @param baseUrl Base URL * @return New payload */ - def generatePayload(returnUrl: String, baseUrl: String, nonce: String) = { + def generatePayload(returnUrl: String, baseUrl: String, nonce: String): String = { val payload = "return_sso_url=" + returnUrl + "&nonce=" + nonce new String(Base64.getEncoder.encode(payload.getBytes(this.CharEncoding))) } @@ -93,7 +98,7 @@ trait SingleSignOnConsumer { * @param payload Payload to sign * @return Signature of payload */ - def generateSignature(payload: String) = hmac_sha256(payload.getBytes(this.CharEncoding)) + def generateSignature(payload: String): String = hmac_sha256(payload.getBytes(this.CharEncoding)) /** * Validates an incoming payload and extracts user information. The @@ -107,54 +112,40 @@ trait SingleSignOnConsumer { * @return [[SpongeUser]] if successful */ def authenticate(payload: String, sig: String)(isNonceValid: String => Future[Boolean])(implicit ec: ExecutionContext): OptionT[Future, SpongeUser] = { - Logger.info("Authenticating SSO payload...") - Logger.info(payload) - Logger.info("Signed with : " + sig) + Logger.debug("Authenticating SSO payload...") + Logger.debug(payload) + Logger.debug("Signed with : " + sig) if (!hmac_sha256(payload.getBytes(this.CharEncoding)).equals(sig)) { - Logger.info(" Could not verify payload against signature.") + Logger.debug(" Could not verify payload against signature.") return OptionT.none[Future, SpongeUser] } // decode payload - val decoded = URLDecoder.decode(new String(Base64.getMimeDecoder.decode(payload)), this.CharEncoding) - Logger.info("Decoded payload:") - Logger.info(decoded) + val query = Uri.Query(Base64.getMimeDecoder.decode(payload)) + Logger.debug("Decoded payload:") + Logger.debug(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.debug(" Invalid nonce.") + None + case (true, user) => + Logger.debug(" " + user) + Some(user) + } } private def hmac_sha256(data: Array[Byte]): String = { @@ -178,10 +169,10 @@ class SpongeSingleSignOnConsumer @Inject()(override val ws: WSClient, config: Co private val conf = this.config.get[Configuration]("security") - override val loginUrl = this.conf.get[String]("sso.loginUrl") - override val signupUrl = this.conf.get[String]("sso.signupUrl") - override val verifyUrl = this.conf.get[String]("sso.verifyUrl") - override val secret = this.conf.get[String]("sso.secret") - override val timeout = this.conf.get[FiniteDuration]("sso.timeout") + override val loginUrl: String = this.conf.get[String]("sso.loginUrl") + override val signupUrl: String = this.conf.get[String]("sso.signupUrl") + override val verifyUrl: String = this.conf.get[String]("sso.verifyUrl") + override val secret: String = this.conf.get[String]("sso.secret") + override val timeout: FiniteDuration = this.conf.get[FiniteDuration]("sso.timeout") } diff --git a/app/security/spauth/SpongeAuthApi.scala b/app/security/spauth/SpongeAuthApi.scala index caf2e996f..6ce91eb15 100644 --- a/app/security/spauth/SpongeAuthApi.scala +++ b/app/security/spauth/SpongeAuthApi.scala @@ -16,6 +16,9 @@ import play.api.libs.ws.{WSClient, WSResponse} import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ +import play.api.Configuration +import play.api.i18n.Lang + /** * Interfaces with the SpongeAuth Web API */ @@ -34,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 _) /** @@ -129,10 +133,10 @@ trait SpongeAuthApi { final class SpongeAuth @Inject()(config: OreConfig, override val ws: WSClient) extends SpongeAuthApi { - val conf = this.config.security + val conf: Configuration = this.config.security - override val url = this.conf.get[String]("api.url") - override val apiKey = this.conf.get[String]("api.key") - override val timeout = this.conf.get[FiniteDuration]("api.timeout") + override val url: String = this.conf.get[String]("api.url") + override val apiKey: String = this.conf.get[String]("api.key") + override val timeout: FiniteDuration = this.conf.get[FiniteDuration]("api.timeout") } 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/FileUtils.scala b/app/util/FileUtils.scala index 24e6499cb..45184cbd5 100644 --- a/app/util/FileUtils.scala +++ b/app/util/FileUtils.scala @@ -33,7 +33,7 @@ object FileUtils { if (Files.exists(dir)) { Files.walkFileTree(dir, DeleteFileVisitor) } else { - Logger.info(s"Tried to remove directory that doesn't exist: $dir") + Logger.debug(s"Tried to remove directory that doesn't exist: $dir") } } @@ -46,7 +46,7 @@ object FileUtils { if (Files.exists(dir)) { Files.walkFileTree(dir, new CleanFileVisitor(dir)) } else { - Logger.info(s"Tried to remove directory that doesn't exist: $dir") + Logger.debug(s"Tried to remove directory that doesn't exist: $dir") } } @@ -60,7 +60,7 @@ object FileUtils { if (Files.exists(file)) { Files.delete(file) } else { - Logger.info(s"Tried to remove file that doesn't exist: $file") + Logger.debug(s"Tried to remove file that doesn't exist: $file") } FileVisitResult.CONTINUE } @@ -69,7 +69,7 @@ object FileUtils { if (Files.exists(dir)) { Files.delete(dir) } else { - Logger.info(s"Tried to remove directory that doesn't exist: $dir") + Logger.debug(s"Tried to remove directory that doesn't exist: $dir") } FileVisitResult.CONTINUE } 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/bootstrap/footer.scala.html b/app/views/bootstrap/footer.scala.html index c2512d0ea..eb0ec0b88 100644 --- a/app/views/bootstrap/footer.scala.html +++ b/app/views/bootstrap/footer.scala.html @@ -28,7 +28,7 @@
Help Needed?
Our Sponsors
diff --git a/app/views/bootstrap/header.scala.html b/app/views/bootstrap/header.scala.html index c407eb714..0b6d426bc 100755 --- a/app/views/bootstrap/header.scala.html +++ b/app/views/bootstrap/header.scala.html @@ -16,11 +16,12 @@
- +
diff --git a/app/views/home.scala.html b/app/views/home.scala.html index 738978925..e38d88205 100755 --- a/app/views/home.scala.html +++ b/app/views/home.scala.html @@ -16,8 +16,9 @@ @import views.html.utils.alert @import scala.util.Random +@import ore.PlatformCategory @(models: Seq[(Project, User, Version, Seq[Tag])], visibleCategories: Option[Seq[Category]], query: Option[String], page: Int, - sort: ProjectSortingStrategy, platform: Option[Platform])(implicit messages: Messages, flash: Flash, + sort: ProjectSortingStrategy, platformCategory: Option[PlatformCategory], platform: Option[Platform])(implicit messages: Messages, flash: Flash, request: OreRequest[_], config: OreConfig) @projectRoutes = @{controllers.project.routes.Projects} @@ -42,19 +43,6 @@ } } -@platformOption = @{ - platform.map(_.name) -} - -@maybeActive(pform: Platform) = @{ - platform.map { p => - if (p.equals(pform)) - "active" - else - "" - } -} - @bootstrap.layout(messages("general.title")) { @@ -133,7 +121,8 @@ page = page, pageSize = config.projects.get[Int]("init-load"), call = page => routes.Application.showHome( - categoryString, query, orderingOption, Some(page), platformOption) + categoryString, query, orderingOption, Some(page), + platformCategory.map(_.name) ,platform.map(_.name)) )
@@ -152,7 +141,10 @@

@messages("project.category.plural")

@if(visibleCategories.isDefined) { - + } @@ -173,7 +165,7 @@

@messages("project.category.plural")

- +
+
diff --git a/app/views/projects/channels/helper/modalManage.scala.html b/app/views/projects/channels/helper/modalManage.scala.html index 26c40824b..827263d73 100755 --- a/app/views/projects/channels/helper/modalManage.scala.html +++ b/app/views/projects/channels/helper/modalManage.scala.html @@ -11,7 +11,7 @@ - @form(action = routes.Application.showHome(None, None, None, None, None)) { + @form(action = routes.Application.showHome(None, None, None, None, None, None)) { @CSRF.formField diff --git a/app/views/projects/log.scala.html b/app/views/projects/log.scala.html index fb7806ad7..d86ce0ed6 100644 --- a/app/views/projects/log.scala.html +++ b/app/views/projects/log.scala.html @@ -3,60 +3,18 @@ @import controllers.sugar.Requests.OreRequest @import db.ModelService -@import models.admin.{ProjectLogEntry, VisibilityChange} +@import models.admin.{ProjectLogEntry, ProjectVisibilityChange} @import models.project.{Project, VisibilityTypes} @import models.user.User @import ore.OreConfig @import util.StringUtils._ -@(project: Project, visibilityChanges: Seq[(VisibilityChange, Option[User])], +@(project: Project, visibilityChanges: Seq[(ProjectVisibilityChange, Option[User])], logs: Seq[ProjectLogEntry])(implicit messages: Messages, request: OreRequest[_], config: OreConfig) @projectRoutes = @{controllers.project.routes.Projects} @bootstrap.layout(messages("project.log.logger.title", project.namespace)) {
-
-
-

@messages("project.log.visibility.title") @project.ownerName/@project.slug

-
-
-
-
-
-
-

@messages("project.log.visibility.title")

-
-
- - - - - - - - - - - @if(visibilityChanges.isEmpty) { - - } - @visibilityChanges.reverse.map { case (entry, createdBy) => - - - - - @if(createdBy.isDefined) { - - } else { - - } - - } - -
StateTimeCommentSet by
No entries founds
@VisibilityTypes.withId(entry.visibility)@prettifyDateAndTime(entry.createdAt.getOrElse(Timestamp.from(Instant.now())))@entry.renderComment()@createdBy.get.nameUnknown
-
-
-
diff --git a/app/views/projects/pages/view.scala.html b/app/views/projects/pages/view.scala.html index d805dd0f0..c0a1dd1f2 100755 --- a/app/views/projects/pages/view.scala.html +++ b/app/views/projects/pages/view.scala.html @@ -67,7 +67,7 @@ @p.settings.licenseName.map { licenseName =>

@Html(messages("project.license.link")) - @licenseName + @licenseName

}
diff --git a/app/views/projects/settings.scala.html b/app/views/projects/settings.scala.html index c7e910839..37d7a0575 100755 --- a/app/views/projects/settings.scala.html +++ b/app/views/projects/settings.scala.html @@ -89,7 +89,7 @@

Description

} -
+
@CSRF.formField
@@ -262,7 +262,12 @@ @form(action = projectRoutes.softDelete(p.project.ownerName, p.project.slug)) { - @if(sp.perms(EditVersions) && v.p.versions != 1) { + @if(sp.perms(UploadVersions) && v.p.publicVersions != 1) {