From f0049a4d93d325df94126b99cd5a8ea0d5d127a1 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Tue, 10 Sep 2024 12:43:37 +0200 Subject: [PATCH] make it work and make it better added html, redirects, did you mean, maybe other stuff I forgot --- pom.xml | 7 ++ .../java/eu/m724/talkpages/IndexResource.java | 1 + src/main/java/eu/m724/talkpages/Startup.java | 42 ++++++++ .../eu/m724/talkpages/TemplateExtensions.java | 24 +++++ .../eu/m724/talkpages/orm/entity/Page.java | 16 ++++ .../talkpages/orm/entity/PageRevision.java | 16 +++- .../m724/talkpages/page/ActionResource.java | 95 ------------------- .../eu/m724/talkpages/page/EditResource.java | 12 ++- .../m724/talkpages/page/HistoryResource.java | 2 +- .../eu/m724/talkpages/page/PageResource.java | 26 ++++- .../talkpages/page/action/ActionResource.java | 91 ++++++++++++++++++ .../talkpages/page/action/ActionService.java | 52 ++++++++++ .../templates/EditResource/create.html | 6 +- .../templates/EditResource/edit.html | 6 +- .../templates/HistoryResource/history.html | 4 + .../templates/IndexResource/index.html | 12 +-- .../templates/PageResource/notFound.html | 12 ++- .../templates/PageResource/page.html | 13 ++- 18 files changed, 318 insertions(+), 119 deletions(-) create mode 100644 src/main/java/eu/m724/talkpages/Startup.java create mode 100644 src/main/java/eu/m724/talkpages/TemplateExtensions.java delete mode 100644 src/main/java/eu/m724/talkpages/page/ActionResource.java create mode 100644 src/main/java/eu/m724/talkpages/page/action/ActionResource.java create mode 100644 src/main/java/eu/m724/talkpages/page/action/ActionService.java diff --git a/pom.xml b/pom.xml index 9724c4b..fc7d427 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,13 @@ + + + org.jsoup + jsoup + 1.18.1 + + io.quarkus quarkus-rest diff --git a/src/main/java/eu/m724/talkpages/IndexResource.java b/src/main/java/eu/m724/talkpages/IndexResource.java index a33e3f5..2c2eab8 100644 --- a/src/main/java/eu/m724/talkpages/IndexResource.java +++ b/src/main/java/eu/m724/talkpages/IndexResource.java @@ -12,6 +12,7 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.resteasy.reactive.RestResponse; import org.jboss.resteasy.reactive.RestResponse.Status; diff --git a/src/main/java/eu/m724/talkpages/Startup.java b/src/main/java/eu/m724/talkpages/Startup.java new file mode 100644 index 0000000..b0ce5fa --- /dev/null +++ b/src/main/java/eu/m724/talkpages/Startup.java @@ -0,0 +1,42 @@ +package eu.m724.talkpages; + +import eu.m724.talkpages.orm.entity.Account; +import eu.m724.talkpages.orm.entity.Page; +import eu.m724.talkpages.orm.entity.PageRevision; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; + +@Singleton +public class Startup { + @Inject + LaunchMode launchMode; + + @Transactional + public void examplePage(@Observes StartupEvent ignoredEvent) { + Account account = new Account("System"); + account.persistAndFlush(); + + addPage(account, "TalkPages", "

A website where the users collaboratively create content

"); + addPage(account, "Talkpages", "ambiguous for [TalkPages]"); + addPage(account, "TP", "@TalkPages"); + } + + @Transactional + public void addPage(Account account, String title, String content) { + Page page = new Page(title); + + PageRevision revision = new PageRevision(page); + revision.author = account; + revision.content = content; + revision.delta = revision.content.length(); + + page.setLatestRevision(revision); + account.revisions.add(revision); + + revision.persistAndFlush(); + } +} diff --git a/src/main/java/eu/m724/talkpages/TemplateExtensions.java b/src/main/java/eu/m724/talkpages/TemplateExtensions.java new file mode 100644 index 0000000..8133c66 --- /dev/null +++ b/src/main/java/eu/m724/talkpages/TemplateExtensions.java @@ -0,0 +1,24 @@ +package eu.m724.talkpages; + +import io.quarkus.qute.TemplateExtension; +import jakarta.enterprise.context.ApplicationScoped; +import org.jsoup.Jsoup; +import org.jsoup.safety.Safelist; + +@ApplicationScoped +public class TemplateExtensions { + // disallowing images because abuse risk and a text only site is refreshing + private static final Safelist safelist = Safelist.relaxed().removeTags("img"); + + /** + * Sanitize HTML. This means remove <script> and stuff + * + * @param content the HTML content + * @return sanitized HTML content + */ + @TemplateExtension + public static String sanitizeContent(String content) { + // I was thinking maybe we could put somewhere that it was sanitized + return Jsoup.clean(content, safelist); + } +} diff --git a/src/main/java/eu/m724/talkpages/orm/entity/Page.java b/src/main/java/eu/m724/talkpages/orm/entity/Page.java index 946c9b7..5232509 100644 --- a/src/main/java/eu/m724/talkpages/orm/entity/Page.java +++ b/src/main/java/eu/m724/talkpages/orm/entity/Page.java @@ -3,6 +3,7 @@ package eu.m724.talkpages.orm.entity; import io.quarkus.hibernate.orm.panache.PanacheEntity; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import jakarta.persistence.*; +import jakarta.transaction.Transactional; import org.hibernate.annotations.GenericGenerator; import java.util.ArrayList; @@ -10,6 +11,12 @@ import java.util.List; @Entity public class Page extends PanacheEntityBase { + public Page() {} + + public Page(String title) { + this.title = title; + } + @Id @GeneratedValue(generator = "title-path") @GenericGenerator(name = "title-path", strategy = "eu.m724.talkpages.orm.generator.PathGenerator") @@ -33,4 +40,13 @@ public class Page extends PanacheEntityBase { public static Page findByTitle(String title) { return find("title", title).firstResult(); } + + public static List findByTitleIgnoreCase(String title) { + return find("lower(title) = ?1", title.toLowerCase()).list(); + } + + public void setLatestRevision(PageRevision pageRevision) { + revisions.add(pageRevision); + latestRevision = pageRevision; + } } diff --git a/src/main/java/eu/m724/talkpages/orm/entity/PageRevision.java b/src/main/java/eu/m724/talkpages/orm/entity/PageRevision.java index 58fd1f9..a834a90 100644 --- a/src/main/java/eu/m724/talkpages/orm/entity/PageRevision.java +++ b/src/main/java/eu/m724/talkpages/orm/entity/PageRevision.java @@ -11,7 +11,19 @@ import java.time.LocalDateTime; uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"}) ) public class PageRevision extends PanacheEntity { - @ManyToOne + public PageRevision() {} + + public PageRevision(Page page) { + long index = 1; + if (page.latestRevision != null) { + index = page.latestRevision.index + 1; + } + + this.index = index; + this.page = page; + } + + @ManyToOne(cascade = CascadeType.ALL) public Page page; /** @@ -29,7 +41,7 @@ public class PageRevision extends PanacheEntity { /** * The author of the revision */ - @ManyToOne + @ManyToOne(cascade = CascadeType.ALL) public Account author; /** diff --git a/src/main/java/eu/m724/talkpages/page/ActionResource.java b/src/main/java/eu/m724/talkpages/page/ActionResource.java deleted file mode 100644 index d1c5161..0000000 --- a/src/main/java/eu/m724/talkpages/page/ActionResource.java +++ /dev/null @@ -1,95 +0,0 @@ -package eu.m724.talkpages.page; - -import eu.m724.talkpages.RedirectService; -import eu.m724.talkpages.orm.entity.Account; -import eu.m724.talkpages.orm.entity.Page; -import eu.m724.talkpages.orm.entity.PageRevision; -import jakarta.inject.Inject; -import jakarta.transaction.Transactional; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.Response; -import org.jboss.resteasy.reactive.RestResponse; - -import java.net.URI; -import java.util.Random; - -@Path("/action") -@Consumes(MediaType.APPLICATION_FORM_URLENCODED) -@Produces(MediaType.TEXT_PLAIN) -public class ActionResource { - @Inject - RedirectService redirectService; - - @POST - @Path("/create") - @Transactional // TODO make this in a service or whatever is it called - public Response create(MultivaluedMap formData) { - String title = formData.getFirst("title"); - String content = formData.getFirst("content"); - - if (title.contains(":")) { - return Response.temporaryRedirect(URI.create("/edit/" + title)).status(Response.Status.SEE_OTHER).build(); - } - - if (Page.findByTitle(title) != null) { - return Response.status(Response.Status.CONFLICT).build(); - } - - Page page = new Page(); - page.title = title; - - Account account = new Account("test user #" + new Random().nextInt(0, 1000)); - - PageRevision revision = new PageRevision(); - revision.author = account; - revision.content = content; - revision.delta = content.length(); - - revision.page = page; - page.revisions.add(revision); - page.latestRevision = revision; - - account.persist(); - page.persist(); - revision.persistAndFlush(); // this stalled me for a few hours... should move that to @Transactional - - return redirectService.page(page).status(RestResponse.Status.SEE_OTHER).build(); - } - - @POST - @Path("/edit") - @Transactional // TODO make this in a service or whatever is it called - public Response edit(MultivaluedMap formData) { - String title = formData.getFirst("title"); - String content = formData.getFirst("content"); - - Page page = Page.findByTitle(title); - - if (Page.findByTitle(title) == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - - Account account = new Account("test user #" + new Random().nextInt(0, 1000)); - - PageRevision revision = new PageRevision(); - revision.index = page.latestRevision.index + 1; - revision.author = account; - revision.content = content; - revision.delta = content.length() - page.latestRevision.content.length(); // TODO optimize - - revision.page = page; - page.revisions.add(revision); - page.latestRevision = revision; - - account.persist(); - page.persist(); - revision.persistAndFlush(); // this stalled me for a few hours - - return redirectService.page(page).status(RestResponse.Status.SEE_OTHER).build(); - } -} diff --git a/src/main/java/eu/m724/talkpages/page/EditResource.java b/src/main/java/eu/m724/talkpages/page/EditResource.java index e6f4638..b6ef196 100644 --- a/src/main/java/eu/m724/talkpages/page/EditResource.java +++ b/src/main/java/eu/m724/talkpages/page/EditResource.java @@ -29,15 +29,19 @@ public class EditResource { } @GET - @Path("/{title}") - public Response editPage(@PathParam("title") String title) { + @Path("/{title:.+}") + public Response editPage(@PathParam("title") String title, @CookieParam("prefilledContent") String prefilledContent) { Page page = Page.findByTitle(title); if (page == null) - return Response.status(Response.Status.NOT_FOUND).entity(Templates.create(title, "198.51.100.42")).build(); + return Response.ok().entity(Templates.create(title, "198.51.100.42")).build(); + + if (prefilledContent == null || prefilledContent.isBlank()) { + prefilledContent = page.latestRevision.content; + } // TODO check for permissions - return Response.ok().entity(Templates.edit(page, page.latestRevision.content, "198.51.100.42")).build(); + return Response.ok().entity(Templates.edit(page, prefilledContent, "198.51.100.42")).build(); } } diff --git a/src/main/java/eu/m724/talkpages/page/HistoryResource.java b/src/main/java/eu/m724/talkpages/page/HistoryResource.java index e8db1fd..5d727f5 100644 --- a/src/main/java/eu/m724/talkpages/page/HistoryResource.java +++ b/src/main/java/eu/m724/talkpages/page/HistoryResource.java @@ -41,7 +41,7 @@ public class HistoryResource { if (page == null) return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(title, redirectService.titleEncoded(title))).build(); // TODO replace with own template - List revisions = page.revisions; + List revisions = page.revisions.reversed(); return Response.ok().entity(Templates.history(page, revisions)).build(); } diff --git a/src/main/java/eu/m724/talkpages/page/PageResource.java b/src/main/java/eu/m724/talkpages/page/PageResource.java index 1d11add..c9b66bb 100644 --- a/src/main/java/eu/m724/talkpages/page/PageResource.java +++ b/src/main/java/eu/m724/talkpages/page/PageResource.java @@ -1,5 +1,6 @@ package eu.m724.talkpages.page; +import eu.m724.talkpages.TemplateExtensions; import eu.m724.talkpages.RedirectService; import eu.m724.talkpages.orm.entity.Page; import eu.m724.talkpages.orm.entity.PageRevision; @@ -10,6 +11,11 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + @Path("/page") @Produces(MediaType.TEXT_HTML) public class PageResource { @@ -20,7 +26,7 @@ public class PageResource { public static class Templates { public static native TemplateInstance page(Page page, PageRevision revision, boolean old); public static native TemplateInstance revisionNotFound(Page page, int revisionId); - public static native TemplateInstance notFound(String title); + public static native TemplateInstance notFound(String title, List suggestions); } @GET @@ -31,15 +37,25 @@ public class PageResource { @GET @Path("/{pageId}") - public Response viewPage(@PathParam("pageId") String pageId, @QueryParam("revision") Integer revisionId) { + public Response viewPage(@PathParam("pageId") String pageId, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) { System.out.println(pageId); Page page = Page.findByTitle(pageId); - if (page == null) - return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(pageId)).build(); - + if (page == null) { + List suggestions = Page.findByTitleIgnoreCase(pageId); + if (suggestions.size() == 1) { + return redirectService.page(suggestions.getFirst()).build(); + } + return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(pageId, suggestions)).build(); + } if (revisionId == null) { + if (page.latestRevision.content.startsWith("@")) { + String target = page.latestRevision.content.substring(1); + if (redirectFrom == null) + redirectFrom = pageId; + return Response.temporaryRedirect(URI.create("/page/" + redirectService.titleEncoded(target) + "?redirectFrom=" + URLEncoder.encode(redirectFrom, StandardCharsets.UTF_8))).build(); // TODO I could really reduce some of this + } return Response.ok().entity(Templates.page(page, page.latestRevision, false)).build(); } else { System.out.printf("History for page: %s %d\n", page.title, page.latestRevision.index); diff --git a/src/main/java/eu/m724/talkpages/page/action/ActionResource.java b/src/main/java/eu/m724/talkpages/page/action/ActionResource.java new file mode 100644 index 0000000..df8a739 --- /dev/null +++ b/src/main/java/eu/m724/talkpages/page/action/ActionResource.java @@ -0,0 +1,91 @@ +package eu.m724.talkpages.page.action; + +import eu.m724.talkpages.RedirectService; +import eu.m724.talkpages.orm.entity.Account; +import eu.m724.talkpages.orm.entity.Page; +import eu.m724.talkpages.orm.entity.PageRevision; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.reactive.RestResponse; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Random; + +@Path("/action") +@Consumes(MediaType.APPLICATION_FORM_URLENCODED) +@Produces(MediaType.TEXT_PLAIN) +public class ActionResource { + @Inject + ActionService actionService; + + @Inject + RedirectService redirectService; + + @POST + @Path("/create") + @Transactional // TODO make this in a service or whatever is it called + public Response create(MultivaluedMap formData) { + String title = formData.getFirst("title"); + String content = formData.getFirst("content"); + + Account account = new Account("test user #" + new Random().nextInt(10000)); + account.persistAndFlush(); + + try { + Page page = actionService.createPage(title, content, account); + return redirectService.page(page) + .cookie(new NewCookie.Builder("prefilledContent").value("").maxAge(0).build()) + .status(RestResponse.Status.SEE_OTHER).build(); + } catch (IllegalArgumentException e) { // TODO I could reduce all this code + // illegal title + String encodedMessage = URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8); + return Response + .temporaryRedirect(URI.create("/edit/" + title + "?errorType=1&error=" + encodedMessage)) + .cookie(new NewCookie.Builder("prefilledContent").path("/edit/" + title).value(content).build()) // TODO find a better way + .status(Response.Status.SEE_OTHER).build(); + } catch (IllegalStateException e) { + // page already exists + String encodedMessage = URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8); + return Response + .temporaryRedirect(URI.create("/edit/" + title + "?errorType=2&error=" + encodedMessage)) + .cookie(new NewCookie.Builder("prefilledContent").path("/edit/" + title).value(content).build()) + .status(Response.Status.SEE_OTHER).build(); + } catch (Exception e) { + e.printStackTrace(); + return Response + .temporaryRedirect(URI.create("/edit/" + title + "?errorType=0&error=Server+error")) + .cookie(new NewCookie.Builder("prefilledContent").path("/edit/" + title).value(content).build()) + .status(Response.Status.SEE_OTHER).build(); + + } + } + + @POST + @Path("/edit") + @Transactional // TODO make this in a service or whatever is it called + public Response edit(MultivaluedMap formData) { + String title = formData.getFirst("title"); + String content = formData.getFirst("content"); + Account account = new Account("test user #" + new Random().nextInt(0, 1000)); + + Page page = Page.findByTitle(title); + + if (Page.findByTitle(title) == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + page = actionService.editPage(page, content, account); + + return redirectService.page(page).status(RestResponse.Status.SEE_OTHER).build(); + } +} diff --git a/src/main/java/eu/m724/talkpages/page/action/ActionService.java b/src/main/java/eu/m724/talkpages/page/action/ActionService.java new file mode 100644 index 0000000..d3beb58 --- /dev/null +++ b/src/main/java/eu/m724/talkpages/page/action/ActionService.java @@ -0,0 +1,52 @@ +package eu.m724.talkpages.page.action; + +import eu.m724.talkpages.orm.entity.Account; +import eu.m724.talkpages.orm.entity.Page; +import eu.m724.talkpages.orm.entity.PageRevision; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; + +@ApplicationScoped +public class ActionService { + @Transactional + Page createPage(String title, String content, Account account) { + // title and content is sanitized so only prohibit if necessary + if (title.contains("/")) { + throw new IllegalArgumentException("Title cannot contain slashes (/). Those are used for sub-pages."); + } else if (Page.findByTitle(title) != null) { + throw new IllegalStateException("Page already exists, I made you edit it."); + } + + Page page = new Page(title); + page.persist(); + + PageRevision revision = new PageRevision(page); + revision.author = account; + revision.content = content; + revision.delta = content.length(); + + page.setLatestRevision(revision); + account.revisions.add(revision); + + revision.persistAndFlush(); + + return page; + } + + @Transactional + Page editPage(Page page, String newContent, Account account) { + // constraints are not checked + + PageRevision revision = new PageRevision(page); + revision.author = account; + revision.content = newContent; + revision.delta = newContent.length() - page.latestRevision.content.length(); // TODO optimize + + page.setLatestRevision(revision); + account.revisions.add(revision); + + revision.persistAndFlush(); + + return page; + } +} diff --git a/src/main/resources/templates/EditResource/create.html b/src/main/resources/templates/EditResource/create.html index c25240b..b543206 100644 --- a/src/main/resources/templates/EditResource/create.html +++ b/src/main/resources/templates/EditResource/create.html @@ -10,4 +10,8 @@
- \ No newline at end of file + + +{#if http:param("error") != null} +

Error: {http:param("error")}

+{/if} \ No newline at end of file diff --git a/src/main/resources/templates/EditResource/edit.html b/src/main/resources/templates/EditResource/edit.html index 0f04b2e..1155b54 100644 --- a/src/main/resources/templates/EditResource/edit.html +++ b/src/main/resources/templates/EditResource/edit.html @@ -7,4 +7,8 @@
- \ No newline at end of file + + +{#if http:param("error") != null} +

Error: {http:param("error")}

+{/if} \ No newline at end of file diff --git a/src/main/resources/templates/HistoryResource/history.html b/src/main/resources/templates/HistoryResource/history.html index 14180fa..5b92066 100644 --- a/src/main/resources/templates/HistoryResource/history.html +++ b/src/main/resources/templates/HistoryResource/history.html @@ -1,7 +1,11 @@

History of {page.title}

{#for revision in revisions} +{#if page.latestRevision == revision} +#{revision.index} ({revision.delta}) {revision.timestamp.toString()} by {revision.author.name} +{#else} #{revision.index} ({revision.delta}) {revision.timestamp.toString()} by {revision.author.name} +{/if}
{/for} diff --git a/src/main/resources/templates/IndexResource/index.html b/src/main/resources/templates/IndexResource/index.html index 772bf47..0b84d85 100644 --- a/src/main/resources/templates/IndexResource/index.html +++ b/src/main/resources/templates/IndexResource/index.html @@ -1,12 +1,10 @@ -

Search

- - -
- + +
- \ No newline at end of file + + +Running TalkPages version {config:["quarkus.application.version"]} \ No newline at end of file diff --git a/src/main/resources/templates/PageResource/notFound.html b/src/main/resources/templates/PageResource/notFound.html index f29cb1c..1e2970d 100644 --- a/src/main/resources/templates/PageResource/notFound.html +++ b/src/main/resources/templates/PageResource/notFound.html @@ -1,6 +1,16 @@

{title}

-There is no such article. +

There is no such article.

+ +{#if !suggestions.isEmpty()} +

Are you looking for:

+ +

Actions:

+{/if}
  • Create
  • diff --git a/src/main/resources/templates/PageResource/page.html b/src/main/resources/templates/PageResource/page.html index 78a2d49..031c88a 100644 --- a/src/main/resources/templates/PageResource/page.html +++ b/src/main/resources/templates/PageResource/page.html @@ -1,13 +1,22 @@ +{#if http:param("redirectFrom") != null} +

    Redirected from {http:param("redirectFrom")}

    +{/if} + {#if old} +{#if page.latestRevision != revision}

    You are viewing an outdated revision #{revision.index} of this page from {revision.timestamp.toString()}, authored by {revision.author.name}. - Go back to current version +
    + See current version

    +{#else} +

    This is the current revision #{revision.index} authored by {revision.author.name}.

    +{/if} {/if}

    {page.title}

    -{revision.content} +{revision.content.sanitizeContent.raw}