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 {
|
fun createNote(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
val content = noteContentLens(request)
|
val content = noteContentLens(request)
|
||||||
return noteService.create(loggedInUser.userId, content).fold(
|
return noteService.create(loggedInUser, content).fold(
|
||||||
{ Response(BAD_REQUEST) },
|
{ Response(BAD_REQUEST) },
|
||||||
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
|
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
|
||||||
)
|
)
|
||||||
@ -45,7 +45,7 @@ class ApiNoteController(
|
|||||||
|
|
||||||
fun update(request: Request, loggedInUser: LoggedInUser): Response {
|
fun update(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
val content = noteContentLens(request)
|
val content = noteContentLens(request)
|
||||||
return noteService.update(loggedInUser.userId, uuidLens(request), content).fold(
|
return noteService.update(loggedInUser, uuidLens(request), content).fold(
|
||||||
{
|
{
|
||||||
Response(BAD_REQUEST)
|
Response(BAD_REQUEST)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class NoteController(
|
|||||||
|
|
||||||
val markdownForm = request.form("markdown") ?: ""
|
val markdownForm = request.form("markdown") ?: ""
|
||||||
|
|
||||||
return noteService.create(loggedInUser.userId, markdownForm).fold(
|
return noteService.create(loggedInUser, markdownForm).fold(
|
||||||
{
|
{
|
||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
MissingMeta -> view.noteEditor(
|
MissingMeta -> view.noteEditor(
|
||||||
@ -112,7 +112,7 @@ class NoteController(
|
|||||||
|
|
||||||
val markdownForm = request.form("markdown") ?: ""
|
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) {
|
val html = when (it) {
|
||||||
MissingMeta -> view.noteEditor(
|
MissingMeta -> view.noteEditor(
|
||||||
|
|||||||
@ -44,7 +44,7 @@ class ApiRoutes(
|
|||||||
"/" bind POST to transaction.then(::createNote),
|
"/" bind POST to transaction.then(::createNote),
|
||||||
"/search" bind POST to ::search,
|
"/search" bind POST to ::search,
|
||||||
"/{uuid}" bind GET to ::note,
|
"/{uuid}" bind GET to ::note,
|
||||||
"/{uuid}" bind PUT to transaction.then(::note),
|
"/{uuid}" bind PUT to transaction.then(::update),
|
||||||
)
|
)
|
||||||
).withBasePath("/notes")
|
).withBasePath("/notes")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
package be.simplenotes.domain.security
|
package be.simplenotes.domain.security
|
||||||
|
|
||||||
|
import be.simplenotes.types.LoggedInUser
|
||||||
|
import org.owasp.html.HtmlChangeListener
|
||||||
import org.owasp.html.HtmlPolicyBuilder
|
import org.owasp.html.HtmlPolicyBuilder
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
internal object HtmlSanitizer {
|
@Singleton
|
||||||
|
class HtmlSanitizer {
|
||||||
private val htmlPolicy = HtmlPolicyBuilder()
|
private val htmlPolicy = HtmlPolicyBuilder()
|
||||||
.allowElements("a")
|
.allowElements("a")
|
||||||
.allowCommonBlockElements()
|
.allowCommonBlockElements()
|
||||||
@ -16,5 +21,18 @@ internal object HtmlSanitizer {
|
|||||||
.requireRelNofollowOnLinks()
|
.requireRelNofollowOnLinks()
|
||||||
.toFactory()!!
|
.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.persistance.repositories.UserRepository
|
||||||
import be.simplenotes.search.NoteSearcher
|
import be.simplenotes.search.NoteSearcher
|
||||||
import be.simplenotes.search.SearchTerms
|
import be.simplenotes.search.SearchTerms
|
||||||
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.types.Note
|
import be.simplenotes.types.Note
|
||||||
import be.simplenotes.types.PersistedNote
|
import be.simplenotes.types.PersistedNote
|
||||||
import be.simplenotes.types.PersistedNoteMetadata
|
import be.simplenotes.types.PersistedNoteMetadata
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.annotation.PostConstruct
|
||||||
|
import javax.annotation.PreDestroy
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -20,25 +23,26 @@ class NoteService(
|
|||||||
private val noteRepository: NoteRepository,
|
private val noteRepository: NoteRepository,
|
||||||
private val userRepository: UserRepository,
|
private val userRepository: UserRepository,
|
||||||
private val searcher: NoteSearcher,
|
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)
|
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 { 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
|
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)
|
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 { 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
|
persistedNote
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +82,9 @@ class NoteService(
|
|||||||
|
|
||||||
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
|
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
fun indexAll() {
|
fun indexAll() {
|
||||||
|
dropAllIndexes()
|
||||||
val userIds = userRepository.findAll()
|
val userIds = userRepository.findAll()
|
||||||
userIds.forEach { id ->
|
userIds.forEach { id ->
|
||||||
val notes = noteRepository.findAllDetails(id)
|
val notes = noteRepository.findAllDetails(id)
|
||||||
@ -88,6 +94,7 @@ class NoteService(
|
|||||||
|
|
||||||
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
|
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
fun dropAllIndexes() = searcher.dropAll()
|
fun dropAllIndexes() = searcher.dropAll()
|
||||||
|
|
||||||
fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)
|
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
|
package be.simplenotes.domain.usecases.markdown
|
||||||
|
|
||||||
import arrow.core.Either
|
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 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 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
|
sealed class MarkdownParsingError
|
||||||
object MissingMeta : MarkdownParsingError()
|
object MissingMeta : MarkdownParsingError()
|
||||||
@ -23,63 +11,6 @@ class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingE
|
|||||||
|
|
||||||
data class Document(val metadata: NoteMetadata, val html: String)
|
data class Document(val metadata: NoteMetadata, val html: String)
|
||||||
|
|
||||||
typealias MetaMdPair = Pair<String, String>
|
|
||||||
|
|
||||||
interface MarkdownConverter {
|
interface MarkdownConverter {
|
||||||
fun renderDocument(input: String): Either<MarkdownParsingError, Document>
|
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