From 1aa85790251c0b3f621ccfca1a6aa9e0ed1778ec Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 5 Sep 2021 14:10:31 +0200 Subject: [PATCH] protect blog post creation with delay and captcha to reduce the abuse a little --- app/controllers/Ublog.scala | 19 ++++++++++++--- app/views/base/captcha.scala | 5 ++++ app/views/site/message.scala | 3 +++ app/views/ublog/form.scala | 15 ++++++++---- modules/ublog/src/main/Env.scala | 3 ++- modules/ublog/src/main/UblogApi.scala | 5 ++++ modules/ublog/src/main/UblogForm.scala | 33 +++++++++++++++++++++----- ui/site/css/build/_ublog.scss | 1 + 8 files changed, 69 insertions(+), 15 deletions(-) diff --git a/app/controllers/Ublog.scala b/app/controllers/Ublog.scala index 9125e7348e..5b0214f685 100644 --- a/app/controllers/Ublog.scala +++ b/app/controllers/Ublog.scala @@ -54,8 +54,18 @@ final class Ublog(env: Env) extends LilaController(env) { def form(username: String) = Auth { implicit ctx => me => NotForKids { - if (!me.is(username)) Redirect(routes.Ublog.form(me.username)).fuccess - else Ok(html.ublog.form.create(me, env.ublog.form.create)).fuccess + if (env.ublog.api.canBlog(me)) { + if (!me.is(username)) Redirect(routes.Ublog.form(me.username)).fuccess + else + env.ublog.form.anyCaptcha map { captcha => + Ok(html.ublog.form.create(me, env.ublog.form.create, captcha)) + } + } else + Unauthorized( + html.site.message.notYet( + "Please play a few games and wait 2 days before you can create blog posts." + ) + ).fuccess } } @@ -70,7 +80,10 @@ final class Ublog(env: Env) extends LilaController(env) { env.ublog.form.create .bindFromRequest()(ctx.body, formBinding) .fold( - err => BadRequest(html.ublog.form.create(me, err)).fuccess, + err => + env.ublog.form.anyCaptcha map { captcha => + BadRequest(html.ublog.form.create(me, err, captcha)) + }, data => CreateLimitPerUser(me.id, cost = if (me.isVerified) 1 else 3) { env.ublog.api.create(data, me) map { post => diff --git a/app/views/base/captcha.scala b/app/views/base/captcha.scala index 2e7a7a02ef..649de82107 100644 --- a/app/views/base/captcha.scala +++ b/app/views/base/captcha.scala @@ -61,4 +61,9 @@ object captcha { ) } ) + + def hiddenEmpty(form: lila.common.Form.FormLike) = frag( + form3.hidden(form("gameId")), + form3.hidden(form("move")) + ) } diff --git a/app/views/site/message.scala b/app/views/site/message.scala index 0f82a96d42..3d5143712e 100644 --- a/app/views/site/message.scala +++ b/app/views/site/message.scala @@ -103,4 +103,7 @@ object message { apply("Temporarily disabled")( "Sorry, his feature is temporarily disabled while we figure out a way to bring it back." ) + + def notYet(text: String)(implicit ctx: Context) = + apply("Not yet available")(text) } diff --git a/app/views/ublog/form.scala b/app/views/ublog/form.scala index 7232d64268..663d404be0 100644 --- a/app/views/ublog/form.scala +++ b/app/views/ublog/form.scala @@ -6,6 +6,7 @@ import play.api.data.Form import lila.api.Context import lila.app.templating.Environment._ import lila.app.ui.ScalatagsTemplate._ +import lila.common.Captcha import lila.ublog.UblogForm.UblogPostData import lila.ublog.UblogPost import lila.user.User @@ -14,15 +15,16 @@ object form { import views.html.ublog.{ post => postView } - def create(user: User, f: Form[UblogPostData])(implicit ctx: Context) = + def create(user: User, f: Form[UblogPostData], captcha: Captcha)(implicit ctx: Context) = views.html.base.layout( moreCss = cssTag("ublog"), + moreJs = captchaTag, title = s"${trans.ublog.xBlog.txt(user.username)} • ${trans.ublog.newPost()}" ) { main(cls := "box box-pad page page-small ublog-post-form")( h1(trans.ublog.newPost()), etiquette, - inner(user, f, none) + inner(user, f, none, captcha.some) ) } @@ -35,7 +37,7 @@ object form { main(cls := "box box-pad page page-small ublog-post-form")( h1(trans.ublog.editYourBlogPost()), imageForm(user, post), - inner(user, f, post.some), + inner(user, f, post.some, none), postForm( cls := "ublog-post-form__delete", action := routes.Ublog.delete(user.username, post.id.value) @@ -67,8 +69,8 @@ object form { def formImage(post: UblogPost) = postView.thumbnail(post, _.Small) - private def inner(user: User, form: Form[UblogPostData], post: Option[UblogPost])(implicit - ctx: Context + private def inner(user: User, form: Form[UblogPostData], post: Option[UblogPost], captcha: Option[Captcha])( + implicit ctx: Context ) = postForm( cls := "form3", @@ -89,6 +91,9 @@ object form { trans.ublog.postBody(), help = frag(markdownAvailable, br, trans.embedsAvailable()).some )(form3.textarea(_)(rows := 30)), + captcha.fold(views.html.base.captcha.hiddenEmpty(form)) { c => + views.html.base.captcha(form, c) + }, form3.actions( a(href := post.fold(routes.Ublog.index(user.username))(views.html.ublog.post.urlOf))(trans.cancel()), form3.submit(trans.apply()) diff --git a/modules/ublog/src/main/Env.scala b/modules/ublog/src/main/Env.scala index 445637b930..60ce2c1f40 100644 --- a/modules/ublog/src/main/Env.scala +++ b/modules/ublog/src/main/Env.scala @@ -11,7 +11,8 @@ final class Env( timeline: lila.hub.actors.Timeline, picfitApi: lila.memo.PicfitApi, picfitUrl: lila.memo.PicfitUrl, - ircApi: lila.irc.IrcApi + ircApi: lila.irc.IrcApi, + captcher: lila.hub.actors.Captcher )(implicit ec: scala.concurrent.ExecutionContext ) { diff --git a/modules/ublog/src/main/UblogApi.scala b/modules/ublog/src/main/UblogApi.scala index 3ab805e2ab..3e37c3c1d2 100644 --- a/modules/ublog/src/main/UblogApi.scala +++ b/modules/ublog/src/main/UblogApi.scala @@ -99,6 +99,11 @@ final class UblogApi( coll.delete.one($id(post.id)) >> picfitApi.deleteByRel(imageRel(post)) + def canBlog(u: User) = + !u.isBot && { + (u.count.game > 0 && u.createdSinceDays(2)) || u.hasTitle || u.isVerified || u.isPatron + } + private def paginatorByUser(user: User, live: Boolean, page: Int): Fu[Paginator[UblogPost.PreviewPost]] = Paginator( adapter = new Adapter[UblogPost.PreviewPost]( diff --git a/modules/ublog/src/main/UblogForm.scala b/modules/ublog/src/main/UblogForm.scala index 4a08c1d234..8953379460 100644 --- a/modules/ublog/src/main/UblogForm.scala +++ b/modules/ublog/src/main/UblogForm.scala @@ -7,28 +7,49 @@ import play.api.data.Forms._ import lila.common.Form.{ cleanNonEmptyText, cleanText, markdownImage } import lila.user.User -final class UblogForm(markup: UblogMarkup) { +final class UblogForm(markup: UblogMarkup, val captcher: lila.hub.actors.Captcher)(implicit + ec: scala.concurrent.ExecutionContext +) extends lila.hub.CaptchedForm { import UblogForm._ - val create = Form( + private val base = mapping( "title" -> cleanNonEmptyText(minLength = 3, maxLength = 100), "intro" -> cleanNonEmptyText(minLength = 0, maxLength = 2_000), "markdown" -> cleanNonEmptyText(minLength = 0, maxLength = 100_000).verifying(markdownImage.constraint), - "live" -> boolean + "live" -> boolean, + "gameId" -> text, + "move" -> text )(UblogPostData.apply)(UblogPostData.unapply) + + val create = Form( + base.verifying(captchaFailMessage, validateCaptcha _) ) def edit(post: UblogPost) = - create.fill( - UblogPostData(title = post.title, intro = post.intro, markdown = post.markdown, live = post.live) + Form(base).fill( + UblogPostData( + title = post.title, + intro = post.intro, + markdown = post.markdown, + live = post.live, + gameId = "", + move = "" + ) ) } object UblogForm { - case class UblogPostData(title: String, intro: String, markdown: String, live: Boolean) { + case class UblogPostData( + title: String, + intro: String, + markdown: String, + live: Boolean, + gameId: String, + move: String + ) { def create(user: User) = { val now = DateTime.now diff --git a/ui/site/css/build/_ublog.scss b/ui/site/css/build/_ublog.scss index 6d5e0b2f08..d1085b8ea6 100644 --- a/ui/site/css/build/_ublog.scss +++ b/ui/site/css/build/_ublog.scss @@ -1,4 +1,5 @@ @import '../../../common/css/plugin'; @import '../../../common/css/component/slist'; @import '../../../common/css/form/form3'; +@import '../../../common/css/form/captcha'; @import '../ublog/ublog';