refactor embeds, use no ctx, manual csp nonce

more-scalatags
Thibault Duplessis 2019-04-15 17:07:12 +07:00
parent c102071217
commit 791d140e4c
22 changed files with 166 additions and 122 deletions

View File

@ -68,16 +68,16 @@ object Analyse extends LilaController {
}
}
def embed(gameId: String, color: String) = Open { implicit ctx =>
def embed(gameId: String, color: String) = Action.async { implicit req =>
GameRepo.gameWithInitialFen(gameId) flatMap {
case Some((game, initialFen)) =>
val pov = Pov(game, chess.Color(color == "white"))
Env.api.roundApi.review(pov, lila.api.Mobile.Api.currentVersion,
Env.api.roundApi.embed(pov, lila.api.Mobile.Api.currentVersion,
initialFenO = initialFen.some,
withFlags = WithFlags(opening = true)) map { data =>
Ok(html.analyse.embed(pov, data))
}
case _ => fuccess(NotFound(html.analyse.embed.notFound()))
case _ => fuccess(NotFound(html.analyse.embed.notFound))
}
}

View File

@ -42,13 +42,12 @@ object Auth extends LilaController {
}
def authenticateUser(u: UserModel, result: Option[String => Result] = None)(implicit ctx: Context): Fu[Result] = {
implicit val req = ctx.req
if (u.ipBan) fuccess(Redirect(routes.Lobby.home))
else api.saveAuthentication(u.id, ctx.mobileApiVersion) flatMap { sessionId =>
negotiate(
html = fuccess {
val redirectTo = get("referrer").filter(goodReferrer) orElse
req.session.get(api.AccessUri) getOrElse
ctxReq.session.get(api.AccessUri) getOrElse
routes.Lobby.home.url
result.fold(Redirect(redirectTo))(_(redirectTo))
},
@ -118,8 +117,7 @@ object Auth extends LilaController {
}
def logout = Open { implicit ctx =>
implicit val req = ctx.req
req.session get "sessionId" foreach lila.security.Store.delete
ctxReq.session get "sessionId" foreach lila.security.Store.delete
negotiate(
html = Redirect(routes.Main.mobile).fuccess,
api = _ => Ok(Json.obj("ok" -> true)).fuccess
@ -128,11 +126,10 @@ object Auth extends LilaController {
// mobile app BC logout with GET
def logoutGet = Open { implicit ctx =>
implicit val req = ctx.req
negotiate(
html = notFound,
api = _ => {
req.session get "sessionId" foreach lila.security.Store.delete
ctxReq.session get "sessionId" foreach lila.security.Store.delete
Ok(Json.obj("ok" -> true)).withCookies(LilaCookie.newSession).fuccess
}
)
@ -317,7 +314,6 @@ object Auth extends LilaController {
}
private def redirectNewUser(user: UserModel)(implicit ctx: Context) = {
implicit val req = ctx.req
api.saveAuthentication(user.id, ctx.mobileApiVersion) flatMap { sessionId =>
negotiate(
html = Redirect(routes.User.show(user.username)).fuccess,

View File

@ -93,7 +93,6 @@ object Challenge extends LilaController {
cond ?? {
GameRepo.game(c.id).map {
_ map { game =>
implicit val req = ctx.req
LilaCookie.cookie(
AnonCookie.name,
game.player(if (owner) c.finalColor else !c.finalColor).id,

View File

@ -47,7 +47,9 @@ private[controllers] trait LilaController
api = _ => fuccess(jsonOkResult)
)
implicit def lang(implicit ctx: Context) = ctx.lang
implicit def ctxLang(implicit ctx: Context) = ctx.lang
implicit def ctxReq(implicit ctx: Context) = ctx.req
implicit def reqConfig(implicit req: RequestHeader) = ui.EmbedConfig(req)
protected def NoCache(res: Result): Result = res.withHeaders(
CACHE_CONTROL -> "no-cache, no-store, must-revalidate", EXPIRES -> "0"
@ -338,8 +340,7 @@ private[controllers] trait LilaController
protected def authenticationFailed(implicit ctx: Context): Fu[Result] =
negotiate(
html = fuccess {
implicit val req = ctx.req
Redirect(routes.Auth.signup) withCookies LilaCookie.session(Env.security.api.AccessUri, req.uri)
Redirect(routes.Auth.signup) withCookies LilaCookie.session(Env.security.api.AccessUri, ctx.req.uri)
},
api = _ => ensureSessionId(ctx.req) {
Unauthorized(jsonError("Login required"))
@ -362,8 +363,8 @@ private[controllers] trait LilaController
if (req.session.data.contains(LilaCookie.sessionId)) res
else res withCookies LilaCookie.makeSessionId(req)
protected def negotiate(html: => Fu[Result], api: ApiVersion => Fu[Result])(implicit ctx: Context): Fu[Result] =
lila.api.Mobile.Api.requestVersion(ctx.req).fold(html) { v =>
protected def negotiate(html: => Fu[Result], api: ApiVersion => Fu[Result])(implicit req: RequestHeader): Fu[Result] =
lila.api.Mobile.Api.requestVersion(req).fold(html) { v =>
api(v) dmap (_ as JSON)
}.dmap(_.withHeaders("Vary" -> "Accept"))

View File

@ -226,14 +226,9 @@ object Puzzle extends LilaController {
}
def frame = Action.async { implicit req =>
implicit val lang = lila.i18n.I18nLangPicker(req, none)
env.daily.get map {
case None => NotFound
case Some(daily) => html.puzzle.embed(
daily,
get("bg", req) | "light",
lila.pref.Theme(~get("theme", req)).cssClass
)
case Some(daily) => html.puzzle.embed(daily)(ui.EmbedConfig(req))
}
}
}

View File

@ -218,7 +218,6 @@ object Setup extends LilaController with TheftPrevention {
}
private[controllers] def redirectPov(pov: Pov)(implicit ctx: Context) = {
implicit val req = ctx.req
val redir = Redirect(routes.Round.watcher(pov.gameId, "white"))
if (ctx.isAuth) redir
else redir withCookies LilaCookie.cookie(

View File

@ -278,17 +278,17 @@ object Study extends LilaController {
)
}
def embed(id: String, chapterId: String) = Open { implicit ctx =>
env.api.byIdWithChapter(id, chapterId) flatMap {
def embed(id: String, chapterId: String) = Action.async { implicit req =>
env.api.byIdWithChapter(id, chapterId).map(_.filterNot(_.study.isPrivate)) flatMap {
_.fold(embedNotFound) {
case WithChapter(study, chapter) => CanViewResult(study) {
case WithChapter(study, chapter) =>
env.jsonView(study.copy(
members = lila.study.StudyMembers(Map.empty) // don't need no members
), List(chapter.metadata), chapter, ctx.me) flatMap { studyJson =>
), List(chapter.metadata), chapter, none) flatMap { studyJson =>
val setup = chapter.setup
val initialFen = chapter.root.fen.some
val pov = UserAnalysis.makePov(initialFen, setup.variant)
val baseData = Env.round.jsonView.userAnalysisJson(pov, ctx.pref, initialFen, setup.orientation, owner = false, me = ctx.me)
val baseData = Env.round.jsonView.userAnalysisJson(pov, lila.pref.Pref.default, initialFen, setup.orientation, owner = false, me = none)
val analysis = baseData ++ Json.obj(
"treeParts" -> partitionTreeJsonWriter.writes {
lila.study.TreeBuilder.makeRoot(chapter.root, setup.variant)
@ -303,12 +303,11 @@ object Study extends LilaController {
api = _ => Ok(Json.obj("study" -> data.study, "analysis" -> data.analysis)).fuccess
)
}
}
}
} map NoCache
}
private def embedNotFound(implicit ctx: Context): Fu[Result] =
private def embedNotFound(implicit req: RequestHeader): Fu[Result] =
fuccess(NotFound(html.study.embed.notFound()))
def cloneStudy(id: String) = Auth { implicit ctx => me =>

View File

@ -79,9 +79,8 @@ object Tv extends LilaController {
/* for BC */
def embed = Action { req =>
Ok {
val bg = get("bg", req) | "light"
val theme = get("theme", req) | "brown"
val url = s"""${req.domain + routes.Tv.frame}?bg=$bg&theme=$theme"""
val config = ui.EmbedConfig(req)
val url = s"""${req.domain + routes.Tv.frame}?bg=${config.bg}&theme=${config.board}"""
s"""document.write("<iframe src='https://$url&embed=" + document.domain + "' class='lichess-tv-iframe' allowtransparency='true' frameBorder='0' style='width: 224px; height: 264px;' title='Lichess free online chess'></iframe>");"""
} as JAVASCRIPT withHeaders (CACHE_CONTROL -> "max-age=86400")
}
@ -89,11 +88,7 @@ object Tv extends LilaController {
def frame = Action.async { implicit req =>
Env.tv.tv.getBestGame map {
case None => NotFound
case Some(game) => Ok(views.html.tv.embed(
Pov first game,
get("bg", req) | "light",
lila.pref.Theme(~get("theme", req)).cssClass
))
case Some(game) => Ok(views.html.tv.embed(Pov first game, ui.EmbedConfig(req)))
}
}
}

View File

@ -147,12 +147,12 @@ object User extends LilaController {
}
}
def online = Open { implicit req =>
def online = Action.async { implicit req =>
val max = 50
negotiate(
html = notFound,
html = notFoundJson(),
api = _ => env.cached.getTop50Online map { list =>
Ok(Json.toJson(list.take(getInt("nb").fold(10)(_ min max)).map(env.jsonView(_))))
Ok(Json.toJson(list.take(getInt("nb", req).fold(10)(_ min max)).map(env.jsonView(_))))
}
)
}

View File

@ -7,7 +7,7 @@ import play.twirl.api.Html
import lila.api.Context
import lila.app.ui.ScalatagsTemplate._
import lila.common.{ AssetVersion, ContentSecurityPolicy }
import lila.common.{ Nonce, AssetVersion, ContentSecurityPolicy }
trait AssetHelper { self: I18nHelper with SecurityHelper =>
@ -135,4 +135,8 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
def embedJs(js: Frag)(implicit ctx: Context): Frag = embedJsUnsafe(js.render)
def embedJs(js: String)(implicit ctx: Context): Frag = embedJsUnsafe(js)
def embedJs(js: String, nonce: Nonce): Frag = raw {
s"""<script nonce="$nonce">$js</script>"""
}
}

View File

@ -0,0 +1,27 @@
package lila.app
package ui
import play.api.mvc.RequestHeader
import lila.common.{ Nonce, Lang }
case class EmbedConfig(bg: String, board: String, lang: Lang, req: RequestHeader, nonce: Nonce)
object EmbedConfig {
object implicits {
implicit def configLang(implicit config: EmbedConfig): Lang = config.lang
implicit def configReq(implicit config: EmbedConfig): RequestHeader = config.req
}
def apply(req: RequestHeader): EmbedConfig = EmbedConfig(
bg = get("bg", req) | "light",
board = lila.pref.Theme(~get("theme", req)).cssClass,
lang = lila.i18n.I18nLangPicker(req, none),
req = req,
nonce = Nonce.random
)
private def get(name: String, req: RequestHeader): Option[String] =
req.queryString get name flatMap (_.headOption) filter (_.nonEmpty)
}

View File

@ -1,10 +1,13 @@
package views.html.analyse
import play.api.libs.json.Json
import play.api.libs.json.JsObject
import play.api.mvc.RequestHeader
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.EmbedConfig
import lila.app.ui.ScalatagsTemplate._
import lila.common.Lang
import lila.common.String.html.safeJsonValue
import controllers.routes
@ -12,22 +15,22 @@ import controllers.routes
object embed {
import views.html.base.layout.bits._
import EmbedConfig.implicits._
def apply(pov: lila.game.Pov, data: play.api.libs.json.JsObject)(implicit ctx: Context) = frag(
def apply(pov: lila.game.Pov, data: JsObject)(implicit config: EmbedConfig) = frag(
doctype,
htmlTag(ctx)(
htmlTag(config.lang)(
topComment,
head(
charset,
viewport,
metaCsp(none),
metaCsp(basicCsp withNonce config.nonce),
st.headTitle(replay titleOf pov),
pieceSprite(lila.pref.PieceSet.default),
responsiveCssTag("analyse.round.embed")
responsiveCssTagWithTheme("analyse.round.embed", config.bg)
),
body(cls := List(
s"highlight ${ctx.currentBg} ${ctx.currentTheme.cssClass}" -> true,
"piece-letter" -> ctx.pref.pieceNotationIsLetter
s"highlight ${config.bg} ${config.board}" -> true
))(
div(cls := "is2d")(
main(cls := "analyse")
@ -53,23 +56,23 @@ object embed {
data: ${safeJsonValue(data)},
embed: true,
i18n: ${views.html.board.userAnalysisI18n(withCeval = false, withExplorer = false)}
});""")
});""", config.nonce)
)
)
)
def notFound()(implicit ctx: Context) = frag(
def notFound(implicit config: EmbedConfig) = frag(
doctype,
htmlTag(ctx)(
htmlTag(config.lang)(
topComment,
head(
charset,
viewport,
metaCsp(none),
metaCsp(basicCsp),
st.headTitle("404 - Game not found"),
responsiveCssTag("analyse.round.embed")
responsiveCssTagWithTheme("analyse.round.embed", "dark")
),
body(cls := ctx.currentBg)(
body(cls := "dark")(
div(cls := "not-found")(
h1("Game not found")
)

View File

@ -6,6 +6,7 @@ import bits.dataPanel
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.Lang
import lila.common.String.html.safeJsonValue
import lila.game.Pov
@ -13,7 +14,7 @@ import controllers.routes
object replay {
private[analyse] def titleOf(pov: Pov)(implicit ctx: Context) =
private[analyse] def titleOf(pov: Pov)(implicit lang: Lang) =
s"${playerText(pov.game.whitePlayer)} vs ${playerText(pov.game.blackPlayer)}: ${pov.game.opening.fold(trans.analysis.txt())(_.opening.ecoName)}"
def apply(

View File

@ -3,7 +3,7 @@ package views.html.base
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.ContentSecurityPolicy
import lila.common.{ Lang, ContentSecurityPolicy }
import controllers.routes
@ -11,7 +11,7 @@ object layout {
object bits {
val doctype = raw("<!doctype html>")
def htmlTag(implicit ctx: Context) = html(st.lang := ctx.lang.language)
def htmlTag(implicit lang: Lang) = html(st.lang := lang.language)
val topComment = raw("""<!-- Lichess is open source! See https://github.com/ornicar/lila -->""")
val charset = raw("""<meta charset="utf-8">""")
val viewport = raw("""<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"/>""")
@ -105,7 +105,7 @@ object layout {
wrapClass: String = ""
)(body: Frag)(implicit ctx: Context) = frag(
doctype,
htmlTag(ctx)(
htmlTag(ctx.lang)(
topComment,
head(
charset,

View File

@ -3,6 +3,7 @@ package views.html.board
import lila.api.Context
import lila.app.templating.Environment._
import lila.common.String.html.safeJsonValue
import lila.common.Lang
import lila.i18n.{ I18nKeys => trans }
object userAnalysisI18n {
@ -11,7 +12,7 @@ object userAnalysisI18n {
withCeval: Boolean = true,
withExplorer: Boolean = true,
withForecast: Boolean = false
)(implicit ctx: Context) = safeJsonValue(i18nJsObject(
)(implicit lang: Lang) = safeJsonValue(i18nJsObject(
baseTranslations ++ {
withCeval ?? cevalTranslations
} ++ {

View File

@ -5,27 +5,29 @@ import play.api.mvc.RequestHeader
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.Lang
import lila.app.ui.EmbedConfig
import views.html.base.layout.{ bits => layout }
import controllers.routes
object embed {
import EmbedConfig.implicits._
private val dataStreamUrl = attr("data-stream-url")
def apply(daily: lila.puzzle.DailyPuzzle, bg: String, board: String)(implicit req: RequestHeader, lang: Lang) = frag(
def apply(daily: lila.puzzle.DailyPuzzle)(implicit config: EmbedConfig) = frag(
layout.doctype,
html(
layout.htmlTag(config.lang)(
head(
layout.charset,
layout.metaCsp(basicCsp),
st.headTitle("lichess.org chess puzzle"),
layout.pieceSprite(lila.pref.PieceSet.default),
responsiveCssTagWithTheme("tv.embed", bg)
responsiveCssTagWithTheme("tv.embed", config.bg)
),
body(
cls := s"base $board merida",
cls := s"base ${config.board}",
dataStreamUrl := routes.Tv.feed
)(
div(id := "daily-puzzle", cls := "embedded", title := trans.clickToSolve.txt())(

View File

@ -1,11 +1,13 @@
package views.html.study
import play.api.libs.json.Json
import play.api.mvc.RequestHeader
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.safeJsonValue
import lila.common.Lang
import controllers.routes
@ -13,73 +15,74 @@ object embed {
import views.html.base.layout.bits._
private def bodyClass(implicit ctx: Context) = List(
"base" -> true,
ctx.currentTheme.cssClass -> true,
(if (ctx.currentBg == "transp") "dark transp" else ctx.currentBg) -> true
)
// private def bodyClass(implicit ctx: Context) = List(
// "base" -> true,
// ctx.currentTheme.cssClass -> true,
// (if (ctx.currentBg == "transp") "dark transp" else ctx.currentBg) -> true
// )
def apply(s: lila.study.Study, chapter: lila.study.Chapter, data: lila.study.JsonView.JsData)(implicit ctx: Context) = frag(
private implicit def lang(implicit req: RequestHeader): Lang = lila.i18n.I18nLangPicker(req, none)
def apply(s: lila.study.Study, chapter: lila.study.Chapter, data: lila.study.JsonView.JsData)(implicit req: RequestHeader) = frag(
doctype,
htmlTag(ctx)(
topComment,
head(
charset,
metaCsp(none),
st.headTitle(s"${s.name} ${chapter.name}"),
// fontStylesheets, // use proper stylesheets instead
// currentBgCss,
cssTags("common.css", "board.css", "analyse.css", "analyse-embed.css"),
pieceSprite
),
body(cls := bodyClass ::: List(
"highlight" -> true,
"piece-letter" -> ctx.pref.pieceNotationIsLetter
))(
div(cls := "is2d")(
div(cls := "embedded_study analyse cg-512")(miniBoardContent)
),
footer {
val url = routes.Study.chapter(s.id.value, chapter.id.value)
div(cls := "left")(
a(target := "_blank", href := url)(h1(s.name.value)),
" ",
em("brought to you by ", a(target := "_blank", href := netBaseUrl)(netDomain))
)
a(target := "_blank", cls := "open", href := url)("Open")
},
jQueryTag,
jsTag("vendor/mousetrap.js"),
jsAt("compiled/util.js"),
jsAt("compiled/trans.js"),
analyseTag,
jsTag("embed-analyse.js"),
embedJs(s"""lichess.startEmbeddedAnalyse({
element: document.querySelector('.embedded_study'),
study: ${safeJsonValue(data.study)},
data: ${safeJsonValue(data.analysis)},
embed: true,
i18n: ${views.html.board.userAnalysisI18n()},
userId: null
});""")
)
htmlTag(lang)( // topComment,
// head(
// charset,
// metaCsp(basicCsp),
// st.headTitle(s"${s.name} ${chapter.name}"),
// // fontStylesheets, // use proper stylesheets instead
// // currentBgCss,
// cssTags("common.css", "board.css", "analyse.css", "analyse-embed.css"),
// pieceSprite
// ),
// body(cls := bodyClass ::: List(
// "highlight" -> true,
// "piece-letter" -> ctx.pref.pieceNotationIsLetter
// ))(
// div(cls := "is2d")(
// div(cls := "embedded_study analyse cg-512")(miniBoardContent)
// ),
// footer {
// val url = routes.Study.chapter(s.id.value, chapter.id.value)
// div(cls := "left")(
// a(target := "_blank", href := url)(h1(s.name.value)),
// " ",
// em("brought to you by ", a(target := "_blank", href := netBaseUrl)(netDomain))
// )
// a(target := "_blank", cls := "open", href := url)("Open")
// },
// jQueryTag,
// jsTag("vendor/mousetrap.js"),
// jsAt("compiled/util.js"),
// jsAt("compiled/trans.js"),
// analyseTag,
// jsTag("embed-analyse.js"),
// embedJs(s"""lichess.startEmbeddedAnalyse({
// element: document.querySelector('.embedded_study'),
// study: ${safeJsonValue(data.study)},
// data: ${safeJsonValue(data.analysis)},
// embed: true,
// i18n: ${views.html.board.userAnalysisI18n()},
// userId: null
// });""")
// )
)
)
def notFound()(implicit ctx: Context) = frag(
def notFound()(implicit req: RequestHeader) = frag(
doctype,
htmlTag(ctx)(
htmlTag(lang)(
topComment,
head(
charset,
metaCsp(none),
metaCsp(basicCsp),
st.headTitle("404 - Study not available"),
// fontStylesheets, // use proper stylesheets instead
// currentBgCss,
cssTags("common.css", "analyse-embed.css")
),
body(cls := bodyClass)(
div(cls := "not_found")(
body()(
div(cls := "not-found")(
h1("Study not available")
)
)

View File

@ -13,7 +13,7 @@ object embed {
private val dataStreamUrl = attr("data-stream-url")
def apply(pov: lila.game.Pov, bg: String, board: String)(implicit req: RequestHeader) = frag(
def apply(pov: lila.game.Pov, config: lila.app.ui.EmbedConfig)(implicit req: RequestHeader) = frag(
bits.doctype,
html(
head(
@ -22,10 +22,10 @@ object embed {
bits.metaCsp(basicCsp),
st.headTitle("lichess.org chess TV"),
bits.pieceSprite(lila.pref.PieceSet.default),
responsiveCssTagWithTheme("tv.embed", bg)
responsiveCssTagWithTheme("tv.embed", config.bg)
),
body(
cls := s"base $board merida",
cls := s"base ${config.board}",
dataStreamUrl := routes.Tv.feed
)(
div(id := "featured-game", cls := "embedded", title := "lichess.org TV")(

View File

@ -90,6 +90,21 @@ private[api] final class RoundApi(
}
}.mon(_.round.api.watcher)
def embed(pov: Pov, apiVersion: ApiVersion,
analysis: Option[Analysis] = None,
initialFenO: Option[Option[FEN]] = None,
withFlags: WithFlags): Fu[JsObject] =
initialFenO.fold(GameRepo initialFen pov.game)(fuccess).flatMap { initialFen =>
jsonView.watcherJson(pov, Pref.default, apiVersion, none, none,
initialFen = initialFen,
withFlags = withFlags) map { json =>
(
withTree(pov, analysis, initialFen, withFlags)_ compose
withAnalysis(pov.game, analysis)_
)(json)
}
}.mon(_.round.api.embed)
def userAnalysisJson(pov: Pov, pref: Pref, initialFen: Option[FEN], orientation: chess.Color, owner: Boolean, me: Option[User]) =
owner.??(forecastApi loadForDisplay pov).map { fco =>
withForecast(pov, owner, fco) {

View File

@ -171,6 +171,7 @@ object mon {
object api {
val player = rec("round.api.player")
val watcher = rec("round.api.watcher")
val embed = rec("round.api.embed")
}
object actor {
val count = rec("round.actor.count")

View File

@ -134,4 +134,7 @@ lichess.startEmbeddedAnalyse = function(opts) {
opts.initialPly = 'url';
opts.trans = lichess.trans(opts.i18n);
LichessAnalyse.start(opts);
window.addEventListener('resize', function() {
lichess.dispatchEvent(document.body, 'chessground.resize');
});
}

View File

@ -94,7 +94,7 @@ function studyButton(ctrl: AnalyseCtrl) {
'data-icon': '4'
}
}, ctrl.trans.noarg('openStudy'));
if (ctrl.study || ctrl.ongoing) return;
if (ctrl.study || ctrl.ongoing || ctrl.embed) return;
return h('form', {
attrs: {
method: 'post',