Skip to content

Commit

Permalink
Merge v0.2024.007 into 'release'.
Browse files Browse the repository at this point in the history
  • Loading branch information
kajmagnus committed Sep 23, 2024
2 parents 07ac81a + 7148768 commit 8340b32
Show file tree
Hide file tree
Showing 62 changed files with 2,347 additions and 284 deletions.
2 changes: 0 additions & 2 deletions appsv/model/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,4 @@ libraryDependencies ++= Seq(
Dependencies.Libs.scalaTest,
)

scalacOptions += "-deprecation"

compileOrder := CompileOrder.JavaThenScala
2 changes: 1 addition & 1 deletion appsv/model/src/main/scala/com/debiki/core/PagePath.scala
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ object PagePath {
}


/** Parses the path part of a URL into a PagePath.
/** Parses the path part of a URL into a PagePath. [ty_url_fmt]
*
* URL path examples:
* - (server)/fold/ers/-pageId [2WBG49]
Expand Down
2 changes: 1 addition & 1 deletion appsv/model/src/main/scala/com/debiki/core/Site.scala
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ sealed abstract class SiteStatus(val IntValue: i32) {
}


case class SuperAdminSitePatch(
case class SuperAdminSitePatch( // ts: interface SASitePatch
siteId: SiteId,
newStatus: SiteStatus,
newNotes: Opt[St],
Expand Down
3 changes: 3 additions & 0 deletions appsv/model/src/main/scala/com/debiki/core/user.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,9 @@ sealed trait MemberInclDetails extends ParticipantInclDetails { RENAME // to Me
*/
def uiPrefs: Option[JsObject]

// Later:
//def modConf: Opt[JsObject] // the pats_t/users3.mod_conf_c column

