commit 271568e6740338c3b46ede36fe9ec22602ecc8a4 Author: Erik C. Thauvin Date: Wed Sep 18 05:16:01 2019 -0700 Initial commit. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a6971e1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*] +insert_final_newline=true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6ec2ae2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# batch files are specific to windows and always crlf +*.bat eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..591d207 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +!.vscode/extensions.json +!.vscode/launch.json +!.vscode/settings.json +!.vscode/tasks.json +**/*.class +**/.idea/$CACHE_FILE$ +**/.idea/$PRODUCT_WORKSPACE_FILE$ +**/.idea/**/dataSources.ids +**/.idea/**/dataSources.local.xml +**/.idea/**/dataSources/ +**/.idea/**/dbnavigator.xml +**/.idea/**/dictionaries +**/.idea/**/dynamic.xml +**/.idea/**/gradle.xml +**/.idea/**/libraries +**/.idea/**/shelf +**/.idea/**/sqlDataSources.xml +**/.idea/**/tasks.xml +**/.idea/**/uiDesigner.xml +**/.idea/**/usage.statistics.xml +**/.idea/**/workspace.xml +*.code-workspace +*.iws +*.sublime-* +.DS_Store +.classpath +.gradle +.kobalt +.nb-gradle +.project +.settings +.vscode/* +/bin +/build +/deploy +/dist +/gen +/gradle.properties +/local.properties +/out +/proguard-project.txt +/project.properties +/target +/test-output +Thumbs.db +ehthumbs.db +kobaltBuild +kobaltw*-test diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..ead1d8a --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..d91f848 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/Erik_s_Copyright_Notice.xml b/.idea/copyright/Erik_s_Copyright_Notice.xml new file mode 100644 index 0000000..08660a1 --- /dev/null +++ b/.idea/copyright/Erik_s_Copyright_Notice.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..1419e40 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8ff795e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,53 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f09e92a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..7044232 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,271 @@ +import com.jfrog.bintray.gradle.tasks.BintrayUploadTask +import org.jetbrains.dokka.gradle.LinkMapping +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.io.FileInputStream +import java.util.Properties + +plugins { + jacoco + java + kotlin("jvm") version "1.3.50" + `maven-publish` + id("com.github.ben-manes.versions") version "0.25.0" + id("com.gradle.build-scan") version "2.4.2" + id("com.jfrog.bintray") version "1.8.4" + id("io.gitlab.arturbosch.detekt") version "1.0.1" + id("net.thauvin.erik.gradle.semver") version "1.0.4" + id("org.jetbrains.dokka") version "0.9.18" + id("org.jetbrains.kotlin.kapt").version("1.3.50") + id("org.jmailen.kotlinter") version "2.1.1" + id("org.sonarqube") version "2.7.1" +} + +group = "net.thauvin.erik" +description = "Akismet for Kotlin/Java, a client library for accesssing the Automattic Kismet (Akismet) spam comments filtering service." + +val gitHub = "ethauvin/$name" +val mavenUrl = "https://github.com/$gitHub" +val deployDir = "deploy" +var isRelease = "release" in gradle.startParameter.taskNames + +var semverProcessor = "net.thauvin.erik:semver:1.2.0" + +val publicationName = "mavenJava" + +// Load local.properties +File("local.properties").apply { + if (exists()) { + FileInputStream(this).use { fis -> + Properties().apply { + load(fis) + forEach { (k, v) -> + extra[k as String] = v + } + } + } + } +} + +repositories { + jcenter() +} + +dependencies { + kapt(semverProcessor) + implementation(semverProcessor) + + implementation("javax.servlet:javax.servlet-api:4.0.1") + implementation("org.mockito:mockito-core:3.0.0") + + compile("com.squareup.okhttp3:okhttp:4.2.0") + compile("com.squareup.okhttp3:logging-interceptor:4.2.0") + + compile(kotlin("stdlib")) + + testImplementation("org.testng:testng:7.0.0") +} + +kapt { + arguments { + arg("semver.project.dir", projectDir) + } +} + +detekt { + input = files("src/main/kotlin", "src/test/kotlin") + filters = ".*/resources/.*,.*/build/.*" + baseline = project.rootDir.resolve("detekt-baseline.xml") +} + +kotlinter { + ignoreFailures = false + reporters = arrayOf("html") + experimentalRules = false + disabledRules = arrayOf("import-ordering") +} + +jacoco { + toolVersion = "0.8.3" +} + +sonarqube { + properties { + property("sonar.projectKey", "ethauvin_$name") + property("sonar.sourceEncoding", "UTF-8") + } +} + +val sourcesJar by tasks.creating(Jar::class) { + archiveClassifier.set("sources") + from(sourceSets.getByName("main").allSource) +} + +val javadocJar by tasks.creating(Jar::class) { + dependsOn(tasks.dokka) + from(tasks.dokka) + archiveClassifier.set("javadoc") + description = "Assembles a JAR of the generated Javadoc." + group = JavaBasePlugin.DOCUMENTATION_GROUP +} + +tasks { + withType { + useTestNG() + } + + withType { + reports { + xml.isEnabled = true + html.isEnabled = true + } + } + + withType { + kotlinOptions.jvmTarget = "1.8" + } + + withType { + destination = file("$projectDir/pom.xml") + } + + assemble { + dependsOn(sourcesJar, javadocJar) + } + + clean { + doLast { + project.delete(fileTree(deployDir)) + } + } + + dokka { + outputFormat = "html" + outputDirectory = "$buildDir/javadoc" + jdkVersion = 8 + val mapping = LinkMapping().apply { + dir = "src/main/kotlin" + url = "https://github.com/ethauvin/$name/blob/${project.version}/src/main/kotlin" + suffix = "#L" + } + linkMappings = arrayListOf(mapping) + includeNonPublic = false + } + + val copyToDeploy by registering(Copy::class) { + from(configurations.runtime) { + exclude("annotations-*.jar") + } + from(jar) + into(deployDir) + } + + register("deploy") { + description = "Copies all needed files to the $deployDir directory." + group = PublishingPlugin.PUBLISH_TASK_GROUP + dependsOn("build", "jar") + outputs.dir(deployDir) + inputs.files(copyToDeploy) + mustRunAfter("clean") + } + + val gitIsDirty by registering(Exec::class) { + description = "Fails if git has uncommitted changes." + group = "verification" + commandLine("git", "diff", "--quiet", "--exit-code") + } + + val gitTag by registering(Exec::class) { + description = "Tags the local repository with version ${project.version}" + group = PublishingPlugin.PUBLISH_TASK_GROUP + dependsOn(gitIsDirty) + if (isRelease) { + commandLine("git", "tag", "-a", project.version, "-m", "Version ${project.version}") + } + } + + val bintrayUpload by existing(BintrayUploadTask::class) { + dependsOn(publishToMavenLocal, gitTag) + } + + buildScan { + termsOfServiceUrl = "https://gradle.com/terms-of-service" + termsOfServiceAgree = "yes" + } + + register("release") { + description = "Publishes version ${project.version} to Bintray." + group = PublishingPlugin.PUBLISH_TASK_GROUP + dependsOn("wrapper", bintrayUpload) + } + + "sonarqube" { + dependsOn("jacocoTestReport") + } +} + +fun findProperty(s: String) = project.findProperty(s) as String? +bintray { + user = findProperty("bintray.user") + key = findProperty("bintray.apikey") + publish = isRelease + setPublications(publicationName) + pkg.apply { + repo = "maven" + name = project.name + desc = description + websiteUrl = mavenUrl + issueTrackerUrl = "$mavenUrl/issues" + githubRepo = gitHub + githubReleaseNotesFile = "README.md" + vcsUrl = "$mavenUrl.git" + setLabels("kotlin", "java", "akismet", "comments", "spam", "blog", "automattic", "kismet") + publicDownloadNumbers = true + version.apply { + name = project.version as String + desc = description + vcsTag = project.version as String + gpg.apply { + sign = true + } + } + } +} + +publishing { + publications { + create(publicationName) { + from(components["java"]) + artifact(sourcesJar) + artifact(javadocJar) + pom.withXml { + asNode().apply { + appendNode("name", project.name) + appendNode("description", project.description) + appendNode("url", mavenUrl) + + appendNode("licenses").appendNode("license").apply { + appendNode("name", "BSD 3-Clause") + appendNode("url", "https://opensource.org/licenses/BSD-3-Clause") + } + + appendNode("developers").appendNode("developer").apply { + appendNode("id", "ethauvin") + appendNode("name", "Erik C. Thauvin") + appendNode("email", "erik@thauvin.net") + } + + appendNode("scm").apply { + appendNode("connection", "scm:git:$mavenUrl.git") + appendNode("developerConnection", "scm:git:git@github.com:$gitHub.git") + appendNode("url", mavenUrl) + } + + appendNode("issueManagement").apply { + appendNode("system", "GitHub") + appendNode("url", "$mavenUrl/issues") + } + } + } + } + } +} diff --git a/detekt-baseline.xml b/detekt-baseline.xml new file mode 100644 index 0000000..4588edb --- /dev/null +++ b/detekt-baseline.xml @@ -0,0 +1,9 @@ + + + + + ComplexMethod:Akismet.kt$Akismet$fun checkComment(userIp: String, userAgent: String, referrer: String = "", permalink: String = "", type: String = "", author: String = "", authorEmail: String = "", authorUrl: String = "", content: String = "", dateGmt: String = "", postModifiedGmt: String = "", blogLang: String = "", blogCharset: String = "", userRole: String = "", recheckReason: String = "", isTest: Boolean = false, other: Map<String, String> = emptyMap() ): Boolean + LongMethod:Akismet.kt$Akismet$fun checkComment(userIp: String, userAgent: String, referrer: String = "", permalink: String = "", type: String = "", author: String = "", authorEmail: String = "", authorUrl: String = "", content: String = "", dateGmt: String = "", postModifiedGmt: String = "", blogLang: String = "", blogCharset: String = "", userRole: String = "", recheckReason: String = "", isTest: Boolean = false, other: Map<String, String> = emptyMap() ): Boolean + LongParameterList:Akismet.kt$Akismet$(userIp: String, userAgent: String, referrer: String = "", permalink: String = "", type: String = "", author: String = "", authorEmail: String = "", authorUrl: String = "", content: String = "", dateGmt: String = "", postModifiedGmt: String = "", blogLang: String = "", blogCharset: String = "", userRole: String = "", recheckReason: String = "", isTest: Boolean = false, other: Map<String, String> = emptyMap() ) + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..5c2d1cf Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7c4388a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..83f2acf --- /dev/null +++ b/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..d231ea0 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/5.6.2/userguide/multi_project_builds.html + */ + +rootProject.name = "akismet-kotlin" diff --git a/src/main/kotlin/net/thauvin/erik/akismet/Akismet.kt b/src/main/kotlin/net/thauvin/erik/akismet/Akismet.kt new file mode 100644 index 0000000..828986d --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/akismet/Akismet.kt @@ -0,0 +1,521 @@ +/* + * Akismet.kt + * + * Copyright (c) 2019, Erik C. Thauvin (erik@thauvin.net) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.akismet + +import net.thauvin.erik.semver.Version +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import java.io.IOException +import java.util.logging.Level +import java.util.logging.Logger +import javax.servlet.http.HttpServletRequest + +/** + * Akismet Kotlin/Java Client Library + */ +@Version(properties = "version.properties", type = "kt") +open class Akismet(apiKey: String, blog: String) { + @Suppress("unused") + companion object { + /** A blog comment. */ + const val COMMENT_TYPE_COMMENT = "comment" + /** A top-level forum post. */ + const val COMMENT_TYPE_FORUM_POST = "forum-post" + /** A reply to a top-level forum post. */ + const val COMMENT_TYPE_REPLY = "reply" + /** A blog post. */ + const val COMMENT_TYPE_BLOG_POST = "blog-post" + /** A contact form or feedback form submission. */ + const val COMMENT_TYPE_CONTACT_FORM = "contact-form" + /** A new user account. */ + const val COMMENT_TYPE_SIGNUP = "signup" + /** A message sent between just a few users. */ + const val COMMENT_TYPE_MESSAGE = "message" + /** Administrator role */ + const val ADMIN_ROLE = "administrator" + } + + private val apiEndPoint = "https://%s.akismet.com/1.1/%s" + private val libUserAgent = "${GeneratedVersion.PROJECT}/${GeneratedVersion.VERSION}" + private val verifyMethod = "verify-key" + private var apiKey: String + private var blog: String + private var client: OkHttpClient + + var isValidKey: Boolean = false + private set + @Suppress("MemberVisibilityCanBePrivate") + var proTip: String = "" + private set + @Suppress("MemberVisibilityCanBePrivate") + var error: String = "" + private set + @Suppress("MemberVisibilityCanBePrivate") + var degugHelp: String = "" + private set + + val logger: Logger by lazy { Logger.getLogger(Akismet::class.java.simpleName) } + + init { + require(!apiKey.isBlank() || apiKey.length != 12) { "An Akismet API key must be specified." } + require(!blog.isBlank()) { "A Blog URL must be specified." } + + this.apiKey = apiKey + this.blog = blog + + if (logger.isLoggable(Level.FINE)) { + val logging = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { + override fun log(message: String) { + logger.log(Level.FINE, message) + } + }) + logging.level = HttpLoggingInterceptor.Level.BODY + client = OkHttpClient.Builder().addInterceptor(logging).build() + } else { + client = OkHttpClient() + } + } + + /** + * Key Verification + * + * @see Akismet API + */ + fun verifyKey(): Boolean { + val params = HashMap() + params["key"] = apiKey + params["blog"] = blog + isValidKey = executeMethod(verifyMethod, FormBody.Builder().build()) + return isValidKey + } + + /** + * Comment Check using [HttpServletRequest][request] content. + * + * @see Akismet API + */ + @JvmOverloads + fun checkComment( + request: HttpServletRequest, + permalink: String = "", + type: String = "", + author: String = "", + authorEmail: String = "", + authorUrl: String = "", + content: String = "", + dateGmt: String = "", + postModifiedGmt: String = "", + blogLang: String = "", + blogCharset: String = "", + userRole: String = "", + isTest: Boolean = false, + recheckReason: String = "", + other: Map = emptyMap() + ): Boolean { + return checkComment( + userIp = request.remoteAddr, + userAgent = request.getHeader("User-Agent"), + referrer = request.getHeader("Referer"), + permalink = permalink, + type = type, + author = author, + authorEmail = authorEmail, + authorUrl = authorUrl, + content = content, + dateGmt = dateGmt, + postModifiedGmt = postModifiedGmt, + blogLang = blogLang, + blogCharset = blogCharset, + userRole = userRole, + isTest = isTest, + recheckReason = recheckReason, + other = buildPhpVars(request, other)) + } + + /** + * Comment Check + * + * @see Akismet API + */ + @JvmOverloads + fun checkComment( + userIp: String, + userAgent: String, + referrer: String = "", + permalink: String = "", + type: String = "", + author: String = "", + authorEmail: String = "", + authorUrl: String = "", + content: String = "", + dateGmt: String = "", + postModifiedGmt: String = "", + blogLang: String = "", + blogCharset: String = "", + userRole: String = "", + isTest: Boolean = false, + recheckReason: String = "", + other: Map = emptyMap() + ): Boolean { + + require(!(userIp.isBlank() && userAgent.isBlank())) { "userIp and/or userAgent are required." } + + return executeMethod( + "comment-check", + buildFormBody( + userIp = userIp, + userAgent = userAgent, + referrer = referrer, + permalink = permalink, + type = type, + author = author, + authorEmail = authorEmail, + authorUrl = authorUrl, + content = content, + dateGmt = dateGmt, + postModifiedGmt = postModifiedGmt, + blogLang = blogLang, + blogCharset = blogCharset, + userRole = userRole, + isTest = isTest, + recheckReason = recheckReason, + other = other)) + } + + /** + * Submit Spam + * + * @see Akismet API + */ + @JvmOverloads + fun submitSpam( + request: HttpServletRequest, + permalink: String = "", + type: String = "", + author: String = "", + authorEmail: String = "", + authorUrl: String = "", + content: String = "", + dateGmt: String = "", + postModifiedGmt: String = "", + blogLang: String = "", + blogCharset: String = "", + userRole: String = "", + isTest: Boolean = false, + recheckReason: String = "", + other: Map = emptyMap() + ): Boolean { + return submitSpam( + userIp = request.remoteAddr, + userAgent = request.getHeader("User-Agent"), + referrer = request.getHeader("Referer"), + permalink = permalink, + type = type, + author = author, + authorEmail = authorEmail, + authorUrl = authorUrl, + content = content, + dateGmt = dateGmt, + postModifiedGmt = postModifiedGmt, + blogLang = blogLang, + blogCharset = blogCharset, + userRole = userRole, + isTest = isTest, + recheckReason = recheckReason, + other = buildPhpVars(request, other)) + } + + /** + * Submit Spam (missed spam) + * + * @see Akismet API + */ + @JvmOverloads + fun submitSpam( + userIp: String, + userAgent: String, + referrer: String = "", + permalink: String = "", + type: String = "", + author: String = "", + authorEmail: String = "", + authorUrl: String = "", + content: String = "", + dateGmt: String = "", + postModifiedGmt: String = "", + blogLang: String = "", + blogCharset: String = "", + userRole: String = "", + isTest: Boolean = false, + recheckReason: String = "", + other: Map = emptyMap() + ): Boolean { + return executeMethod( + "submit-spam", + buildFormBody( + userIp = userIp, + userAgent = userAgent, + referrer = referrer, + permalink = permalink, + type = type, + author = author, + authorEmail = authorEmail, + authorUrl = authorUrl, + content = content, + dateGmt = dateGmt, + postModifiedGmt = postModifiedGmt, + blogLang = blogLang, + blogCharset = blogCharset, + userRole = userRole, + isTest = isTest, + recheckReason = recheckReason, + other = other)) + } + + /** + * Submit Ham (false positives) + * + * @see Akismet API + */ + @JvmOverloads + fun submitHam( + request: HttpServletRequest, + permalink: String = "", + type: String = "", + author: String = "", + authorEmail: String = "", + authorUrl: String = "", + content: String = "", + dateGmt: String = "", + postModifiedGmt: String = "", + blogLang: String = "", + blogCharset: String = "", + userRole: String = "", + isTest: Boolean = false, + recheckReason: String = "", + other: Map = emptyMap() + ): Boolean { + return submitHam( + userIp = request.remoteAddr, + userAgent = request.getHeader("User-Agent"), + referrer = request.getHeader("Referer"), + permalink = permalink, + type = type, + author = author, + authorEmail = authorEmail, + authorUrl = authorUrl, + content = content, + dateGmt = dateGmt, + postModifiedGmt = postModifiedGmt, + blogLang = blogLang, + blogCharset = blogCharset, + userRole = userRole, + isTest = isTest, + recheckReason = recheckReason, + other = buildPhpVars(request, other)) + } + + /** + * Submit Ham + * + * @see Akismet API + */ + @JvmOverloads + fun submitHam( + userIp: String, + userAgent: String, + referrer: String = "", + permalink: String = "", + type: String = "", + author: String = "", + authorEmail: String = "", + authorUrl: String = "", + content: String = "", + dateGmt: String = "", + postModifiedGmt: String = "", + blogLang: String = "", + blogCharset: String = "", + userRole: String = "", + isTest: Boolean = false, + recheckReason: String = "", + other: Map = emptyMap() + ): Boolean { + return executeMethod( + "submit-ham", + buildFormBody( + userIp = userIp, + userAgent = userAgent, + referrer = referrer, + permalink = permalink, + type = type, + author = author, + authorEmail = authorEmail, + authorUrl = authorUrl, + content = content, + dateGmt = dateGmt, + postModifiedGmt = postModifiedGmt, + blogLang = blogLang, + blogCharset = blogCharset, + userRole = userRole, + isTest = isTest, + recheckReason = recheckReason, + other = other)) + } + + private fun executeMethod(method: String, formBody: FormBody): Boolean { + val apiUrl = buildApiUrl(method).toHttpUrlOrNull() + if (apiUrl != null) { + val request = Request.Builder().url(apiUrl).post(formBody).header("User-Agent", libUserAgent).build() + try { + val result = client.newCall(request).execute() + proTip = result.header("x-akismet-pro-tip", "").toString() + error = result.header("x-akismet-error", "").toString() + degugHelp = result.header("X-akismet-debug-help", "").toString() + val body = result.body?.string() + if (body != null) { + val response = body.trim() + if (response.equals("valid", true) || + response.equals("true", true) || + response.startsWith("Thanks", true)) { + return true + } + } + } catch (e: IOException) { + logger.log(Level.SEVERE, "An IO error occurred while communicating with the Akismet service.", e) + } + } else { + logger.severe("Invalid API end point URL: $method. The API Key is likely invalid.") + } + return false + } + + private fun buildApiUrl(method: String): String { + if (method == verifyMethod) { + return String.format(apiEndPoint, "rest", method) + } + return String.format(apiEndPoint, apiKey, method) + } + + private fun buildPhpVars(request: HttpServletRequest, other: Map): HashMap { + val params = HashMap() + params["REMOTE_ADDR"] = request.remoteAddr + params["REQUEST_URI"] = request.requestURI + + val names = request.headerNames + while (names.hasMoreElements()) { + val name = names.nextElement() + if (!name.equals("cookie", true)) { + params["HTTP_${name.toUpperCase()}"] = request.getHeader(name) + } + } + + if (other.isEmpty()) { + params.putAll(other) + } + + return params + } + + private fun buildFormBody( + userIp: String, + userAgent: String, + referrer: String, + permalink: String, + type: String, + author: String, + authorEmail: String, + authorUrl: String, + content: String, + dateGmt: String, + postModifiedGmt: String, + blogLang: String, + blogCharset: String, + userRole: String, + isTest: Boolean, + recheckReason: String, + other: Map + ): FormBody { + return FormBody.Builder().apply { + add("blog", blog) + add("user_ip", userIp) + add("user_agent", userAgent) + + if (referrer.isNotBlank()) { + add("referrer", referrer) + } + if (permalink.isNotBlank()) { + add("permalink", permalink) + } + if (type.isNotBlank()) { + add("comment_type", type) + } + if (author.isNotBlank()) { + add("comment_author", author) + } + if (authorEmail.isNotBlank()) { + add("comment_author_email", authorEmail) + } + if (authorUrl.isNotBlank()) { + add("comment_author_url", authorUrl) + } + if (content.isNotBlank()) { + add("comment_content", content) + } + if (dateGmt.isNotBlank()) { + add("comment_date_gmt", dateGmt) + } + if (postModifiedGmt.isNotBlank()) { + add("comment_post_modified_gmt", postModifiedGmt) + } + if (blogLang.isNotBlank()) { + add("blog_lang", blogLang) + } + if (blogCharset.isNotBlank()) { + add("blog_charset", blogCharset) + } + if (userRole.isNotBlank()) { + add("user_role", userRole) + } + if (isTest) { + add("is_test", "true") + } + if (recheckReason.isNotBlank()) { + add("recheck_reason", recheckReason) + } + + other.forEach { (k, v) -> add(k, v) } + }.build() + } +} diff --git a/src/test/kotlin/net/thauvin/erik/akismet/AkismetTest.kt b/src/test/kotlin/net/thauvin/erik/akismet/AkismetTest.kt new file mode 100644 index 0000000..a93f904 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/akismet/AkismetTest.kt @@ -0,0 +1,156 @@ +/* + * AkismetTest.kt + * + * Copyright (c) 2019, Erik C. Thauvin (erik@thauvin.net) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.akismet + +import org.mockito.Mockito +import org.testng.Assert.assertFalse +import org.testng.Assert.assertTrue +import org.testng.Assert.expectThrows +import org.testng.annotations.BeforeClass +import org.testng.annotations.Test +import java.io.File +import java.io.FileInputStream +import java.util.Collections +import java.util.Properties +import java.util.logging.ConsoleHandler +import java.util.logging.Level +import javax.servlet.http.HttpServletRequest + +/** + * The AkismetTest class. + * + * @author Erik C. Thauvin + * @created 2019-09-17 + * @since 1.0 + */ + +fun getApiKey(): String { + var apiKey = System.getenv("AKISMET_API_KEY") ?: "" + if (apiKey.isBlank()) { + val localProps = File("local.properties") + if (localProps.exists()) + localProps.apply { + if (exists()) { + FileInputStream(this).use { fis -> + Properties().apply { + load(fis) + apiKey = getProperty("AKISMET_API_KEY", "") + } + } + } + } + } + return apiKey +} + +class AkismetTest { + private val userIp = "127.0.0.1" + private val userAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2) Gecko/20100115 Firefox/3.6" + private val referrer = "http://www.google.com" + private val permalink = "http://yourblogdomainname.com/blog/post=1" + private val type = "comment" + private val author = "admin" + private val authorEmail = "test@test.com" + private val authorUrl = "http://www.CheckOutMyCoolSite.com" + private val content = "It means a lot that you would take the time to review our software. Thanks again." + private val akismet = Akismet(getApiKey(), "http://erik.thauvin.net/blog/") + private val request = Mockito.mock(HttpServletRequest::class.java) + + @BeforeClass + fun beforeClass() { + with(akismet.logger) { + addHandler(ConsoleHandler().apply { level = Level.FINE }) + level = Level.FINE + } + + Mockito.`when`(request.remoteAddr).thenReturn(userIp) + Mockito.`when`(request.requestURI).thenReturn("/blog/post=1") + Mockito.`when`(request.getHeader("User-Agent")).thenReturn(userAgent) + Mockito.`when`(request.getHeader("Referer")).thenReturn(referrer) + Mockito.`when`(request.getHeader("Cookie")).thenReturn("name=value; name2=value2; name3=value3") + Mockito.`when`(request.getHeader("Accept-Encoding")).thenReturn("gzip") + Mockito.`when`(request.headerNames) + .thenReturn(Collections.enumeration(listOf("User-Agent", "Referer", "Cookie", "Accept-Encoding"))) + } + + @Test + fun constructorTest() { + expectThrows(IllegalArgumentException::class.java) { + Akismet("123456789012", "http://www.foo.com/") + Akismet("", "http://www.foo.com/") + Akismet("123456789012", "") + } + } + + @Test + fun verifyKeyTest() { + assertFalse(akismet.isValidKey, "isValidKey -> false") + assertTrue(akismet.verifyKey(), "verify_key") + assertTrue(akismet.isValidKey, "isValidKey -> true") + } + + @Test + fun checkCommentTest() { +// assertFalse(akismet.checkComment(userIp = userIp, +// userAgent = userAgent, +// referrer = referrer, +// permalink = permalink, +// type = type, +// author = author, +// authorEmail = authorEmail, +// authorUrl = authorUrl, +// content = content, +// userRole = Akismet.ADMIN_ROLE, +// isTest = true), "check_comment -> false") +// +// assertTrue(akismet.checkComment(userIp = userIp, +// userAgent = userAgent, +// referrer = referrer, +// permalink = permalink, +// type = type, +// author = author, +// authorEmail = authorEmail, +// authorUrl = authorUrl, +// content = content, +// isTest = true), "check_comment -> true") + + assertTrue(akismet.checkComment(request, + permalink = permalink, + type = type, + author = author, + authorEmail = authorEmail, + authorUrl = authorUrl, + content = content, + isTest = true), "check_comment(request) -> true") + } +} diff --git a/version.properties b/version.properties new file mode 100644 index 0000000..1ad431e --- /dev/null +++ b/version.properties @@ -0,0 +1,9 @@ +#Generated by the Semver Plugin for Gradle +#Tue Sep 17 13:40:32 PDT 2019 +version.buildmeta= +version.major=0 +version.minor=9 +version.patch=0 +version.prerelease=beta +version.project=Akismet Kotlin +version.semver=0.9.0