Compare commits
No commits in common. "6197b4ec9ae22ba48ce28b4676608c8c2837e2b3" and "6a11f7ec6ccf6b5b4175ff6d867d24edaa576a66" have entirely different histories.
6197b4ec9a
...
6a11f7ec6c
14 changed files with 48 additions and 170 deletions
|
@ -22,6 +22,9 @@ public class Startup {
|
||||||
@ConfigProperty(name = "talkpages.systemUser.name")
|
@ConfigProperty(name = "talkpages.systemUser.name")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "talkpages.createExamplePage")
|
||||||
|
private boolean createExamplePage;
|
||||||
|
|
||||||
void installStaticRoute(@Observes StartupEvent startupEvent, Router router) {
|
void installStaticRoute(@Observes StartupEvent startupEvent, Router router) {
|
||||||
router.route()
|
router.route()
|
||||||
.path("/static/*")
|
.path("/static/*")
|
||||||
|
@ -30,7 +33,7 @@ public class Startup {
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void examplePage(@Observes StartupEvent ignoredEvent) {
|
public void examplePage(@Observes StartupEvent ignoredEvent) {
|
||||||
if (Account.findByName(username) != null) {
|
if (Account.findById(1) != null) {
|
||||||
// system user exists so assuming this is not the first run
|
// system user exists so assuming this is not the first run
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -40,18 +43,15 @@ public class Startup {
|
||||||
Account account = new Account(username);
|
Account account = new Account(username);
|
||||||
account.persistAndFlush();
|
account.persistAndFlush();
|
||||||
|
|
||||||
Page talkPagesPage = addPage(account, "TalkPages", "<p>A website where the users collaboratively create content</p><ul><li><a href=\"https://git.m724.eu/Minecon724/talkpages\">Source code</a></li></ul>");
|
if (createExamplePage) {
|
||||||
|
addPage(account, "TalkPages", "<p>A website where the users collaboratively create content</p><ul><li><a href=\"https://git.m724.eu/Minecon724/talkpages\">Source code</a></li></ul>");
|
||||||
addPage(account, "Talkpages", "ambiguous for [TalkPages]");
|
addPage(account, "Talkpages", "ambiguous for [TalkPages]");
|
||||||
addPage(account, "TP", "@TalkPages");
|
addPage(account, "TP", "@TalkPages");
|
||||||
|
}
|
||||||
Page tosPage = addPage(account, "Terms of Service", "TODO");
|
|
||||||
addPage(account, "ToS", "@TalkPages/Terms of Service");
|
|
||||||
|
|
||||||
tosPage.setParentPage(talkPagesPage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Page addPage(Account account, String title, String content) {
|
public void addPage(Account account, String title, String content) {
|
||||||
Page page = new Page(title);
|
Page page = new Page(title);
|
||||||
PageRevision revision = new PageRevision(page, account, content);
|
PageRevision revision = new PageRevision(page, account, content);
|
||||||
|
|
||||||
|
@ -59,6 +59,6 @@ public class Startup {
|
||||||
page.persistAndFlush();
|
page.persistAndFlush();
|
||||||
|
|
||||||
account.getRevisions().add(revision);
|
account.getRevisions().add(revision);
|
||||||
return page;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ public class AuthService {
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
Session register(String username, String password) throws UsernameExistsException {
|
Session register(String username, String password) throws UsernameExistsException {
|
||||||
Account account = Account.findByName(username);
|
Account account = Account.findById(username);
|
||||||
|
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
throw new UsernameExistsException();
|
throw new UsernameExistsException();
|
||||||
|
@ -63,7 +63,7 @@ public class AuthService {
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
Session authenticate(String username, String password) throws InvalidCredentialsException {
|
Session authenticate(String username, String password) throws InvalidCredentialsException {
|
||||||
Account account = Account.findByName(username);
|
Account account = Account.findById(username);
|
||||||
|
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
throw new InvalidCredentialsException(false);
|
throw new InvalidCredentialsException(false);
|
||||||
|
|
|
@ -14,13 +14,8 @@ import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
// TODO consider moving away authentication to allow like multiple profiles on one account
|
// TODO consider moving away authentication to allow like multiple profiles on one account
|
||||||
// TODO also consider giving different, namespaced ids to system and addressed accounts
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
public class Account extends PanacheEntityBase {
|
||||||
indexes = @Index(name = "idx_name", columnList = "name"),
|
|
||||||
uniqueConstraints = @UniqueConstraint(columnNames = "name")
|
|
||||||
)
|
|
||||||
public class Account extends PanacheEntity {
|
|
||||||
private Account() {}
|
private Account() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,6 +46,7 @@ public class Account extends PanacheEntity {
|
||||||
|
|
||||||
// Columns
|
// Columns
|
||||||
|
|
||||||
|
@Id
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
private String slug;
|
private String slug;
|
||||||
|
@ -76,11 +72,4 @@ public class Account extends PanacheEntity {
|
||||||
public List<PageRevision> getRevisions() { return revisions; }
|
public List<PageRevision> getRevisions() { return revisions; }
|
||||||
|
|
||||||
public boolean isSystemAccount() { return password == null; }
|
public boolean isSystemAccount() { return password == null; }
|
||||||
|
|
||||||
|
|
||||||
// Operations
|
|
||||||
|
|
||||||
public static Account findByName(String name) {
|
|
||||||
return Account.find("name", name).firstResult();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,35 @@
|
||||||
package eu.m724.talkpages.orm.entity.content;
|
package eu.m724.talkpages.orm.entity.content;
|
||||||
|
|
||||||
import eu.m724.talkpages.page.action.NoSuchNamespaceException;
|
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.GenericGenerator;
|
||||||
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
indexes = @Index(name = "idx_title", columnList = "title"),
|
indexes = @Index(name = "idx_title", columnList = "title"),
|
||||||
uniqueConstraints = @UniqueConstraint(columnNames = {"title", "parentPage"})
|
uniqueConstraints = @UniqueConstraint(columnNames = "title")
|
||||||
)
|
)
|
||||||
public class Page extends PanacheEntity {
|
public class Page extends PanacheEntityBase {
|
||||||
private Page() {}
|
private Page() {}
|
||||||
|
|
||||||
public Page(String title) {
|
public Page(String title) {
|
||||||
|
// TODO maybe map space to _
|
||||||
|
this.slug = URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20");
|
||||||
this.title = title;
|
this.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Columns
|
// Columns
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private String slug;
|
||||||
|
|
||||||
|
// TODO is this really necessary
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@OneToOne
|
@OneToOne
|
||||||
|
@ -32,103 +38,21 @@ public class Page extends PanacheEntity {
|
||||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private List<PageRevision> revisions = new ArrayList<>();
|
private List<PageRevision> revisions = new ArrayList<>();
|
||||||
|
|
||||||
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}, mappedBy = "parentPage")
|
|
||||||
private List<Page> subpages = new ArrayList<>();
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "parentPage_id")
|
|
||||||
private Page parentPage = null;
|
|
||||||
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
|
|
||||||
@PreRemove
|
|
||||||
private void preRemove() {
|
|
||||||
subpages.forEach(page -> page.parentPage = null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
|
||||||
|
public String getSlug() { return slug; }
|
||||||
public String getTitle() { return title; }
|
public String getTitle() { return title; }
|
||||||
public PageRevision getLatestRevision() { return latestRevision; }
|
public PageRevision getLatestRevision() { return latestRevision; }
|
||||||
public List<PageRevision> getRevisions() { return revisions; }
|
public List<PageRevision> getRevisions() { return revisions; }
|
||||||
public List<Page> getSubpages() { return subpages; }
|
|
||||||
public Page getParentPage() { return parentPage; }
|
|
||||||
|
|
||||||
public void setLatestRevision(PageRevision pageRevision) {
|
public void setLatestRevision(PageRevision pageRevision) {
|
||||||
revisions.add(pageRevision);
|
revisions.add(pageRevision);
|
||||||
latestRevision = pageRevision;
|
latestRevision = pageRevision;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setParentPage(Page parentPage) {
|
|
||||||
// Remove from current parent, if any
|
|
||||||
if (this.parentPage != null)
|
|
||||||
parentPage.subpages.remove(this);
|
|
||||||
|
|
||||||
// Set the parent page and add as a child
|
|
||||||
this.parentPage = parentPage;
|
|
||||||
if (parentPage != null)
|
|
||||||
parentPage.getSubpages().add(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSlug() {
|
|
||||||
// TODO maybe map space to _
|
|
||||||
return URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chain of parents. The broadest (root) one is the first one.<br>
|
|
||||||
* The list is empty if there's no parents.
|
|
||||||
*
|
|
||||||
* @return chain of parents
|
|
||||||
*/
|
|
||||||
public List<Page> getChain() {
|
|
||||||
List<Page> parents = new ArrayList<>();
|
|
||||||
|
|
||||||
Page parent = this.getParentPage();
|
|
||||||
while (parent != null) {
|
|
||||||
parents.add(parent);
|
|
||||||
parent = parent.getParentPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
return parents.reversed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Operations
|
// Operations
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a {@link Page} by title. The title must not be encoded.<br>
|
|
||||||
* Separate namespaces with a slash (<code>/</code>)<br>
|
|
||||||
*
|
|
||||||
* @param title the title, not encoded
|
|
||||||
* @return page if found, else null
|
|
||||||
* @throws IllegalStateException if
|
|
||||||
*/
|
|
||||||
public static Page findByPath(String title) {
|
|
||||||
String[] parts = title.split("/", -1);
|
|
||||||
|
|
||||||
Page page = findByTitle(parts[0]);
|
|
||||||
|
|
||||||
for (int i=1; i<parts.length; i++) {
|
|
||||||
String part = parts[i];
|
|
||||||
|
|
||||||
// those checks apply to the previous iteration, but won't run on the last one
|
|
||||||
if (page == null)
|
|
||||||
break; // TODO maybe raise an error here
|
|
||||||
|
|
||||||
if (page.title.isBlank())
|
|
||||||
throw new IllegalStateException("Can't have empty title in the middle"); // TODO also enforce
|
|
||||||
|
|
||||||
|
|
||||||
page = page.getSubpages().stream()
|
|
||||||
.filter(p -> p.getTitle().equals(part))
|
|
||||||
.findFirst().orElse(null); // TODO optimize
|
|
||||||
}
|
|
||||||
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Page findByTitle(String title) {
|
public static Page findByTitle(String title) {
|
||||||
return find("title", title).firstResult();
|
return find("title", title).firstResult();
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
indexes = @Index(columnList = "page_id"),
|
indexes = @Index(columnList = "page_slug"),
|
||||||
uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"})
|
uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"})
|
||||||
)
|
)
|
||||||
public class PageRevision extends PanacheEntity {
|
public class PageRevision extends PanacheEntity {
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class HistoryResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{title:.+}")
|
@Path("/{title}")
|
||||||
public Response pageHistory(@PathParam("title") String title) {
|
public Response pageHistory(@PathParam("title") String title) {
|
||||||
Page page = Page.findByTitle(title);
|
Page page = Page.findByTitle(title);
|
||||||
|
|
||||||
|
|
|
@ -35,16 +35,16 @@ public class PageResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{title:.+}")
|
@Path("/{pageId}")
|
||||||
public Response viewPage(@PathParam("title") String title, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) {
|
public Response viewPage(@PathParam("pageId") String pageId, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) {
|
||||||
Page page = Page.findByPath(title);
|
Page page = Page.findByTitle(pageId);
|
||||||
|
|
||||||
if (page == null) {
|
if (page == null) {
|
||||||
List<Page> suggestions = Page.findByTitleIgnoreCase(title);
|
List<Page> suggestions = Page.findByTitleIgnoreCase(pageId);
|
||||||
if (suggestions.size() == 1) {
|
if (suggestions.size() == 1) {
|
||||||
return redirectService.page(suggestions.getFirst()).build();
|
return redirectService.page(suggestions.getFirst()).build();
|
||||||
}
|
}
|
||||||
return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(title, suggestions)).build();
|
return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(pageId, suggestions)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (revisionId == null) {
|
if (revisionId == null) {
|
||||||
|
@ -52,7 +52,7 @@ public class PageResource {
|
||||||
if (revision.getContent().startsWith("@")) {
|
if (revision.getContent().startsWith("@")) {
|
||||||
String target = revision.getContent().substring(1);
|
String target = revision.getContent().substring(1);
|
||||||
if (redirectFrom == null)
|
if (redirectFrom == null)
|
||||||
redirectFrom = title;
|
redirectFrom = pageId;
|
||||||
return Response.temporaryRedirect(URI.create("/page/" + redirectService.titleEncoded(target) + "?redirectFrom=" + URLEncoder.encode(redirectFrom, StandardCharsets.UTF_8))).build();
|
return Response.temporaryRedirect(URI.create("/page/" + redirectService.titleEncoded(target) + "?redirectFrom=" + URLEncoder.encode(redirectFrom, StandardCharsets.UTF_8))).build();
|
||||||
}
|
}
|
||||||
return Response.ok().entity(Templates.page(page, page.getLatestRevision(), false)).build();
|
return Response.ok().entity(Templates.page(page, page.getLatestRevision(), false)).build();
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package eu.m724.talkpages.page.action;
|
|
||||||
|
|
||||||
public class NoSuchNamespaceException extends RuntimeException {
|
|
||||||
public final String namespace;
|
|
||||||
|
|
||||||
public NoSuchNamespaceException(String namespace) {
|
|
||||||
this.namespace = namespace;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,10 @@
|
||||||
talkpages.homePage=/
|
talkpages.homePage=/
|
||||||
talkpages.systemUser.name=System
|
talkpages.systemUser.name=System
|
||||||
|
talkpages.createExamplePage=true
|
||||||
|
|
||||||
quarkus.http.auth.basic=true
|
quarkus.http.auth.basic=true
|
||||||
|
|
||||||
quarkus.hibernate-orm.database.generation=update
|
quarkus.hibernate-orm.database.generation=drop-and-create
|
||||||
|
|
||||||
quarkus.datasource.db-kind=h2
|
quarkus.datasource.db-kind=h2
|
||||||
quarkus.datasource.username=username-default
|
quarkus.datasource.username=username-default
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/page/{page.getSlug}">Back to {page.getTitle}</a></li>
|
<li><a href="/page/{page.getSlug}">Back to {page.getTitle}</a></li>
|
||||||
<li><a href="/page/Talk/{page.getSlug}">Talk/{page.getTitle}</a></li>
|
<li><a href="/page/Talk:{page.getSlug}">Talk:{page.getTitle}</a></li>
|
||||||
<li><a href="/edit/{page.getSlug}">Edit page</a></li>
|
<li><a href="/edit/{page.getSlug}">Edit page</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{/include}
|
{/include}
|
|
@ -8,21 +8,12 @@
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/edit">Create a page</a></li>
|
<li><a href="/edit">Create a page</a></li>
|
||||||
<li><a href="/auth">
|
{#if !user:loggedIn}
|
||||||
{#if user:loggedIn}
|
<li><a href="/auth">Login or register</a></li>
|
||||||
Logged in as {user:name}
|
|
||||||
{#else}
|
{#else}
|
||||||
Login or register
|
<li><a href="/auth/logout">Logout ({user:name})</a></li>
|
||||||
{/if}
|
{/if}
|
||||||
</a></li>
|
<li><a href="/theme">{#if theme:darkTheme}Light mode{#else}Dark mode{/if}</a></li>
|
||||||
<li><a href="/theme">
|
|
||||||
{#if theme:darkTheme}
|
|
||||||
Light mode
|
|
||||||
{#else}
|
|
||||||
Dark mode
|
|
||||||
{/if}
|
|
||||||
</a></li>
|
|
||||||
<li><a href="/page/TalkPages/Terms of Service">Terms of Service</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<small>Running <a href="/page/TalkPages">TalkPages</a> version {config:["quarkus.application.version"]}</small>
|
<small>Running <a href="/page/TalkPages">TalkPages</a> version {config:["quarkus.application.version"]}</small>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{#include layout customTitle=true noHeading=true}
|
{#include layout}
|
||||||
{#header}
|
{#header}
|
||||||
{#if http:param("redirectFrom") != null}
|
{#if http:param("redirectFrom") != null}
|
||||||
<p>Redirected from {http:param("redirectFrom")}</p>
|
<p>Redirected from {http:param("redirectFrom")}</p>
|
||||||
|
@ -15,28 +15,14 @@
|
||||||
<h4>This is the current revision #{revision.index} authored by {revision.author.name}.</h4>
|
<h4>This is the current revision #{revision.index} authored by {revision.author.name}.</h4>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{/header}
|
{/header}
|
||||||
|
|
||||||
{#pageTitle}{page.getTitle} - TalkPages{/pageTitle}
|
{#pageTitle}{page.title}{/pageTitle}
|
||||||
|
|
||||||
{#if page.getChain.isEmpty} <!-- TODO don't repeat that -->
|
|
||||||
<h1>{page.getTitle}</h1>
|
|
||||||
{#else}
|
|
||||||
<h1>
|
|
||||||
<small>
|
|
||||||
{#for parent in page.getChain}
|
|
||||||
<a href="/page/{parent.getSlug}">{parent.getTitle}</a> /
|
|
||||||
{/for}
|
|
||||||
</small>
|
|
||||||
{page.getTitle}
|
|
||||||
</h1>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{revision.content.sanitizeContent.raw}
|
{revision.content.sanitizeContent.raw}
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<small>Modified {revision.timestamp.toString()} | <a href="/history/{page.getSlug}">Full history</a> | <a href="/edit/{page.getSlug}">Edit</a>
|
<small>Modified {revision.timestamp.toString()} | <a href="/history/{page.getSlug}">Full history</a> | <a href="/edit/{page.getSlug}">Edit</a>
|
||||||
{#if !page.title.startsWith("Talk/")} | <a href="/page/Talk/{page.getSlug}">Talk</a>{/if}</small>
|
{#if !page.title.startsWith("Talk:")} | <a href="/page/Talk:{page.getSlug}">Talk</a>{/if}</small>
|
||||||
{/include}
|
{/include}
|
|
@ -4,7 +4,6 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<!-- TODO opengraph tags and maybe some nice css. also add errors and stuff here -->
|
<!-- TODO opengraph tags and maybe some nice css. also add errors and stuff here -->
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
|
||||||
{#if theme:darkTheme}
|
{#if theme:darkTheme}
|
||||||
<link rel="stylesheet" href="/static/dark.css">
|
<link rel="stylesheet" href="/static/dark.css">
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -12,10 +11,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{#insert header}{/}
|
{#insert header}{/}
|
||||||
|
|
||||||
{#if !noHeading??}
|
|
||||||
<h1>{#insert pageTitle /}</h1>
|
<h1>{#insert pageTitle /}</h1>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#insert}{/}
|
{#insert}{/}
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Reference in a new issue