def copyPrefs(uiPrefs: Opt[JsObject] = null, privPrefs: MemberPrivacyPrefs = null): MemberVb = {
this match {
case thiz: GroupVb =>
Expand Down
6 changes: 5 additions & 1 deletion appsv/server/controllers/DraftsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,13 @@ class DraftsController @Inject()(cc: ControllerComponents, edContext: TyContext)

if (draft.isReply) {
val postType = draft.postType getOrDie "TyER35SKS02GU"
val pageAuthor =
if (pageMeta.authorId == requester.id) requester
else dao.getTheParticipant(pageMeta.authorId)
throwNoUnless(Authz.mayPostReply(
request.theUserAndLevels, asAlias = None, dao.getOnesGroupIds(requester),
postType, pageMeta, Vector(post), dao.getAnyPrivateGroupTalkMembers(pageMeta),
postType, pageMeta, pageAuthor = pageAuthor,
Vector(post), dao.getAnyPrivateGroupTalkMembers(pageMeta),
inCategoriesRootLast = categoriesRootLast,
tooManyPermissions = dao.getPermsOnPages(categoriesRootLast)), "EdEZBXK3M2")
}
Expand Down
16 changes: 12 additions & 4 deletions appsv/server/controllers/ReplyController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ class ReplyController @Inject()(cc: ControllerComponents, edContext: TyContext)
REMOVE // these 3 vals, once we're using dao.insertReplyIfAuZ() instead of
// doing authz here in this fn.
val pageMeta = dao.getPageMeta(pageId) getOrElse throwIndistinguishableNotFound("EdE5FKW20")
val pageAuthor =
if (pageMeta.authorId == requester.id) requester
else dao.getTheParticipant(pageMeta.authorId)
val replyToPosts = dao.loadPostsAllOrError(pageId, replyToPostNrs) getOrIfBad { missingPostNr =>
throwNotFound(s"Post nr $missingPostNr not found", "EdEW3HPY08")
}
Expand All @@ -85,8 +88,9 @@ class ReplyController @Inject()(cc: ControllerComponents, edContext: TyContext)

CLEAN_UP // [dupl_re_authz_chk] and see the REM OVE just above too, and COU LD below.
throwNoUnless(Authz.mayPostReply(
request.theUserAndLevels, asAlias, dao.getOnesGroupIds(request.theUser),
postType, pageMeta, replyToPosts, dao.getAnyPrivateGroupTalkMembers(pageMeta),
request.theUserAndLevels, asAlias, dao.getOnesGroupIds(requester),
postType, pageMeta, pageAuthor = pageAuthor,
replyToPosts, dao.getAnyPrivateGroupTalkMembers(pageMeta),
inCategoriesRootLast = categoriesRootLast,
tooManyPermissions = dao.getPermsOnPages(categoriesRootLast)),
"TyEM0REPLY_")
Expand Down Expand Up @@ -119,7 +123,7 @@ class ReplyController @Inject()(cc: ControllerComponents, edContext: TyContext)

def handleChatMessage: Action[JsValue] = PostJsonAction(RateLimits.PostReply,
maxBytes = MaxPostSize) { request =>
import request.{body, dao}
import request.{body, dao, reqr}
val pageId = (body \ "pageId").as[PageId]
val text = (body \ "text").as[String].trim
val deleteDraftNr = (body \ "deleteDraftNr").asOpt[DraftNr]
Expand All @@ -132,12 +136,16 @@ class ReplyController @Inject()(cc: ControllerComponents, edContext: TyContext)
val pageMeta = dao.getPageMeta(pageId) getOrElse {
throwIndistinguishableNotFound("EdE7JS2")
}
val pageAuthor =
if (pageMeta.authorId == reqr.id) reqr
else dao.getTheParticipant(pageMeta.authorId)
val replyToPosts = Nil // currently cannot reply to specific posts, in the chat [7YKDW3]
val categoriesRootLast = dao.getAncestorCategoriesRootLast(pageMeta.categoryId)

throwNoUnless(Authz.mayPostReply(
request.theUserAndLevels, asAlias = None, dao.getOnesGroupIds(request.theMember),
PostType.ChatMessage, pageMeta, replyToPosts, dao.getAnyPrivateGroupTalkMembers(pageMeta),
PostType.ChatMessage, pageMeta, pageAuthor = pageAuthor,
replyToPosts, dao.getAnyPrivateGroupTalkMembers(pageMeta),
inCategoriesRootLast = categoriesRootLast,
tooManyPermissions = dao.getPermsOnPages(categoriesRootLast)),
"EdEHDETG4K5")
Expand Down
49 changes: 29 additions & 20 deletions appsv/server/controllers/UploadsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import play.api._
import play.api.libs.Files
import play.api.libs.json.{JsString, JsValue, Json}
import play.api.mvc._

import play.api.mvc.MultipartFormData.FilePart

/** Uploads files and serves uploaded files.
*/
Expand Down Expand Up @@ -137,22 +137,24 @@ class UploadsController @Inject()(cc: ControllerComponents, edContext: TyContext
if (files.length != 1)
throwBadRequest("EdE7UYMF3", s"Use the multipart form data key name 'file' please")

val file = files.head
val filePart: FilePart[Files.TemporaryFile] = files.head

// This far, in this endpoint, we've verified only that size <= maxBytesLargeFile.
// However, the upload limit might be lower, for this user or this site.
// We've checked this already [upl_sz_ck] — let's double check, and this
// time, there're no form-data boundaries.

_throwForbiddenMaybe(Some(file.filename), sizeBytes = file.fileSize, request)
_throwForbiddenMaybe(Some(filePart.filename), sizeBytes = filePart.fileSize, request)

val file: jio.File = filePart.ref.path.toFile

val uploadRef = dao.addUploadedFile(
file.filename, file.ref.file, request.theReqerTrueId, request.theBrowserIdData)
filePart.filename, file, request.theReqerTrueId, request.theBrowserIdData)

// Delete the temporary file. (It will be gone already, if we couldn't optimize it,
// i.e. make it smaller, because then we've moved it to the uploads dir (rather than
// a smaller compressed copy). Deleting file ref although gone already, doesn't do anything.)
file.ref.delete()
filePart.ref.delete()

// Don't use OkSafeJson here because Dropzone doesn't understand the safe-JSON prefix.
Ok(JsString(uploadRef.url)) as JSON
Expand Down Expand Up @@ -193,31 +195,38 @@ class UploadsController @Inject()(cc: ControllerComponents, edContext: TyContext
throwBadRequest("EdE35UY0", o"""Upload three images please: a tiny, a small and a medium
sized avatar image — instead I got $numFilesUploaded files""")

val tinyFile = multipartFormData.files.find(_.key == "images[tiny]") getOrElse {
val fileParts: Seq[FilePart[Files.TemporaryFile]] =
multipartFormData.files

val tinyFilePart = fileParts.find(_.key == "images[tiny]") getOrElse {
throwBadRequest("EdE8GYF2", o"""Upload a tiny size avatar image please""")
}

val smallFile = multipartFormData.files.find(_.key == "images[small]") getOrElse {
val smallFilePart = fileParts.find(_.key == "images[small]") getOrElse {
throwBadRequest("EdE4YF21", o"""Upload a small size avatar image please""")
}

val mediumFile = multipartFormData.files.find(_.key == "images[medium]") getOrElse {
val mediumFilePart = fileParts.find(_.key == "images[medium]") getOrElse {
throwBadRequest("EdE8YUF2", o"""Upload a medium size avatar image please""")
}

val tinyFile = tinyFilePart.ref.path.toFile
val smallFile = smallFilePart.ref.path.toFile
val mediumFile = mediumFilePart.ref.path.toFile

def throwIfTooLarge(whichFile: String, file: jio.File, maxBytes: Int): Unit = {
val length = file.length
if (length > maxBytes)
throwForbidden("DwE7YMF2", s"The $whichFile is too large: $length bytes, max is: $maxBytes")
}

throwIfTooLarge("tiny avatar image", tinyFile.ref.file, MaxAvatarTinySizeBytes)
throwIfTooLarge("small avatar image", smallFile.ref.file, MaxAvatarSmallSizeBytes)
throwIfTooLarge("medium avatar image", mediumFile.ref.file, MaxAvatarMediumSizeBytes)
throwIfTooLarge("tiny avatar image", tinyFile, MaxAvatarTinySizeBytes)
throwIfTooLarge("small avatar image", smallFile, MaxAvatarSmallSizeBytes)
throwIfTooLarge("medium avatar image", mediumFile, MaxAvatarMediumSizeBytes)

ImageUtils.throwUnlessJpegWithSideBetween(tinyFile.ref.file, "Tiny", 20, 35)
ImageUtils.throwUnlessJpegWithSideBetween(smallFile.ref.file, "Small", 40, 60)
ImageUtils.throwUnlessJpegWithSideBetween(mediumFile.ref.file, "Medium", 150, 800)
ImageUtils.throwUnlessJpegWithSideBetween(tinyFile, "Tiny", 20, 35)
ImageUtils.throwUnlessJpegWithSideBetween(smallFile, "Small", 40, 60)
ImageUtils.throwUnlessJpegWithSideBetween(mediumFile, "Medium", 150, 800)

// First add metadata entries for the files and move them in place.
// Then, if there were no errors, update the user so that it starts using the new
Expand All @@ -226,18 +235,18 @@ class UploadsController @Inject()(cc: ControllerComponents, edContext: TyContext
// (since they're unused) — deleting them is not yet implemented though [9YMU2Y].

val tinyAvatarRef = request.dao.addUploadedFile(
tinyFile.filename, tinyFile.ref.file, request.theReqerTrueId, request.theBrowserIdData)
tinyFilePart.filename, tinyFile, request.theReqerTrueId, request.theBrowserIdData)

val smallAvatarRef = request.dao.addUploadedFile(
smallFile.filename, smallFile.ref.file, request.theReqerTrueId, request.theBrowserIdData)
smallFilePart.filename, smallFile, request.theReqerTrueId, request.theBrowserIdData)

val mediumAvatarRef = request.dao.addUploadedFile(
mediumFile.filename, mediumFile.ref.file, request.theReqerTrueId, request.theBrowserIdData)
mediumFilePart.filename, mediumFile, request.theReqerTrueId, request.theBrowserIdData)

// Delete the temporary files.
tinyFile.ref.delete()
smallFile.ref.delete()
mediumFile.ref.delete()
tinyFilePart.ref.delete()
smallFilePart.ref.delete()
mediumFilePart.ref.delete()

// Now the images are in place in the uploads dir, and we've created metadata entries.
// We just need to link the user to the images:
Expand Down
3 changes: 2 additions & 1 deletion appsv/server/debiki/ImageUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ object ImageUtils {

val MimeTypeJpeg = "image/jpeg"

def throwUnlessJpegWithSideBetween(file: jio.File, which: String, minSide: Int, maxSide: Int): Unit = {
def throwUnlessJpegWithSideBetween(file: jio.File, which: St,
minSide: i32, maxSide: i32): U = {
// java.nio.file.Files.probeContentType doesn't work in Alpine Linux + JRE 8. Instead, use Tika.
// (This detects mime type based on actual document content, not just the suffix.) dupl [7YKW23]
val tika = new org.apache.tika.Tika()
Expand Down
9 changes: 5 additions & 4 deletions appsv/server/debiki/dao/ForumDao.scala
Original file line number Diff line number Diff line change
Expand Up @@ -818,10 +818,11 @@ object ForumDao {
* (Of course admins can change this, for their own site.) [DEFMAYEDWIKI]
* Sync with dupl code in Typescript. [7KFWY025]
*/
def makeFullMembersDefaultCategoryPerms(categoryId: CategoryId): PermsOnPages =
makeEveryonesDefaultCategoryPerms(categoryId).copy(
forPeopleId = Group.FullMembersId,
mayEditWiki = Some(true))
def makeFullMembersDefaultCategoryPerms(catId: CatId): PermsOnPages = PermsOnPages(
id = NoPermissionId,
forPeopleId = Group.FullMembersId,
onCategoryId = Some(catId),
mayEditWiki = Some(true))


// Sync with dupl code in Typescript. [7KFWY025]
Expand Down
24 changes: 16 additions & 8 deletions appsv/server/debiki/dao/PostsDao.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ trait PostsDao {

val pageMeta = this.getPageMeta(pageId) getOrElse throwIndistinguishableNotFound(
"TyE5FKW20", showErrCodeAnyway = reqrAndReplyer.reqrIsAdmin)
val pageAuthor =
if (pageMeta.authorId == reqrAndReplyer.reqr.id) reqrAndReplyer.reqr
else this.getTheParticipant(pageMeta.authorId)

val catsRootLast = this.getAncestorCategoriesSelfFirst(pageMeta.categoryId)
val tooManyPermissions = getPermsOnPages(categories = catsRootLast)
Expand All @@ -94,16 +97,16 @@ trait PostsDao {
reqrAndLevels,
// See [4_doer_not_reqr].
asAlias = if (reqrAndReplyer.areNotTheSame) None else asAlias,
this.getOnesGroupIds(reqrAndLevels.user),
postType, pageMeta, replyToPosts, privTalkMembers,
groupIds = this.getOnesGroupIds(reqrAndLevels.user),
postType, pageMeta, pageAuthor = pageAuthor, replyToPosts, privTalkMembers,
inCategoriesRootLast = catsRootLast, tooManyPermissions),
"TyEM0REPLY1")

if (reqrAndReplyer.areNotTheSame) {
val replyerAndLevels = readTx(this.loadUserAndLevels(reqrAndReplyer.targetToWho, _))
throwNoUnless(Authz.mayPostReply(
replyerAndLevels, asAlias, this.getOnesGroupIds(replyerAndLevels.user),
postType, pageMeta, replyToPosts, privTalkMembers,
postType, pageMeta, pageAuthor = pageAuthor, replyToPosts, privTalkMembers,
inCategoriesRootLast = catsRootLast, tooManyPermissions),
"TyEM0REPLY2")
}
Expand Down Expand Up @@ -186,9 +189,12 @@ trait PostsDao {
val authorMaybeAnon: Pat = SiteDao.getAliasOrTruePat(
truePat = realAuthor, pageId = pageId, asAlias)(tx, IfBadAbortReq)

val pageAuthor = tx.loadTheParticipant(page.meta.authorId)

dieOrThrowNoUnless(Authz.mayPostReply(
realAuthorAndLevels, asAlias /* _not_same_tx, ok */, realAuthorAndGroupIds,
postType, page.meta, replyToPosts, tx.loadAnyPrivateGroupTalkMembers(page.meta),
postType, page.meta, pageAuthor = pageAuthor,
replyToPosts, tx.loadAnyPrivateGroupTalkMembers(page.meta),
tx.loadCategoryPathRootLast(page.meta.categoryId, inclSelfFirst = true),
tx.loadPermsOnPages()), "EdEMAY0RE")

Expand Down Expand Up @@ -658,14 +664,16 @@ trait PostsDao {

SHOULD_OPTIMIZE // don't load all posts [2GKF0S6], because this is a chat, could be too many.
val page = newPageDao(pageId, tx)
val pageAuthor = tx.loadTheParticipant(page.meta.authorId)
val replyToPosts = Nil // currently cannot reply to specific posts, in the chat. [7YKDW3]
val asAlias = None // [anon_chats]

dieOrThrowNoUnless(Authz.mayPostReply(
authorAndLevels, asAlias = asAlias, tx.loadGroupIdsMemberIdFirst(author),
PostType.ChatMessage, page.meta, Nil, tx.loadAnyPrivateGroupTalkMembers(page.meta),
tx.loadCategoryPathRootLast(page.meta.categoryId, inclSelfFirst = true),
tx.loadPermsOnPages()), "EdEMAY0CHAT")
authorAndLevels, asAlias = asAlias, groupIds = tx.loadGroupIdsMemberIdFirst(author),
PostType.ChatMessage, page.meta, pageAuthor = pageAuthor,
replyToPosts = Nil, tx.loadAnyPrivateGroupTalkMembers(page.meta),
tx.loadCategoryPathRootLast(page.meta.categoryId, inclSelfFirst = true),
tx.loadPermsOnPages()), "EdEMAY0CHAT")

val (reviewReasons: Seq[ReviewReason], _) =
throwOrFindNewPostReviewReasons(page.meta, authorAndLevels, tx)
Expand Down
6 changes: 5 additions & 1 deletion appsv/server/debiki/dao/RenderContentService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ class RenderContentActor(
// COULD SECURITY DoS attack: Want to enqueue this case last-in-first-out, per page & params, so won't
// rerender the page more than once, even if is in the queue many times (with different hashes).
// Can that be done with Akka actors in some simple way?
case (sitePageId: SitePageId, paramsAndHash: Opt[RenderParamsAndFreshHash]) =>
case (sitePageId: SitePageId, paramsAndHash0: Opt[Any]) =>
val paramsAndHash: Opt[RenderParamsAndFreshHash] = paramsAndHash0 flatMap {
case p: RenderParamsAndFreshHash => Some(p)
case x => logger.warn(o"""Bad type: ${classNameOf(x)} [TyE5FL205F]""") ; None
}
// The page has 1) been modified, or accessed and was out-of-date. [4KGJW2]
// Or 2) edited and uncached, and now it's being rerendered in advance (but
// no one asked for it exactly now — paramsAndHash is None). [7BWTWR2]
Expand Down
7 changes: 4 additions & 3 deletions appsv/server/debiki/dao/UploadsDao.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ trait UploadsDao {
* directory, if stored on localhost (some file systems don't want 99999 files in a
* single directory).
*/
def addUploadedFile(uploadedFileName: String, tempFile: jio.File, uploadedById: TrueId,
def addUploadedFile(uploadedFileName: St, tempFile: jio.File, uploadedById: TrueId,
browserIdData: BrowserIdData): UploadRef = {

// Over quota? [fs_quota]
Expand Down Expand Up @@ -363,10 +363,11 @@ object UploadsDao {
private val Log4 = math.log(4)


def makeHashPath(file: jio.File, dotSuffix: String): String = {
def makeHashPath(file: jio.File, dotSuffix: St): St = {
// (It's okay to truncate the hash, see e.g.:
// http://crypto.stackexchange.com/questions/9435/is-truncating-a-sha512-hash-to-the-first-160-bits-as-secure-as-using-sha1 )
val hashCode = guava.io.Files.hash(file, guava.hash.Hashing.sha256)
val hashCode: guava.hash.HashCode =
guava.io.Files.asByteSource(file).hash(guava.hash.Hashing.sha256)
val hashString = base32lowercaseEncoder.encode(hashCode.asBytes) take HashLength
makeHashPath(file.length().toInt, hashString, dotSuffix)
}
Expand Down
4 changes: 2 additions & 2 deletions appsv/server/debiki/dao/UserDao.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1001,8 +1001,8 @@ trait UserDao {
}


def getTheParticipant(userId: UserId): Participant = {
getParticipant(userId).getOrElse(throw UserNotFoundException(userId))
def getTheParticipant(userId: UserId, anyTx: Opt[SiteTx] = None): Participant = {
getParticipant(userId, anyTx).getOrElse(throw UserNotFoundException(userId))
}


Expand Down
14 changes: 8 additions & 6 deletions appsv/server/talkyard/server/authz/Authz.scala
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ object Authz {
groupIds: immutable.Seq[GroupId],
postType: PostType,
pageMeta: PageMeta,
pageAuthor: Pat,
replyToPosts: immutable.Seq[Post],
privateGroupTalkMemberIds: Set[UserId],
inCategoriesRootLast: immutable.Seq[Category],
Expand All @@ -442,10 +443,7 @@ object Authz {
val mayWhat = checkPermsOnPages(
Some(user), asAlias = asAlias, groupIds,
Some(pageMeta),
// ANON_UNIMPL: Needed, for maySeeOwn [granular_perms], and later, if there'll be
// a mayReplyOnOwn permission?
// But let's wait until true author id is incl in posts3/nodes_t. [posts3_true_id]
pageAuthor = None,
pageAuthor = Some(pageAuthor),
pageMembers = Some(privateGroupTalkMemberIds),
catsRootLast = inCategoriesRootLast, tooManyPermissions)

Expand Down Expand Up @@ -852,8 +850,10 @@ object Authz {
// then, pat may not see it, or any sub cat. [see_sub_cat]
if (mayWhatThisCat.maySee isNot true) {
// But maySeeOwn=true has precedence over maySee=false.
if (isOwnPage && mayWhatThisCat.maySeeOwn) {
// Fine, continue.
if ((isOwnPage || pageMeta.isEmpty) && mayWhatThisCat.maySeeOwn) {
// Fine: Either it's our own page (which we may see), or no page specified — then
// we may still see the category.
mayWhatThisCat = mayWhatThisCat.copy(maySee = Some(true))
}
else
return mayWhatThisCat
Expand Down Expand Up @@ -1037,6 +1037,8 @@ case class MayWhat(
mayCreatePage: Bo = false,
mayPostComment: Bo = false,
maySee: Opt[Bo] = None,
// Used to derive maySee: if is own page, and maySeeOwn, then, maySee becomest Some(true).
// Is that a bit weird? Works for now.
maySeeOwn: Bo = false,
// maySeePagesOneIsMembOf: Bo = false
// mayListUnlistedCat
Expand Down
Loading

0 comments on commit 8340b32

Please sign in to comment.