View webapp in progress
This commit is contained in:
@@ -48,10 +48,6 @@
|
|||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-enforcer-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-resources-plugin</artifactId>
|
<artifactId>maven-resources-plugin</artifactId>
|
||||||
@@ -66,9 +62,7 @@
|
|||||||
<outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF</outputDirectory>
|
<outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF</outputDirectory>
|
||||||
<resources>
|
<resources>
|
||||||
<resource>
|
<resource>
|
||||||
<directory>${basedir}/..</directory>
|
<directory>${project.basedir}/src/main/config</directory>
|
||||||
<include>pairgoth.properties</include>
|
|
||||||
<!-- <filtering>true</filtering> -->
|
|
||||||
</resource>
|
</resource>
|
||||||
</resources>
|
</resources>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -161,13 +155,6 @@
|
|||||||
<version>${slf4j.version}</version>
|
<version>${slf4j.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!--
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>jcl-over-slf4j</artifactId>
|
|
||||||
<version>${slf4j.version}</version>
|
|
||||||
</dependency>
|
|
||||||
-->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.diogonunes</groupId>
|
<groupId>com.diogonunes</groupId>
|
||||||
<artifactId>JColor</artifactId>
|
<artifactId>JColor</artifactId>
|
||||||
|
@@ -1,11 +1,8 @@
|
|||||||
package org.jeudego.pairgoth.model
|
package org.jeudego.pairgoth.model
|
||||||
|
|
||||||
import com.republicate.kson.Json
|
import com.republicate.kson.Json
|
||||||
import com.republicate.kson.toJsonArray
|
|
||||||
import org.jeudego.pairgoth.api.ApiHandler
|
|
||||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||||
import org.jeudego.pairgoth.store.Store
|
import org.jeudego.pairgoth.store.Store
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
// Pairable
|
// Pairable
|
||||||
|
|
||||||
|
4
pom.xml
4
pom.xml
@@ -23,7 +23,7 @@
|
|||||||
<id>republicate.com</id>
|
<id>republicate.com</id>
|
||||||
<url>https://republicate.com/maven2</url>
|
<url>https://republicate.com/maven2</url>
|
||||||
<releases>
|
<releases>
|
||||||
<enabled>true</enabled>
|
<enabled>false</enabled>
|
||||||
</releases>
|
</releases>
|
||||||
<snapshots>
|
<snapshots>
|
||||||
<enabled>true</enabled>
|
<enabled>true</enabled>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>api-webapp</module>
|
<module>api-webapp</module>
|
||||||
<!-- <module>view-webapp</module> in progress -->
|
<module>view-webapp</module>
|
||||||
<module>container</module>
|
<module>container</module>
|
||||||
<module>bootstrap</module>
|
<module>bootstrap</module>
|
||||||
<module>application</module>
|
<module>application</module>
|
||||||
|
262
view-webapp/pom.xml
Normal file
262
view-webapp/pom.xml
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.jeudego.pairgoth</groupId>
|
||||||
|
<artifactId>engine-parent</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>view-webapp</artifactId>
|
||||||
|
|
||||||
|
<packaging>war</packaging>
|
||||||
|
<name>${project.groupId}:${project.artifactId}</name>
|
||||||
|
<description>PairGoth pairing system</description>
|
||||||
|
<url>TODO</url>
|
||||||
|
<properties>
|
||||||
|
<kotlin.version>1.8.21</kotlin.version>
|
||||||
|
<kotlin.code.style>official</kotlin.code.style>
|
||||||
|
<kotlin.compiler.jvmTarget>10</kotlin.compiler.jvmTarget>
|
||||||
|
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
|
||||||
|
<pac4j.version>5.7.1</pac4j.version>
|
||||||
|
</properties>
|
||||||
|
<build>
|
||||||
|
<defaultGoal>package</defaultGoal>
|
||||||
|
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||||
|
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-maven-plugin</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>compile</id>
|
||||||
|
<phase>compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>test-compile</id>
|
||||||
|
<phase>test-compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>test-compile</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-resources-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy-resources</id>
|
||||||
|
<phase>process-resources</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>copy-resources</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF</outputDirectory>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>${project.basedir}/src/main/config</directory>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<classpathDependencyExcludes>
|
||||||
|
<classpathDependencyExclude>com.republicate:webapp-slf4j-logger</classpathDependencyExclude>
|
||||||
|
</classpathDependencyExcludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit</groupId>
|
||||||
|
<artifactId>junit-bom</artifactId>
|
||||||
|
<version>5.9.3</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<!-- main dependencies -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-test-junit5</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-reflect</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlinx</groupId>
|
||||||
|
<artifactId>kotlinx-datetime-jvm</artifactId>
|
||||||
|
<version>0.4.0</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- servlets and mail APIs -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.servlet</groupId>
|
||||||
|
<artifactId>jakarta.servlet-api</artifactId>
|
||||||
|
<version>${servlet.api.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.sun.mail</groupId>
|
||||||
|
<artifactId>jakarta.mail</artifactId>
|
||||||
|
<version>1.6.7</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- auth -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.pac4j</groupId>
|
||||||
|
<artifactId>pac4j-oauth</artifactId>
|
||||||
|
<version>${pac4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- logging -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.microutils</groupId>
|
||||||
|
<artifactId>kotlin-logging-jvm</artifactId>
|
||||||
|
<version>3.0.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>${slf4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.republicate</groupId>
|
||||||
|
<artifactId>webapp-slf4j-logger</artifactId>
|
||||||
|
<version>3.0</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<version>${slf4j.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.diogonunes</groupId>
|
||||||
|
<artifactId>JColor</artifactId>
|
||||||
|
<version>5.0.1</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- mailer -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.republicate</groupId>
|
||||||
|
<artifactId>simple-mailer</artifactId>
|
||||||
|
<version>1.6</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- json -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.republicate.kson</groupId>
|
||||||
|
<artifactId>essential-kson-jvm</artifactId>
|
||||||
|
<version>2.3</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- charset detection
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.ibm.icu</groupId>
|
||||||
|
<artifactId>icu4j</artifactId>
|
||||||
|
<version>70.1</version>
|
||||||
|
</dependency>
|
||||||
|
-->
|
||||||
|
<!-- net clients
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
<version>4.5.13</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpmime</artifactId>
|
||||||
|
<version>4.5.13</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-io</groupId>
|
||||||
|
<artifactId>commons-io</artifactId>
|
||||||
|
<version>2.11.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-net</groupId>
|
||||||
|
<artifactId>commons-net</artifactId>
|
||||||
|
<version>3.8.0</version>
|
||||||
|
</dependency>
|
||||||
|
-->
|
||||||
|
<!-- server-side events -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.republicate</groupId>
|
||||||
|
<artifactId>jeasse-servlet3</artifactId>
|
||||||
|
<version>1.2</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- templating -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.velocity.tools</groupId>
|
||||||
|
<artifactId>velocity-tools-view</artifactId>
|
||||||
|
<version>3.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.velocity</groupId>
|
||||||
|
<artifactId>velocity-engine-core</artifactId>
|
||||||
|
<version>2.4-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- tests -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>${junit.jupiter.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito.kotlin</groupId>
|
||||||
|
<artifactId>mockito-kotlin</artifactId>
|
||||||
|
<version>4.1.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- test emails -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.icegreen</groupId>
|
||||||
|
<artifactId>greenmail</artifactId>
|
||||||
|
<version>1.6.12</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>javax.activation</groupId>
|
||||||
|
<artifactId>activation</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.sun.mail</groupId>
|
||||||
|
<artifactId>javax.mail</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
10
view-webapp/src/main/config/jetty-web.xml
Normal file
10
view-webapp/src/main/config/jetty-web.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
|
||||||
|
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
|
||||||
|
<!-- https://www.eclipse.org/jetty/documentation/jetty-9/index.html#file-alias-serving -->
|
||||||
|
<Call name="addAliasCheck">
|
||||||
|
<Arg>
|
||||||
|
<New class="org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker" />
|
||||||
|
</Arg>
|
||||||
|
</Call>
|
||||||
|
</Configure>
|
18
view-webapp/src/main/config/pairgoth.default.properties
Normal file
18
view-webapp/src/main/config/pairgoth.default.properties
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# webapp
|
||||||
|
webapp.env = dev
|
||||||
|
webapp.url = http://localhost:8080
|
||||||
|
|
||||||
|
# store
|
||||||
|
store = file
|
||||||
|
store.file.path = tournamentfiles
|
||||||
|
|
||||||
|
# smtp
|
||||||
|
smtp.sender =
|
||||||
|
smtp.host =
|
||||||
|
smtp.port = 587
|
||||||
|
smtp.user =
|
||||||
|
smtp.password =
|
||||||
|
|
||||||
|
# logging
|
||||||
|
logger.level = trace
|
||||||
|
logger.format = [%level] %ip [%logger] %message
|
0
view-webapp/src/main/config/translations/fr
Normal file
0
view-webapp/src/main/config/translations/fr
Normal file
56
view-webapp/src/main/config/web.xml
Normal file
56
view-webapp/src/main/config/web.xml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
|
||||||
|
version="3.1">
|
||||||
|
<!-- Listeners -->
|
||||||
|
<!-- we're not using @WebListener annotations so that our manager is initialized *after* the webapp logger -->
|
||||||
|
<listener>
|
||||||
|
<listener-class>com.republicate.slf4j.impl.ServletContextLoggerListener</listener-class>
|
||||||
|
</listener>
|
||||||
|
<listener>
|
||||||
|
<listener-class>org.jeudego.pairgoth.web.WebappManager</listener-class>
|
||||||
|
</listener>
|
||||||
|
|
||||||
|
<!-- filters -->
|
||||||
|
<filter>
|
||||||
|
<filter-name>webapp-slf4j-logger-ip-tag-filter</filter-name>
|
||||||
|
<filter-class>com.republicate.slf4j.impl.IPTagFilter</filter-class>
|
||||||
|
<async-supported>true</async-supported>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- filters mapping -->
|
||||||
|
<filter-mapping>
|
||||||
|
<filter-name>webapp-slf4j-logger-ip-tag-filter</filter-name>
|
||||||
|
<url-pattern>/*</url-pattern>
|
||||||
|
<dispatcher>REQUEST</dispatcher>
|
||||||
|
<dispatcher>FORWARD</dispatcher>
|
||||||
|
</filter-mapping>
|
||||||
|
|
||||||
|
<!-- servlets -->
|
||||||
|
<servlet>
|
||||||
|
<servlet-name>view</servlet-name>
|
||||||
|
<servlet-class>org.jeudego.pairgoth.web.ViewServlet</servlet-class>
|
||||||
|
</servlet>
|
||||||
|
<servlet>
|
||||||
|
<servlet-name>sse</servlet-name>
|
||||||
|
<servlet-class>org.jeudego.pairgoth.web.SSEServlet</servlet-class>
|
||||||
|
<load-on-startup>1</load-on-startup>
|
||||||
|
<async-supported>true</async-supported>
|
||||||
|
</servlet>
|
||||||
|
|
||||||
|
<!-- servlet mappings -->
|
||||||
|
<servlet-mapping>
|
||||||
|
<servlet-name>view</servlet-name>
|
||||||
|
<url-pattern>/*</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
|
<servlet-mapping>
|
||||||
|
<servlet-name>sse</servlet-name>
|
||||||
|
<url-pattern>/events/*</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
|
|
||||||
|
<!-- context params -->
|
||||||
|
<context-param>
|
||||||
|
<param-name>webapp-slf4j-logger.format</param-name>
|
||||||
|
<param-value>%logger [%level] [%ip] %message @%file:%line:%column</param-value>
|
||||||
|
</context-param>
|
||||||
|
</web-app>
|
@@ -0,0 +1,28 @@
|
|||||||
|
package org.jeudego.pairgoth.oauth
|
||||||
|
|
||||||
|
class FacebookHelper : OAuthHelper() {
|
||||||
|
override val name: String
|
||||||
|
get() = "facebook"
|
||||||
|
|
||||||
|
override fun getLoginURL(sessionId: String?): String {
|
||||||
|
return "https://www.facebook.com/v14.0/dialog/oauth?" +
|
||||||
|
"client_id=" + clientId +
|
||||||
|
"&redirect_uri=" + redirectURI +
|
||||||
|
"&scope=email" +
|
||||||
|
"&state=" + getState(sessionId!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAccessTokenURL(code: String): String? {
|
||||||
|
return "https://graph.facebook.com/v14.0/oauth/access_token?" +
|
||||||
|
"client_id=" + clientId +
|
||||||
|
"&redirect_uri=" + redirectURI +
|
||||||
|
"&client_secret=" + secret +
|
||||||
|
"&code=" + code
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserInfosURL(accessToken: String): String? {
|
||||||
|
return "https://graph.facebook.com/me?" +
|
||||||
|
"field=email" +
|
||||||
|
"&access_token=" + accessToken
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
package org.jeudego.pairgoth.oauth
|
||||||
|
|
||||||
|
class GoogleHelper : OAuthHelper() {
|
||||||
|
override val name: String
|
||||||
|
get() = "google"
|
||||||
|
|
||||||
|
override fun getLoginURL(sessionId: String?): String {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAccessTokenURL(code: String): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserInfosURL(accessToken: String): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
package org.jeudego.pairgoth.oauth
|
||||||
|
|
||||||
|
class InstagramHelper : OAuthHelper() {
|
||||||
|
override val name: String
|
||||||
|
get() = "instagram"
|
||||||
|
|
||||||
|
override fun getLoginURL(sessionId: String?): String {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAccessTokenURL(code: String): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserInfosURL(accessToken: String): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,77 @@
|
|||||||
|
package org.jeudego.pairgoth.oauth
|
||||||
|
|
||||||
|
// In progress
|
||||||
|
|
||||||
|
import com.republicate.kson.Json
|
||||||
|
import org.jeudego.pairgoth.web.WebappManager
|
||||||
|
//import com.republicate.modality.util.AESCryptograph
|
||||||
|
//import com.republicate.modality.util.Cryptograph
|
||||||
|
import org.apache.commons.codec.binary.Base64
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.UnsupportedEncodingException
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
abstract class OAuthHelper {
|
||||||
|
abstract val name: String
|
||||||
|
abstract fun getLoginURL(sessionId: String?): String
|
||||||
|
protected val clientId: String
|
||||||
|
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".client_id")
|
||||||
|
protected val secret: String
|
||||||
|
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".secret")
|
||||||
|
protected val redirectURI: String?
|
||||||
|
protected get() = try {
|
||||||
|
val uri: String = WebappManager.Companion.getProperty("webapp.url") + "/oauth.html"
|
||||||
|
URLEncoder.encode(uri, "UTF-8")
|
||||||
|
} catch (uee: UnsupportedEncodingException) {
|
||||||
|
logger.error("could not encode redirect URI", uee)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getState(sessionId: String): String {
|
||||||
|
return name + ":" + encrypt(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkState(state: String, expectedSessionId: String): Boolean {
|
||||||
|
val foundSessionId = decrypt(state)
|
||||||
|
return expectedSessionId == foundSessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun getAccessTokenURL(code: String): String?
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getAccessToken(code: String): String {
|
||||||
|
val json: Json.Object = Json.Object() // TODO - apiClient.get(getAccessTokenURL(code))
|
||||||
|
return json.getString("access_token")!! // ?!
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun getUserInfosURL(accessToken: String): String?
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getUserEmail(accessToken: String): String {
|
||||||
|
val json: Json.Object = Json.Object()
|
||||||
|
// TODO
|
||||||
|
// apiClient.get(getUserInfosURL(accessToken))
|
||||||
|
return json.getString("email") ?: throw IOException("could not fetch email")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
protected var logger = LoggerFactory.getLogger("oauth")
|
||||||
|
private const val salt = "0efd28fb53cbac42"
|
||||||
|
// private val sessionIdCrypto: Cryptograph = AESCryptograph().apply {
|
||||||
|
// init(salt)
|
||||||
|
// }
|
||||||
|
|
||||||
|
private fun encrypt(input: String): String {
|
||||||
|
return "TODO"
|
||||||
|
// return Base64.encodeBase64URLSafeString(sessionIdCrypto.encrypt(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decrypt(input: String): String {
|
||||||
|
return "TODO"
|
||||||
|
// return sessionIdCrypto.decrypt(Base64.decodeBase64(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// private val apiClient: ApiClient = ApiClient()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package org.jeudego.pairgoth.oauth
|
||||||
|
|
||||||
|
object OauthHelperFactory {
|
||||||
|
private val facebook: OAuthHelper = FacebookHelper()
|
||||||
|
private val google: OAuthHelper = GoogleHelper()
|
||||||
|
private val instagram: OAuthHelper = InstagramHelper()
|
||||||
|
private val twitter: OAuthHelper = TwitterHelper()
|
||||||
|
fun getHelper(provider: String?): OAuthHelper {
|
||||||
|
return when (provider) {
|
||||||
|
"facebook" -> facebook
|
||||||
|
"google" -> google
|
||||||
|
"instagram" -> instagram
|
||||||
|
"twitter" -> twitter
|
||||||
|
else -> throw RuntimeException("wrong provider")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
package org.jeudego.pairgoth.oauth
|
||||||
|
|
||||||
|
class TwitterHelper : OAuthHelper() {
|
||||||
|
override val name: String
|
||||||
|
get() = "twitter"
|
||||||
|
|
||||||
|
override fun getLoginURL(sessionId: String?): String {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAccessTokenURL(code: String): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserInfosURL(accessToken: String): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
package org.jeudego.pairgoth.util
|
||||||
|
|
||||||
|
import com.diogonunes.jcolor.Ansi
|
||||||
|
import com.diogonunes.jcolor.AnsiFormat
|
||||||
|
import com.diogonunes.jcolor.Attribute
|
||||||
|
|
||||||
|
private val blue = AnsiFormat(Attribute.BRIGHT_BLUE_TEXT())
|
||||||
|
private val green = AnsiFormat(Attribute.BRIGHT_GREEN_TEXT())
|
||||||
|
private val red = AnsiFormat(Attribute.BRIGHT_RED_TEXT())
|
||||||
|
private val bold = AnsiFormat(Attribute.BOLD())
|
||||||
|
|
||||||
|
object Colorizer {
|
||||||
|
|
||||||
|
fun blue(str: String) = Ansi.colorize(str, blue)
|
||||||
|
fun green(str: String) = Ansi.colorize(str, green)
|
||||||
|
fun red(str: String) = Ansi.colorize(str, red)
|
||||||
|
fun bold(str: String) = Ansi.colorize(str, bold)
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
package org.jeudego.pairgoth.util
|
||||||
|
|
||||||
|
import com.republicate.kson.Json
|
||||||
|
import java.io.Reader
|
||||||
|
import java.io.Writer
|
||||||
|
|
||||||
|
|
||||||
|
fun Json.Companion.parse(reader: Reader) = Json.Companion.parse(object: Json.Input {
|
||||||
|
override fun read() = reader.read().toChar()
|
||||||
|
})
|
||||||
|
|
||||||
|
fun Json.toString(writer: Writer) = toString(object: Json.Output {
|
||||||
|
override fun writeChar(c: Char): Json.Output {
|
||||||
|
writer.write(c.code)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
override fun writeString(s: String): Json.Output {
|
||||||
|
writer.write(s)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
override fun writeString(s: String, from: Int, to: Int): Json.Output {
|
||||||
|
writer.write(s, from, to)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
})
|
@@ -0,0 +1,20 @@
|
|||||||
|
package org.jeudego.pairgoth.util
|
||||||
|
|
||||||
|
import org.apache.velocity.Template
|
||||||
|
import org.apache.velocity.exception.ResourceNotFoundException
|
||||||
|
import org.apache.velocity.runtime.directive.Parse
|
||||||
|
import org.jeudego.pairgoth.view.TranslationTool
|
||||||
|
import org.jeudego.pairgoth.web.LanguageFilter
|
||||||
|
|
||||||
|
class TranslateDirective : Parse() {
|
||||||
|
override fun getName(): String {
|
||||||
|
return "translate"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTemplate(path: String, encoding: String): Template? {
|
||||||
|
val template = super.getTemplate(path, encoding)
|
||||||
|
val translator = TranslationTool.translator.get()
|
||||||
|
?: throw RuntimeException("no current active translator")
|
||||||
|
return translator.translate(path, template)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,169 @@
|
|||||||
|
package org.jeudego.pairgoth.util
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringEscapeUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.velocity.Template
|
||||||
|
import org.apache.velocity.runtime.parser.node.ASTText
|
||||||
|
import org.apache.velocity.runtime.parser.node.SimpleNode
|
||||||
|
import org.jeudego.pairgoth.web.WebappManager
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.io.StringWriter
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
import kotlin.io.path.readLines
|
||||||
|
import kotlin.io.path.useDirectoryEntries
|
||||||
|
|
||||||
|
class Translator private constructor(private val iso: String) {
|
||||||
|
|
||||||
|
fun translate(enText: String) = translations[iso]?.get(enText) ?: enText.also {
|
||||||
|
reportMissingTranslation(enText)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun translate(uri: String, template: Template): Template? {
|
||||||
|
if (iso == "en") return template
|
||||||
|
val key = Pair(uri, iso)
|
||||||
|
var translated = translatedTemplates[key]
|
||||||
|
if (translated != null && translated.lastModified < template.lastModified) {
|
||||||
|
translatedTemplates.remove(key)
|
||||||
|
translated = null
|
||||||
|
}
|
||||||
|
if (translated == null) {
|
||||||
|
synchronized(translatedTemplates) {
|
||||||
|
translated = translatedTemplates[key]
|
||||||
|
if (translated == null) {
|
||||||
|
translated = template.clone() as Template
|
||||||
|
val data: SimpleNode = translated!!.data as SimpleNode
|
||||||
|
translateNode(data)
|
||||||
|
translatedTemplates[key] = translated!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return translated
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun translateNode(node: SimpleNode, ignoringInput: String? = null): String? {
|
||||||
|
var ignoring = ignoringInput
|
||||||
|
if (node is ASTText) translateFragments(node.text, ignoring).let {
|
||||||
|
node.text = it.first
|
||||||
|
ignoring = it.second
|
||||||
|
}
|
||||||
|
else for (i in 0 until node.jjtGetNumChildren()) {
|
||||||
|
ignoring = translateNode(node.jjtGetChild(i) as SimpleNode, ignoring)
|
||||||
|
}
|
||||||
|
return ignoring
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun translateFragments(text: String, ignoringInput: String?): Pair<String, String?> {
|
||||||
|
var ignoring = ignoringInput
|
||||||
|
val ignoreMap = buildIgnoreMap(text, ignoring).also {
|
||||||
|
ignoring = it.second
|
||||||
|
}.first
|
||||||
|
val sw = StringWriter()
|
||||||
|
val output = PrintWriter(sw)
|
||||||
|
val matcher = textExtractor.matcher(text)
|
||||||
|
var pos = 0
|
||||||
|
while (matcher.find(pos)) {
|
||||||
|
val start = matcher.start()
|
||||||
|
val end = matcher.end()
|
||||||
|
if (start > pos) output.print(text.substring(pos, start))
|
||||||
|
val ignore: Boolean = ignoreMap.floorEntry(start).value
|
||||||
|
if (ignore) output.print(text.substring(start, end)) else {
|
||||||
|
var group = 1
|
||||||
|
var groupStart = matcher.start(group)
|
||||||
|
while (groupStart == -1 && group < matcher.groupCount()) groupStart = matcher.start(++group)
|
||||||
|
if (groupStart == -1) throw RuntimeException("unexpected case")
|
||||||
|
if (groupStart > start) output.print(text.substring(start, groupStart))
|
||||||
|
val capture = matcher.group(group)
|
||||||
|
var token: String = StringEscapeUtils.unescapeHtml4(capture)
|
||||||
|
if (StringUtils.containsOnly(token, "\r\n\t -;:.\"/<>\u00A00123456789€!")) output.print(capture) else {
|
||||||
|
token = normalize(token)
|
||||||
|
token = translate(token)
|
||||||
|
output.print(StringEscapeUtils.escapeHtml4(token))
|
||||||
|
}
|
||||||
|
val groupEnd = matcher.end(group)
|
||||||
|
if (groupEnd < end) output.print(text.substring(groupEnd, end))
|
||||||
|
}
|
||||||
|
pos = end
|
||||||
|
}
|
||||||
|
if (pos < text.length) output.print(text.substring(pos))
|
||||||
|
return Pair(sw.toString(), ignoring)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalize(str: String): String {
|
||||||
|
return str.replace(Regex("\\s+"), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildIgnoreMap(text: String, ignoringInput: String?): Pair<NavigableMap<Int, Boolean>, String?> {
|
||||||
|
val map: NavigableMap<Int, Boolean> = TreeMap()
|
||||||
|
var ignoring = ignoringInput
|
||||||
|
var pos = 0
|
||||||
|
map[0] = (ignoring != null)
|
||||||
|
while (pos < text.length) {
|
||||||
|
if (ignoring == null) {
|
||||||
|
val nextIgnore = ignoredTags.map { tag ->
|
||||||
|
Pair(tag, text.indexOf("<$tag(?:>\\s)"))
|
||||||
|
}.filter {
|
||||||
|
it.second != -1
|
||||||
|
}.sortedBy {
|
||||||
|
it.second
|
||||||
|
}.firstOrNull()
|
||||||
|
if (nextIgnore == null) pos = text.length
|
||||||
|
else {
|
||||||
|
ignoring = nextIgnore.first
|
||||||
|
pos += nextIgnore.first.length + 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val closingTag = text.indexOf("</$ignoring>")
|
||||||
|
if (closingTag == -1) pos = text.length
|
||||||
|
else {
|
||||||
|
pos += ignoring.length + 3
|
||||||
|
ignoring = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair(map, ignoring)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var ASTText.text: String
|
||||||
|
get() = textAccessor[this] as String
|
||||||
|
set(value: String) { textAccessor[this] = value }
|
||||||
|
|
||||||
|
private fun reportMissingTranslation(enText: String) {
|
||||||
|
logger.warn("missing translation towards {}: {}", iso, enText)
|
||||||
|
// CB TODO - create file
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val textAccessor = ASTText::class.java.getDeclaredField("ctext").apply { isAccessible = true }
|
||||||
|
private val logger = LoggerFactory.getLogger("translation")
|
||||||
|
private val translatedTemplates: MutableMap<Pair<String, String>, Template> = ConcurrentHashMap<Pair<String, String>, Template>()
|
||||||
|
private val textExtractor = Pattern.compile(
|
||||||
|
"<[^>]+\\splaceholder=\"(?<placeholder>[^\"]*)\"[^>]*>|(?<=>)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?<text>[^<>]+?)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?=<|$)|(?<=>|^)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?<text2>[^<>]+?)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?=<)",
|
||||||
|
Pattern.DOTALL
|
||||||
|
)
|
||||||
|
private val ignoredTags = setOf("head", "script", "style")
|
||||||
|
|
||||||
|
private val translations = Path.of(WebappManager.context.getRealPath("WEB-INF/translations")).useDirectoryEntries("??") { entries ->
|
||||||
|
entries.map { file ->
|
||||||
|
Pair(
|
||||||
|
file.fileName.toString(),
|
||||||
|
file.readLines(StandardCharsets.UTF_8).filter {
|
||||||
|
it.isNotEmpty() && it.contains('\t') && !it.startsWith('#')
|
||||||
|
}.map {
|
||||||
|
Pair(it.substringBefore('\t'), it.substringAfter('\t'))
|
||||||
|
}.toMap()
|
||||||
|
)
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val translators = ConcurrentHashMap<String, Translator>()
|
||||||
|
fun getTranslator(iso: String) = translators.getOrPut(iso) { Translator(iso) }
|
||||||
|
|
||||||
|
val providedLanguages = setOf("en", "fr")
|
||||||
|
const val defaultLanguage = "en"
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package org.jeudego.pairgoth.view
|
||||||
|
|
||||||
|
import org.apache.velocity.tools.config.ValidScope
|
||||||
|
import org.jeudego.pairgoth.util.Translator
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
|
@ValidScope("request")
|
||||||
|
class TranslationTool {
|
||||||
|
|
||||||
|
fun translate(enText: String): String {
|
||||||
|
return translator.get().translate(enText)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val translator = ThreadLocal<Translator>()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
package org.jeudego.egc2024.web
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import javax.servlet.Filter
|
||||||
|
import javax.servlet.FilterChain
|
||||||
|
import javax.servlet.FilterConfig
|
||||||
|
import javax.servlet.RequestDispatcher
|
||||||
|
import javax.servlet.ServletException
|
||||||
|
import javax.servlet.ServletRequest
|
||||||
|
import javax.servlet.ServletResponse
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
|
class DispatchingFilter : Filter {
|
||||||
|
|
||||||
|
protected val defaultRequestDispatcher: RequestDispatcher by lazy {
|
||||||
|
filterConfig.servletContext.getNamedDispatcher("default")
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var filterConfig: FilterConfig
|
||||||
|
|
||||||
|
override fun init(filterConfig: FilterConfig) {
|
||||||
|
this.filterConfig = filterConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroy() {}
|
||||||
|
|
||||||
|
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||||
|
val req = request as HttpServletRequest
|
||||||
|
val resp = response as HttpServletResponse
|
||||||
|
val uri = req.requestURI
|
||||||
|
when {
|
||||||
|
uri.endsWith('/') -> response.sendRedirect("${uri}index")
|
||||||
|
uri.contains('.') -> defaultRequestDispatcher.forward(request, response)
|
||||||
|
else -> chain.doFilter(request, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
package org.jeudego.pairgoth.web
|
||||||
|
|
||||||
|
import org.jeudego.pairgoth.util.Translator
|
||||||
|
import org.jeudego.pairgoth.util.Translator.Companion.defaultLanguage
|
||||||
|
import org.jeudego.pairgoth.util.Translator.Companion.getTranslator
|
||||||
|
import org.jeudego.pairgoth.util.Translator.Companion.providedLanguages
|
||||||
|
import org.jeudego.pairgoth.view.TranslationTool
|
||||||
|
import javax.servlet.Filter
|
||||||
|
import javax.servlet.FilterChain
|
||||||
|
import javax.servlet.FilterConfig
|
||||||
|
import javax.servlet.ServletRequest
|
||||||
|
import javax.servlet.ServletResponse
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
|
class LanguageFilter : Filter {
|
||||||
|
private var filterConfig: FilterConfig? = null
|
||||||
|
|
||||||
|
override fun init(filterConfig: FilterConfig) {
|
||||||
|
this.filterConfig = filterConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doFilter(req: ServletRequest, resp: ServletResponse, chain: FilterChain) {
|
||||||
|
val request = req as HttpServletRequest
|
||||||
|
val response = resp as HttpServletResponse
|
||||||
|
|
||||||
|
val uri = request.requestURI
|
||||||
|
val match = langPattern.matchEntire(uri)
|
||||||
|
val lang = match?.groupValues?.get(1)
|
||||||
|
val target = match?.groupValues?.get(2) ?: uri
|
||||||
|
|
||||||
|
if (lang != null && providedLanguages.contains(lang)) {
|
||||||
|
// the target URI contains a language we provide
|
||||||
|
request.setAttribute("lang", lang)
|
||||||
|
request.setAttribute("target", target)
|
||||||
|
TranslationTool.translator.set(Translator.getTranslator(lang))
|
||||||
|
chain.doFilter(request, response)
|
||||||
|
} else {
|
||||||
|
// the request must be redirected
|
||||||
|
val preferredLanguage = getPreferredLanguage(request)
|
||||||
|
val destination = if (lang != null) target else uri
|
||||||
|
response.sendRedirect("${preferredLanguage}${destination}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPreferredLanguage(request: HttpServletRequest): String {
|
||||||
|
return (request.session.getAttribute("lang") as String?) ?:
|
||||||
|
( langHeaderParser.findAll(request.getHeader("Accept-Language") ?: "").filter {
|
||||||
|
providedLanguages.contains(it.groupValues[1])
|
||||||
|
}.sortedByDescending {
|
||||||
|
it.groupValues[2].toDoubleOrNull() ?: 1.0
|
||||||
|
}.firstOrNull()?.let {
|
||||||
|
it.groupValues[1]
|
||||||
|
} ?: defaultLanguage ).also {
|
||||||
|
request.session.setAttribute("lang", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroy() {}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val langPattern = Regex("/([a-z]{2})(/.+)")
|
||||||
|
private val langHeaderParser = Regex("(?:\\b(\\*|[a-z]{2})(?:_\\w+)?)(?:;q=([0-9.]+))?")
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,71 @@
|
|||||||
|
package org.jeudego.pairgoth.web
|
||||||
|
|
||||||
|
import com.republicate.kson.Json
|
||||||
|
import org.jeudego.pairgoth.util.Colorizer.blue
|
||||||
|
import org.jeudego.pairgoth.util.Colorizer.green
|
||||||
|
import org.jeudego.pairgoth.util.toString
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import java.io.StringWriter
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
|
fun Logger.logRequest(req: HttpServletRequest, logHeaders: Boolean = false) {
|
||||||
|
val builder = StringBuilder()
|
||||||
|
builder.append(req.method).append(' ')
|
||||||
|
.append(req.scheme).append("://")
|
||||||
|
.append(req.localName)
|
||||||
|
val port = req.localPort
|
||||||
|
if (port != 80) builder.append(':').append(port)
|
||||||
|
if (!req.contextPath.isEmpty()) {
|
||||||
|
builder.append(req.contextPath)
|
||||||
|
}
|
||||||
|
builder.append(req.requestURI)
|
||||||
|
if (req.method == "GET") {
|
||||||
|
val qs = req.queryString
|
||||||
|
if (qs != null) builder.append('?').append(qs)
|
||||||
|
}
|
||||||
|
// builder.append(' ').append(req.getProtocol());
|
||||||
|
info(blue("<< {}"), builder.toString())
|
||||||
|
if (isTraceEnabled && logHeaders) {
|
||||||
|
// CB TODO - should be bufferized and asynchronously written in synchronous chunks
|
||||||
|
// so that header lines from parallel requests are not mixed up in the logs ;
|
||||||
|
// synchronizing the whole request log is not desirable
|
||||||
|
val headerNames = req.headerNames
|
||||||
|
while (headerNames.hasMoreElements()) {
|
||||||
|
val name = headerNames.nextElement()
|
||||||
|
val value = req.getHeader(name)
|
||||||
|
trace(blue("<< {}: {}"), name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Logger.logPayload(prefix: String?, payload: Json, upstream: Boolean) {
|
||||||
|
val writer = StringWriter()
|
||||||
|
//payload.toPrettyString(writer, "");
|
||||||
|
payload.toString(writer)
|
||||||
|
if (isTraceEnabled) {
|
||||||
|
for (line in writer.toString().split("\n".toRegex()).dropLastWhile { it.isEmpty() }
|
||||||
|
.toTypedArray()) {
|
||||||
|
trace(if (upstream) blue("{}{}") else green("{}{}"), prefix, line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var line = writer.toString()
|
||||||
|
val pos = line.indexOf('\n')
|
||||||
|
if (pos != -1) line = line.substring(0, pos)
|
||||||
|
if (line.length > 50) line = line.substring(0, 50) + "..."
|
||||||
|
debug(if (upstream) blue("{}{}") else green("{}{}"), prefix, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun HttpServletRequest.getRemoteAddress(): String? {
|
||||||
|
var ip = getHeader("X-Forwarded-For")
|
||||||
|
if (ip == null) {
|
||||||
|
ip = remoteAddr
|
||||||
|
} else {
|
||||||
|
val comma = ip.indexOf(',')
|
||||||
|
if (comma != -1) {
|
||||||
|
ip = ip.substring(0, comma).trim { it <= ' ' } // keep the left-most IP address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
package org.jeudego.pairgoth.web
|
||||||
|
|
||||||
|
import info.macias.sse.EventBroadcast
|
||||||
|
import info.macias.sse.events.MessageEvent
|
||||||
|
import info.macias.sse.servlet3.ServletEventTarget
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import javax.servlet.http.HttpServlet
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
|
|
||||||
|
class SSEServlet: HttpServlet() {
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger("sse")
|
||||||
|
private var zeInstance: SSEServlet? = null
|
||||||
|
internal fun getInstance(): SSEServlet = zeInstance ?: throw Error("SSE servlet not ready")
|
||||||
|
}
|
||||||
|
init {
|
||||||
|
if (zeInstance != null) throw Error("Multiple instances of SSE servlet found!")
|
||||||
|
zeInstance = this
|
||||||
|
}
|
||||||
|
private val broadcast = EventBroadcast()
|
||||||
|
|
||||||
|
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse?) {
|
||||||
|
logger.trace("<< new channel")
|
||||||
|
broadcast.addSubscriber(ServletEventTarget(req), req.getHeader("Last-Event-Id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun broadcast(message: MessageEvent) = broadcast.broadcast(message)
|
||||||
|
}
|
@@ -0,0 +1,93 @@
|
|||||||
|
package org.jeudego.pairgoth.web
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.tuple.Pair
|
||||||
|
import org.apache.velocity.Template
|
||||||
|
import org.apache.velocity.context.Context
|
||||||
|
import org.apache.velocity.tools.view.ServletUtils
|
||||||
|
import org.apache.velocity.tools.view.VelocityViewServlet
|
||||||
|
import org.jeudego.pairgoth.util.Translator
|
||||||
|
import org.jeudego.pairgoth.web.WebappManager
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.io.UnsupportedEncodingException
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.*
|
||||||
|
import java.util.function.Function
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
|
class ViewServlet : VelocityViewServlet() {
|
||||||
|
private fun fileExists(path: String): Boolean {
|
||||||
|
return File(servletContext.getRealPath(path)).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeURI(request: HttpServletRequest): String {
|
||||||
|
var uri = request.requestURI
|
||||||
|
uri = try {
|
||||||
|
URLDecoder.decode(uri, "UTF-8")
|
||||||
|
} catch (use: UnsupportedEncodingException) {
|
||||||
|
throw RuntimeException("could not decode URI $uri", use)
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTemplate(request: HttpServletRequest, response: HttpServletResponse?): Template = getTemplate(STANDARD_LAYOUT)
|
||||||
|
|
||||||
|
override fun fillContext(context: Context, request: HttpServletRequest) {
|
||||||
|
super.fillContext(context, request)
|
||||||
|
var uri = decodeURI(request)
|
||||||
|
context.put("page", uri)
|
||||||
|
val base = uri.replaceFirst(".html$".toRegex(), "")
|
||||||
|
val suffixes = Arrays.asList("js", "css")
|
||||||
|
for (suffix in suffixes) {
|
||||||
|
val resource = "/$suffix$base.$suffix"
|
||||||
|
if (fileExists(resource)) {
|
||||||
|
context.put(suffix, resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val lang = request.getAttribute("lang") as String
|
||||||
|
/*
|
||||||
|
val menu = menuEntries!![uri]
|
||||||
|
var title: String? = null
|
||||||
|
if (lang != null && menu != null) title = menu.getString(lang)
|
||||||
|
if (title != null) context.put("title", title)
|
||||||
|
if (lang != null) context.put(
|
||||||
|
"dateformat",
|
||||||
|
DateFormat.getDateInstance(DateFormat.LONG, Locale.forLanguageTag(lang))
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun error(
|
||||||
|
request: HttpServletRequest?,
|
||||||
|
response: HttpServletResponse,
|
||||||
|
e: Throwable?
|
||||||
|
) {
|
||||||
|
val path: String = ServletUtils.getPath(request)
|
||||||
|
if (response.isCommitted) {
|
||||||
|
log.error("An error occured but the response headers have already been sent.")
|
||||||
|
log.error("Error processing a template for path '{}'", path, e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
log.error("Error processing a template for path '{}'", path, e)
|
||||||
|
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
// clearly something is quite wrong.
|
||||||
|
// let's log the new exception then give up and
|
||||||
|
// throw a runtime exception that wraps the first one
|
||||||
|
val msg = "Exception while printing error screen"
|
||||||
|
log.error(msg, e2)
|
||||||
|
throw RuntimeException(msg, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val STANDARD_LAYOUT = "/WEB-INF/layouts/standard.html"
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,167 @@
|
|||||||
|
package org.jeudego.pairgoth.web
|
||||||
|
|
||||||
|
import com.republicate.mailer.SmtpLoop
|
||||||
|
import org.apache.commons.lang3.tuple.Pair
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.IOException
|
||||||
|
import java.lang.IllegalAccessError
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.*
|
||||||
|
import java.util.IllegalFormatCodePointException
|
||||||
|
import javax.net.ssl.*
|
||||||
|
import javax.servlet.*
|
||||||
|
import javax.servlet.annotation.WebListener
|
||||||
|
import javax.servlet.http.HttpSessionEvent
|
||||||
|
import javax.servlet.http.HttpSessionListener
|
||||||
|
|
||||||
|
@WebListener
|
||||||
|
class WebappManager : ServletContextListener, ServletContextAttributeListener, HttpSessionListener {
|
||||||
|
private fun disableSSLCertificateChecks() {
|
||||||
|
// see http://www.nakov.com/blog/2009/07/16/disable-certificate-validation-in-java-ssl-connections/
|
||||||
|
try {
|
||||||
|
// Create a trust manager that does not validate certificate chains
|
||||||
|
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
|
||||||
|
override fun getAcceptedIssuers(): Array<X509Certificate>? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("TrustAllX509TrustManager")
|
||||||
|
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
|
||||||
|
@Suppress("TrustAllX509TrustManager")
|
||||||
|
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Install the all-trusting trust manager
|
||||||
|
val sc = SSLContext.getInstance("SSL")
|
||||||
|
sc.init(null, trustAllCerts, SecureRandom())
|
||||||
|
HttpsURLConnection.setDefaultSSLSocketFactory(sc.socketFactory)
|
||||||
|
|
||||||
|
// Create all-trusting host name verifier
|
||||||
|
val allHostsValid = HostnameVerifier { hostname, session -> true }
|
||||||
|
|
||||||
|
// Install the all-trusting host verifier
|
||||||
|
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("could not disable SSL certificate checks", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ServletContextListener interface */
|
||||||
|
override fun contextInitialized(sce: ServletContextEvent) {
|
||||||
|
context = sce.servletContext
|
||||||
|
logger.info("---------- Starting Web Application ----------")
|
||||||
|
context.setAttribute("manager", this)
|
||||||
|
webappRoot = context.getRealPath("/")
|
||||||
|
try {
|
||||||
|
// load default properties
|
||||||
|
properties.load(context.getResourceAsStream("/WEB-INF/pairgoth.default.properties"))
|
||||||
|
// override with system properties after stripping off the 'pairgoth.' prefix
|
||||||
|
System.getProperties().filter { (key, value) -> key is String && key.startsWith(PAIRGOTH_PROPERTIES_PREFIX)
|
||||||
|
}.forEach { (key, value) ->
|
||||||
|
properties[(key as String).removePrefix(PAIRGOTH_PROPERTIES_PREFIX)] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Using profile {}", properties.getProperty("webapp.env"))
|
||||||
|
|
||||||
|
// set system user agent string to empty string
|
||||||
|
System.setProperty("http.agent", "")
|
||||||
|
|
||||||
|
// disable (for now ?) the SSL certificate checks, because many sites
|
||||||
|
// fail to correctly implement SSL...
|
||||||
|
disableSSLCertificateChecks()
|
||||||
|
|
||||||
|
// start smtp loop
|
||||||
|
if (properties.containsKey("smtp.host")) {
|
||||||
|
registerService("smtp", SmtpLoop(properties))
|
||||||
|
startService("smtp")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (ioe: IOException) {
|
||||||
|
logger.error("webapp initialization error", ioe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contextDestroyed(sce: ServletContextEvent) {
|
||||||
|
logger.info("---------- Stopping Web Application ----------")
|
||||||
|
|
||||||
|
val context = sce.servletContext
|
||||||
|
for (service in webServices.keys) stopService(service, true)
|
||||||
|
// ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ServletContextAttributeListener interface */
|
||||||
|
override fun attributeAdded(event: ServletContextAttributeEvent) {}
|
||||||
|
override fun attributeRemoved(event: ServletContextAttributeEvent) {}
|
||||||
|
override fun attributeReplaced(event: ServletContextAttributeEvent) {}
|
||||||
|
|
||||||
|
/* HttpSessionListener interface */
|
||||||
|
override fun sessionCreated(se: HttpSessionEvent) {}
|
||||||
|
override fun sessionDestroyed(se: HttpSessionEvent) {}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PAIRGOTH_PROPERTIES_PREFIX = "pairgoth."
|
||||||
|
lateinit var webappRoot: String
|
||||||
|
lateinit var context: ServletContext
|
||||||
|
private val webServices: MutableMap<String?, Pair<Runnable, Thread?>> = TreeMap()
|
||||||
|
var logger = LoggerFactory.getLogger(WebappManager::class.java)
|
||||||
|
val properties = Properties()
|
||||||
|
fun getProperty(prop: String): String? {
|
||||||
|
return properties.getProperty(prop)
|
||||||
|
}
|
||||||
|
fun getMandatoryProperty(prop: String): String {
|
||||||
|
return properties.getProperty(prop) ?: throw Error("missing property: ${prop}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val webappURL by lazy { getProperty("webapp.url") }
|
||||||
|
|
||||||
|
private val services = mutableMapOf<String, Pair<Runnable, Thread>>()
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun registerService(name: String?, task: Runnable, initialStatus: Boolean? = null) {
|
||||||
|
if (webServices.containsKey(name)) {
|
||||||
|
logger.warn("service {} already registered")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.debug("registered service {}", name)
|
||||||
|
webServices[name] =
|
||||||
|
Pair.of(task, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startService(name: String?) {
|
||||||
|
val service = webServices[name]!!
|
||||||
|
if (service.right != null && service.right!!.isAlive) {
|
||||||
|
logger.warn("service {} is already running", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.debug("starting service {}", name)
|
||||||
|
val thread = Thread(service.left, name)
|
||||||
|
thread.start()
|
||||||
|
webServices[name] =
|
||||||
|
Pair.of(
|
||||||
|
service.left,
|
||||||
|
thread
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun stopService(name: String?, webappClosing: Boolean = false) {
|
||||||
|
val service = webServices[name]!!
|
||||||
|
val thread = service.right
|
||||||
|
if (thread == null || !thread.isAlive) {
|
||||||
|
logger.warn("service {} is already stopped", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.debug("stopping service {}", name)
|
||||||
|
thread.interrupt()
|
||||||
|
try {
|
||||||
|
thread.join()
|
||||||
|
} catch (ie: InterruptedException) {
|
||||||
|
}
|
||||||
|
if (!webappClosing) {
|
||||||
|
webServices[name] = Pair.of(service.left, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
view-webapp/src/test/kotlin/.gitkeep
Normal file
0
view-webapp/src/test/kotlin/.gitkeep
Normal file
24
view-webapp/src/test/kotlin/TestBase.kt
Normal file
24
view-webapp/src/test/kotlin/TestBase.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package org.jeudego.pairgoth.test
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.TestInfo
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
abstract class TestBase {
|
||||||
|
companion object {
|
||||||
|
val logger = LoggerFactory.getLogger("test")
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
@JvmStatic
|
||||||
|
fun prepare() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun before(testInfo: TestInfo) {
|
||||||
|
val testName = testInfo.displayName.removeSuffix("()")
|
||||||
|
logger.info("===== Running $testName =====")
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user