Log discarded html tags + small refactor
This commit is contained in:
parent
941380ad16
commit
dd174a6327
@ -27,7 +27,7 @@ class ApiNoteController(
|
||||
|
||||
fun createNote(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val content = noteContentLens(request)
|
||||
return noteService.create(loggedInUser.userId, content).fold(
|
||||
return noteService.create(loggedInUser, content).fold(
|
||||
{ Response(BAD_REQUEST) },
|
||||
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
|
||||
)
|
||||
@ -45,7 +45,7 @@ class ApiNoteController(
|
||||
|
||||
fun update(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val content = noteContentLens(request)
|
||||
return noteService.update(loggedInUser.userId, uuidLens(request), content).fold(
|
||||
return noteService.update(loggedInUser, uuidLens(request), content).fold(
|
||||
{
|
||||
Response(BAD_REQUEST)
|
||||
},
|
||||
|
||||
@ -32,7 +32,7 @@ class NoteController(
|
||||
|
||||
val markdownForm = request.form("markdown") ?: ""
|
||||
|
||||
return noteService.create(loggedInUser.userId, markdownForm).fold(
|
||||
return noteService.create(loggedInUser, markdownForm).fold(
|
||||
{
|
||||
val html = when (it) {
|
||||
MissingMeta -> view.noteEditor(
|
||||
@ -112,7 +112,7 @@ class NoteController(
|
||||
|
||||
val markdownForm = request.form("markdown") ?: ""
|
||||
|
||||
return noteService.update(loggedInUser.userId, note.uuid, markdownForm).fold(
|
||||
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
|
||||
{
|
||||
val html = when (it) {
|
||||
MissingMeta -> view.noteEditor(
|
||||
|
||||
@ -44,7 +44,7 @@ class ApiRoutes(
|
||||
"/" bind POST to transaction.then(::createNote),
|
||||
"/search" bind POST to ::search,
|
||||
"/{uuid}" bind GET to ::note,
|
||||
"/{uuid}" bind PUT to transaction.then(::note),
|
||||
"/{uuid}" bind PUT to transaction.then(::update),
|
||||
)
|
||||
).withBasePath("/notes")
|
||||
}
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
package be.simplenotes.domain.security
|
||||
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.owasp.html.HtmlChangeListener
|
||||
import org.owasp.html.HtmlPolicyBuilder
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.inject.Singleton
|
||||
|
||||
internal object HtmlSanitizer {
|
||||
@Singleton
|
||||
class HtmlSanitizer {
|
||||
private val htmlPolicy = HtmlPolicyBuilder()
|
||||
.allowElements("a")
|
||||
.allowCommonBlockElements()
|
||||
@ -16,5 +21,18 @@ internal object HtmlSanitizer {
|
||||
.requireRelNofollowOnLinks()
|
||||
.toFactory()!!
|
||||
|
||||
fun sanitize(unsafeHtml: String) = htmlPolicy.sanitize(unsafeHtml)!!
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
private val htmlChangeListener = object : HtmlChangeListener<LoggedInUser> {
|
||||
override fun discardedTag(context: LoggedInUser?, elementName: String) {
|
||||
logger.warn("Discarded tag $elementName for user $context")
|
||||
}
|
||||
|
||||
override fun discardedAttributes(context: LoggedInUser?, tagName: String, vararg attributeNames: String) {
|
||||
logger.warn("Discarded attributes ${attributeNames.contentToString()} on tag $tagName for user $context")
|
||||
}
|
||||
}
|
||||
|
||||
fun sanitize(userId: LoggedInUser, unsafeHtml: String) =
|
||||
htmlPolicy.sanitize(unsafeHtml, htmlChangeListener, userId)!!
|
||||
}
|
||||
|
||||
@ -8,10 +8,13 @@ import be.simplenotes.persistance.repositories.NoteRepository
|
||||
import be.simplenotes.persistance.repositories.UserRepository
|
||||
import be.simplenotes.search.NoteSearcher
|
||||
import be.simplenotes.search.SearchTerms
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.types.Note
|
||||
import be.simplenotes.types.PersistedNote
|
||||
import be.simplenotes.types.PersistedNoteMetadata
|
||||
import java.util.*
|
||||
import javax.annotation.PostConstruct
|
||||
import javax.annotation.PreDestroy
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@ -20,25 +23,26 @@ class NoteService(
|
||||
private val noteRepository: NoteRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val searcher: NoteSearcher,
|
||||
private val htmlSanitizer: HtmlSanitizer,
|
||||
) {
|
||||
|
||||
fun create(userId: Int, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote> {
|
||||
fun create(user: LoggedInUser, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote> {
|
||||
val persistedNote = !markdownConverter.renderDocument(markdownText)
|
||||
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
|
||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
||||
.map { noteRepository.create(userId, it) }
|
||||
.map { noteRepository.create(user.userId, it) }
|
||||
|
||||
searcher.indexNote(userId, persistedNote)
|
||||
searcher.indexNote(user.userId, persistedNote)
|
||||
persistedNote
|
||||
}
|
||||
|
||||
fun update(userId: Int, uuid: UUID, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote?> {
|
||||
fun update(user: LoggedInUser, uuid: UUID, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote?> {
|
||||
val persistedNote = !markdownConverter.renderDocument(markdownText)
|
||||
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
|
||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
||||
.map { noteRepository.update(userId, uuid, it) }
|
||||
.map { noteRepository.update(user.userId, uuid, it) }
|
||||
|
||||
persistedNote?.let { searcher.updateIndex(userId, it) }
|
||||
persistedNote?.let { searcher.updateIndex(user.userId, it) }
|
||||
persistedNote
|
||||
}
|
||||
|
||||
@ -78,7 +82,9 @@ class NoteService(
|
||||
|
||||
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
|
||||
|
||||
@PostConstruct
|
||||
fun indexAll() {
|
||||
dropAllIndexes()
|
||||
val userIds = userRepository.findAll()
|
||||
userIds.forEach { id ->
|
||||
val notes = noteRepository.findAllDetails(id)
|
||||
@ -88,6 +94,7 @@ class NoteService(
|
||||
|
||||
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
|
||||
|
||||
@PreDestroy
|
||||
fun dropAllIndexes() = searcher.dropAll()
|
||||
|
||||
fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
package be.simplenotes.domain.usecases.markdown
|
||||
|
||||
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
|
||||
import com.vladsch.flexmark.html.HtmlRenderer
|
||||
import com.vladsch.flexmark.parser.Parser
|
||||
import com.vladsch.flexmark.util.data.MutableDataSet
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Factory
|
||||
class FlexmarkFactory {
|
||||
|
||||
@Singleton
|
||||
fun parser(options: MutableDataSet) = Parser.builder(options).build()
|
||||
|
||||
@Singleton
|
||||
fun htmlRenderer(options: MutableDataSet) = HtmlRenderer.builder(options).build()
|
||||
|
||||
@Singleton
|
||||
fun options() = MutableDataSet().apply {
|
||||
set(Parser.EXTENSIONS, listOf(TaskListExtension.create()))
|
||||
set(TaskListExtension.TIGHT_ITEM_CLASS, "")
|
||||
set(TaskListExtension.LOOSE_ITEM_CLASS, "")
|
||||
set(
|
||||
TaskListExtension.ITEM_DONE_MARKER,
|
||||
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" /> """
|
||||
)
|
||||
set(
|
||||
TaskListExtension.ITEM_NOT_DONE_MARKER,
|
||||
"""<input type="checkbox" disabled="disabled" readonly="readonly" /> """
|
||||
)
|
||||
set(HtmlRenderer.SOFT_BREAK, "<br>")
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,8 @@
|
||||
package be.simplenotes.domain.usecases.markdown
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.computations.either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import be.simplenotes.domain.validation.NoteValidations
|
||||
import be.simplenotes.types.NoteMetadata
|
||||
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
|
||||
import com.vladsch.flexmark.html.HtmlRenderer
|
||||
import com.vladsch.flexmark.parser.Parser
|
||||
import com.vladsch.flexmark.util.data.MutableDataSet
|
||||
import io.konform.validation.ValidationErrors
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import org.yaml.snakeyaml.parser.ParserException
|
||||
import org.yaml.snakeyaml.scanner.ScannerException
|
||||
import javax.inject.Singleton
|
||||
|
||||
sealed class MarkdownParsingError
|
||||
object MissingMeta : MarkdownParsingError()
|
||||
@ -23,63 +11,6 @@ class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingE
|
||||
|
||||
data class Document(val metadata: NoteMetadata, val html: String)
|
||||
|
||||
typealias MetaMdPair = Pair<String, String>
|
||||
|
||||
interface MarkdownConverter {
|
||||
fun renderDocument(input: String): Either<MarkdownParsingError, Document>
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class MarkdownConverterImpl : MarkdownConverter {
|
||||
private val yamlBoundPattern = "-{3}".toRegex()
|
||||
private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> {
|
||||
val split = input.split(yamlBoundPattern, 3)
|
||||
if (split.size < 3) return MissingMeta.left()
|
||||
return (split[1].trim() to split[2].trim()).right()
|
||||
}
|
||||
|
||||
private val yaml = Yaml()
|
||||
private fun parseMeta(input: String): Either<InvalidMeta, NoteMetadata> {
|
||||
val load: Map<String, Any> = try {
|
||||
yaml.load(input)
|
||||
} catch (e: ParserException) {
|
||||
return InvalidMeta.left()
|
||||
} catch (e: ScannerException) {
|
||||
return InvalidMeta.left()
|
||||
}
|
||||
|
||||
val title = when (val titleNode = load["title"]) {
|
||||
is String, is Number -> titleNode.toString()
|
||||
else -> return InvalidMeta.left()
|
||||
}
|
||||
|
||||
val tagsNode = load["tags"]
|
||||
val tags = if (tagsNode !is List<*>)
|
||||
emptyList()
|
||||
else
|
||||
tagsNode.map { it.toString() }
|
||||
|
||||
return NoteMetadata(title, tags).right()
|
||||
}
|
||||
|
||||
private val parser: Parser
|
||||
private val renderer: HtmlRenderer
|
||||
|
||||
init {
|
||||
val options = MutableDataSet()
|
||||
options.set(Parser.EXTENSIONS, listOf(TaskListExtension.create()))
|
||||
options.set(HtmlRenderer.SOFT_BREAK, "<br>")
|
||||
parser = Parser.builder(options).build()
|
||||
renderer = HtmlRenderer.builder(options).build()
|
||||
}
|
||||
|
||||
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
|
||||
|
||||
override fun renderDocument(input: String) = either.eager<MarkdownParsingError, Document> {
|
||||
val (meta, md) = !splitMetaFromDocument(input)
|
||||
val parsedMeta = !parseMeta(meta)
|
||||
!Either.fromNullable(NoteValidations.validateMetadata(parsedMeta)).swap()
|
||||
val html = renderMarkdown(md)
|
||||
Document(parsedMeta, html)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
package be.simplenotes.domain.usecases.markdown
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.computations.either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import be.simplenotes.domain.validation.NoteValidations
|
||||
import be.simplenotes.types.NoteMetadata
|
||||
import com.vladsch.flexmark.html.HtmlRenderer
|
||||
import com.vladsch.flexmark.parser.Parser
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import org.yaml.snakeyaml.parser.ParserException
|
||||
import org.yaml.snakeyaml.scanner.ScannerException
|
||||
import javax.inject.Singleton
|
||||
|
||||
private typealias MetaMdPair = Pair<String, String>
|
||||
|
||||
@Singleton
|
||||
internal class MarkdownConverterImpl(
|
||||
private val parser: Parser,
|
||||
private val renderer: HtmlRenderer,
|
||||
) : MarkdownConverter {
|
||||
private val yamlBoundPattern = "-{3}".toRegex()
|
||||
private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> {
|
||||
val split = input.split(yamlBoundPattern, 3)
|
||||
if (split.size < 3) return MissingMeta.left()
|
||||
return (split[1].trim() to split[2].trim()).right()
|
||||
}
|
||||
|
||||
private val yaml = Yaml()
|
||||
private fun parseMeta(input: String): Either<InvalidMeta, NoteMetadata> {
|
||||
val load: Map<String, Any> = try {
|
||||
yaml.load(input)
|
||||
} catch (e: ParserException) {
|
||||
return InvalidMeta.left()
|
||||
} catch (e: ScannerException) {
|
||||
return InvalidMeta.left()
|
||||
}
|
||||
|
||||
val title = when (val titleNode = load["title"]) {
|
||||
is String, is Number -> titleNode.toString()
|
||||
else -> return InvalidMeta.left()
|
||||
}
|
||||
|
||||
val tagsNode = load["tags"]
|
||||
val tags = if (tagsNode !is List<*>)
|
||||
emptyList()
|
||||
else
|
||||
tagsNode.map { it.toString() }
|
||||
|
||||
return NoteMetadata(title, tags).right()
|
||||
}
|
||||
|
||||
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
|
||||
|
||||
override fun renderDocument(input: String) = either.eager<MarkdownParsingError, Document> {
|
||||
val (meta, md) = !splitMetaFromDocument(input)
|
||||
val parsedMeta = !parseMeta(meta)
|
||||
!Either.fromNullable(NoteValidations.validateMetadata(parsedMeta)).swap()
|
||||
val html = renderMarkdown(md)
|
||||
Document(parsedMeta, html)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user