├── CNAME ├── docs ├── CNAME ├── robots.txt ├── logo.png ├── favicon.ico ├── logosq.png ├── splash.png ├── companies.png ├── mm_intro1.png ├── mm_intro2.png ├── mm_intro3.png ├── mm_intro4.png ├── mm_intro5.png ├── mm_step1.png ├── mm_step2.png ├── mm_step3.png ├── scoold_mm.png ├── splashdark.png ├── scoold_slack.png ├── scoold_teams.png ├── slack_intro1.png ├── slack_intro2.png ├── slack_intro3.png ├── slack_intro4.png ├── slack_intro5.png ├── slack_step1.png ├── slack_step2.png ├── slack_step3.png ├── slack_step4.png ├── slack_step5.png ├── slack_step6.png ├── slack_step7.png ├── slack_step8.png ├── teams_intro1.png ├── teams_intro2.png ├── teams_intro3.png ├── teams_intro4.png ├── teams_intro5.png ├── teams_step1.png ├── teams_step2.png ├── humans.txt └── slack.svg ├── src ├── main │ ├── resources │ │ ├── templates │ │ │ ├── blank.vm │ │ │ ├── space.vm │ │ │ ├── reply.vm │ │ │ ├── notfound.vm │ │ │ ├── comment.vm │ │ │ ├── apidocs.vm │ │ │ ├── feed.vm │ │ │ ├── error.vm │ │ │ ├── pagination.vm │ │ │ ├── languages.vm │ │ │ ├── revisions.vm │ │ │ ├── search.vm │ │ │ └── tags.vm │ │ ├── META-INF │ │ │ ├── services │ │ │ │ └── com.google.inject.Module │ │ │ ├── native-image │ │ │ │ └── com.erudika │ │ │ │ │ └── scoold │ │ │ │ │ ├── predefined-classes-config.json │ │ │ │ │ ├── jni-config.json │ │ │ │ │ ├── proxy-config.json │ │ │ │ │ └── serialization-config.json │ │ │ └── beans.xml │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── images │ │ │ │ ├── favicon.ico │ │ │ │ ├── logosq.png │ │ │ │ ├── logowhite.png │ │ │ │ ├── maskable512.png │ │ │ │ ├── anon.svg │ │ │ │ ├── logo.svg │ │ │ │ ├── amazon.svg │ │ │ │ └── logosq.svg │ │ │ ├── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ ├── fontawesome-webfont.woff2 │ │ │ │ ├── roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-300.woff │ │ │ │ ├── roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-700.woff │ │ │ │ ├── roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-300.woff2 │ │ │ │ ├── roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-700.woff2 │ │ │ │ ├── roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-regular.woff │ │ │ │ └── roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-regular.woff2 │ │ │ ├── styles │ │ │ │ ├── api.css │ │ │ │ ├── highlighting.css │ │ │ │ ├── rtl.css │ │ │ │ └── dark.css │ │ │ ├── humans.txt │ │ │ ├── sitemap.xml │ │ │ ├── scripts │ │ │ │ ├── emoji-picker.js │ │ │ │ └── inline-attachment.min.js │ │ │ └── service-worker.js │ │ ├── bootstrap │ │ ├── banner.txt │ │ ├── application.properties │ │ ├── themes │ │ │ ├── default.css │ │ │ ├── red.css │ │ │ ├── green.css │ │ │ ├── orange.css │ │ │ ├── blue.css │ │ │ ├── mono.css │ │ │ └── dark.css │ │ └── logback.xml │ ├── java │ │ └── com │ │ │ └── erudika │ │ │ └── scoold │ │ │ ├── core │ │ │ ├── UnapprovedQuestion.java │ │ │ ├── UnapprovedReply.java │ │ │ ├── Reply.java │ │ │ ├── Sticky.java │ │ │ ├── Question.java │ │ │ ├── Feedback.java │ │ │ ├── Badge.java │ │ │ ├── Comment.java │ │ │ ├── Revision.java │ │ │ └── Report.java │ │ │ ├── utils │ │ │ ├── avatars │ │ │ │ ├── AvatarRepository.java │ │ │ │ ├── AvatarFormat.java │ │ │ │ ├── DefaultAvatarRepository.java │ │ │ │ ├── ImgurAvatarRepository.java │ │ │ │ ├── GravatarAvatarGenerator.java │ │ │ │ ├── CloudinaryAvatarRepository.java │ │ │ │ ├── GravatarAvatarRepository.java │ │ │ │ └── AvatarRepositoryProxy.java │ │ │ ├── UnauthorizedException.java │ │ │ ├── BadRequestException.java │ │ │ ├── RequestNotSupportedExceptionHandler.java │ │ │ ├── CoreUtils.java │ │ │ └── ScooldEmailer.java │ │ │ ├── velocity │ │ │ ├── VelocityConfig.java │ │ │ ├── VelocityViewResolver.java │ │ │ └── VelocityLayoutViewResolver.java │ │ │ ├── controllers │ │ │ ├── NotFoundController.java │ │ │ ├── ErrorController.java │ │ │ ├── TermsController.java │ │ │ ├── PrivacyController.java │ │ │ ├── LanguagesController.java │ │ │ ├── RevisionsController.java │ │ │ ├── AboutController.java │ │ │ └── ApiDocsController.java │ │ │ └── api │ │ │ └── WebhooksController.java │ └── java-templates │ │ └── com │ │ └── erudika │ │ └── scoold │ │ └── utils │ │ └── Version.java ├── assembly │ └── zip.xml └── test │ └── java │ └── com │ └── erudika │ └── scoold │ └── utils │ └── avatars │ ├── DefaultAvatarRepositoryTest.java │ ├── ImgurAvatarRepositoryTest.java │ ├── CloudinaryAvatarRepositoryTest.java │ └── GravatarAvatarGeneratorTest.java ├── system.properties ├── Procfile ├── assets ├── logo.png ├── header.png ├── logosq.png └── header-pro.png ├── .dockerignore ├── azuredeploy.parameters.json ├── helm └── scoold │ ├── templates │ ├── configmap.yaml │ ├── service.yaml │ ├── tests │ │ └── test-connection.yaml │ ├── ingress.yaml │ ├── _helpers.tpl │ ├── NOTES.txt │ └── deployment.yaml │ ├── Chart.yaml │ ├── .helmignore │ └── values.yaml ├── .editorconfig ├── app.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── latest.yml │ └── tag.yml ├── .gitignore ├── app.gae.yaml ├── Dockerfile ├── SECURITY.md ├── docker-compose.yml ├── installer.sh ├── gencerts.sh └── azuredeploy.json /CNAME: -------------------------------------------------------------------------------- 1 | scoold.com 2 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | scoold.com -------------------------------------------------------------------------------- /src/main/resources/templates/blank.vm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=21 2 | -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * -------------------------------------------------------------------------------- /src/main/resources/templates/space.vm: -------------------------------------------------------------------------------- 1 | #spacebox($space) -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.google.inject.Module: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -Dserver.port=$PORT $JAVA_OPTS -jar target/*.jar 2 | -------------------------------------------------------------------------------- /src/main/resources/templates/reply.vm: -------------------------------------------------------------------------------- 1 | #answerspage($answerslist $showPost) -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/logo.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/assets/logo.png -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/logosq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/logosq.png -------------------------------------------------------------------------------- /docs/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/splash.png -------------------------------------------------------------------------------- /assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/assets/header.png -------------------------------------------------------------------------------- /assets/logosq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/assets/logosq.png -------------------------------------------------------------------------------- /docs/companies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/companies.png -------------------------------------------------------------------------------- /docs/mm_intro1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/mm_intro1.png -------------------------------------------------------------------------------- /docs/mm_intro2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/mm_intro2.png -------------------------------------------------------------------------------- /docs/mm_intro3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/mm_intro3.png -------------------------------------------------------------------------------- /docs/mm_intro4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/mm_intro4.png -------------------------------------------------------------------------------- /docs/mm_intro5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/mm_intro5.png -------------------------------------------------------------------------------- /docs/mm_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/mm_step1.png -------------------------------------------------------------------------------- /docs/mm_step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/mm_step2.png -------------------------------------------------------------------------------- /docs/mm_step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/mm_step3.png -------------------------------------------------------------------------------- /docs/scoold_mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/scoold_mm.png -------------------------------------------------------------------------------- /docs/splashdark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/splashdark.png -------------------------------------------------------------------------------- /assets/header-pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/assets/header-pro.png -------------------------------------------------------------------------------- /docs/scoold_slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/scoold_slack.png -------------------------------------------------------------------------------- /docs/scoold_teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/scoold_teams.png -------------------------------------------------------------------------------- /docs/slack_intro1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_intro1.png -------------------------------------------------------------------------------- /docs/slack_intro2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_intro2.png -------------------------------------------------------------------------------- /docs/slack_intro3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_intro3.png -------------------------------------------------------------------------------- /docs/slack_intro4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_intro4.png -------------------------------------------------------------------------------- /docs/slack_intro5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_intro5.png -------------------------------------------------------------------------------- /docs/slack_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_step1.png -------------------------------------------------------------------------------- /docs/slack_step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_step2.png -------------------------------------------------------------------------------- /docs/slack_step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_step3.png -------------------------------------------------------------------------------- /docs/slack_step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_step4.png -------------------------------------------------------------------------------- /docs/slack_step5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_step5.png -------------------------------------------------------------------------------- /docs/slack_step6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_step6.png -------------------------------------------------------------------------------- /docs/slack_step7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_step7.png -------------------------------------------------------------------------------- /docs/slack_step8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/slack_step8.png -------------------------------------------------------------------------------- /docs/teams_intro1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/teams_intro1.png -------------------------------------------------------------------------------- /docs/teams_intro2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/teams_intro2.png -------------------------------------------------------------------------------- /docs/teams_intro3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/teams_intro3.png -------------------------------------------------------------------------------- /docs/teams_intro4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/teams_intro4.png -------------------------------------------------------------------------------- /docs/teams_intro5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/teams_intro5.png -------------------------------------------------------------------------------- /docs/teams_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/teams_step1.png -------------------------------------------------------------------------------- /docs/teams_step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/docs/teams_step2.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Allow Docker builds to see the full source tree; only ignore build outputs. 2 | target/ 3 | **/target/ 4 | -------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/images/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/images/logosq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/images/logosq.png -------------------------------------------------------------------------------- /src/main/resources/static/images/logowhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/images/logowhite.png -------------------------------------------------------------------------------- /src/main/resources/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/main/resources/static/images/maskable512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/images/maskable512.png -------------------------------------------------------------------------------- /src/main/resources/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/main/resources/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/main/resources/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/main/resources/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/com.erudika/scoold/predefined-classes-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type":"agent-extracted", 4 | "classes":[ 5 | ] 6 | } 7 | ] 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ${LAMBDA_TASK_ROOT:-.} 4 | export SCOOLD_VERSION 5 | 6 | ./scoold -Dconfig.file=application.conf -Dspring.main.banner-mode=off -Dserver.port=8000 7 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | __ __ 2 | ______________ ____ / /___/ / 3 | / ___/ ___/ __ \/ __ \/ / __ / 4 | (__ ) /__/ /_/ / /_/ / / /_/ / 5 | /____/\___/\____/\____/_/\__,_/ ${application.formatted-version} 6 | -------------------------------------------------------------------------------- /src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-300.woff -------------------------------------------------------------------------------- /src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-700.woff -------------------------------------------------------------------------------- /src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-300.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-700.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-regular.woff -------------------------------------------------------------------------------- /src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erudika/scoold/HEAD/src/main/resources/static/fonts/roboto-v20-vietnamese_greek-ext_cyrillic-ext_latin-ext_latin_greek_cyrillic-regular.woff2 -------------------------------------------------------------------------------- /src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/templates/notfound.vm: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Error 5 |
6 |

404 - $!lang.get("pagenotfound")

7 |
8 |
9 |
10 | 11 | -------------------------------------------------------------------------------- /azuredeploy.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "name": { 6 | "value": "scoold" 7 | }, 8 | "image": { 9 | "value": "erudikaltd/scoold:latest_stable" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /helm/scoold/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.applicationConf }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "scoold.fullname" . }}-config 6 | labels: 7 | {{- include "scoold.labels" . | nindent 4 }} 8 | data: 9 | application.conf: | 10 | {{- nindent 4 .Values.applicationConf }} 11 | {{- end }} 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /app.yml: -------------------------------------------------------------------------------- 1 | # DigitalOcean installer.71m.us 2 | 3 | name: Scoold 4 | image: ubuntu-18-10-x64 5 | min_size: 1gb 6 | config: 7 | users: 8 | - name: ubuntu 9 | groups: sudo 10 | shell: /bin/bash 11 | packages: 12 | - wget 13 | - openjdk-11-jre 14 | runcmd: 15 | - "cd /home/ubuntu/ && wget https://raw.githubusercontent.com/Erudika/scoold/master/installer.sh && bash installer.sh" 16 | -------------------------------------------------------------------------------- /helm/scoold/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | type: application 3 | appVersion: 1.65.0 4 | description: Scoold - Stack Overflow in a JAR 5 | name: scoold 6 | home: https://scoold.com 7 | sources: 8 | - https://github.com/Erudika/scoold 9 | icon: https://scoold.com/logosq.png 10 | version: 0.3.0 11 | keywords: 12 | - qa 13 | - q-and-a 14 | - knowledge-base 15 | - scoold 16 | - stackoverflow 17 | - forum 18 | - questions 19 | - answers 20 | -------------------------------------------------------------------------------- /src/main/resources/templates/comment.vm: -------------------------------------------------------------------------------- 1 |
2 | #if(!$scooldUtils.isAjaxRequest($request)) 3 |

$!lang.get("comment.title")

4 | #end 5 | 6 | #if ($showComment) 7 | #commentbox($showComment) 8 | #elseif($commentslist && $commentslist.size() > 0) 9 | #paginate("comments" $itemcount "" "page" ) 10 | #elseif(!$scooldUtils.isAjaxRequest($request)) 11 | $!lang.get("search.notfound") 12 | #end 13 |
-------------------------------------------------------------------------------- /helm/scoold/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /helm/scoold/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "scoold.fullname" . }} 5 | labels: 6 | {{- include "scoold.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: {{ .Values.service.name }} 14 | selector: 15 | {{- include "scoold.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /helm/scoold/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "scoold.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "scoold.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox:1.36 13 | command: 14 | - sh 15 | - -c 16 | args: 17 | - wget --spider --timeout=5 "{{ include "scoold.fullname" . }}:{{ .Values.service.port }}" 18 | restartPolicy: Never 19 | -------------------------------------------------------------------------------- /src/main/resources/static/images/anon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/humans.txt: -------------------------------------------------------------------------------- 1 | # humanstxt.org 2 | # The humans responsible & technology colophon 3 | __ __ 4 | ______________ ____ / /___/ / 5 | / ___/ ___/ __ \/ __ \/ / __ / 6 | (__ ) /__/ /_/ / /_/ / / /_/ / 7 | /____/\___/\____/\____/_/\__,_/ 8 | 9 | # TEAM 10 | 11 | Alex Bogdanovski 12 | Contact: alex [at] erudika.com 13 | Twitter: @albogdano 14 | 15 | # THANKS 16 | 17 | ParaIO.com - backend 18 | GitHub.com - hosting 19 | 20 | # TECHNOLOGY COLOPHON 21 | 22 | HTML5, CSS3 23 | Materialize CSS, jQuery, Para 24 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/com.erudika/scoold/jni-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"java.lang.Boolean", 4 | "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] 5 | }, 6 | { 7 | "name":"sun.management.VMManagementImpl", 8 | "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **PR Checklist** 2 | 3 | - [ ] I have read the **[CONTRIBUTING](../CONTRIBUTING.md)** document. 4 | - [ ] I checked to ensure there aren't other open [Pull Requests](https://github.com/Erudika/scoold/pulls) or issues on the same topic? 5 | - [ ] I have updated the documentation accordingly (if necessary at all). 6 | - [ ] My code follows the code style of this project and **does not** alter whitespace formatting. 7 | - [ ] My pull request **does not** introduce features which are already implemented in Scoold Pro. 8 | 9 | *Large PRs with changes to more than 20 files are strongly discouraged but will be reviewed.* -------------------------------------------------------------------------------- /src/main/resources/templates/apidocs.vm: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 21 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Spring Boot config properties ONLY - edit "application.conf" instead 2 | logging.level.com.erudika.para.client = INFO 3 | # used for backup and restore 4 | spring.mvc.async.request-timeout=-1 5 | spring.servlet.multipart.max-file-size=-1 6 | spring.servlet.multipart.max-request-size=-1 7 | # response compression 8 | server.compression.enabled=true 9 | server.compression.min-response-size=128 10 | server.compression.mime-types=text/html, text/xml, text/plain, text/css, text/javascript, application/javascript, application/json 11 | # prevents occasional auth failures with code 431 12 | server.max-http-request-header-size=50KB 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.class 3 | *.jar 4 | *.zip 5 | *.bak 6 | *.log 7 | keys.txt 8 | application.conf 9 | application-dev.conf 10 | reference.conf 11 | nbactions.xml 12 | Tomcat.dpf 13 | licenseheader.txt 14 | nb-configuration.xml 15 | nbactions*.xml 16 | nbactions.xml 17 | rebel.xml 18 | scoold-search/dist/ 19 | scoold-search/build/ 20 | scoold-search/nbproject/ 21 | scoold-search/lib/ 22 | scoold-search/target/ 23 | target/ 24 | tmp/ 25 | src/main/webapp/WEB-INF/lib/** 26 | src/web/src/main/webapp/WEB-INF/classes/ 27 | src/main/webapp/click/ 28 | data/ 29 | docs-src/ 30 | old/ 31 | deploy*.sh 32 | *.log 33 | demo.conf 34 | template.yml 35 | taillogs-demo.sh 36 | 37 | AGENTS.md -------------------------------------------------------------------------------- /src/main/resources/templates/feed.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${baseurl}feed.xml 4 | $title 5 | $description 6 | $updated 7 | 8 | #foreach($entry in $entries) 9 | 10 | $entry.id 11 | $entry.title 12 | $entry.body 13 | $entry.url 14 | $entry.created 15 | 16 | #end 17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/static/styles/api.css: -------------------------------------------------------------------------------- 1 | html 2 | { 3 | box-sizing: border-box; 4 | overflow: -moz-scrollbars-vertical; 5 | overflow-y: scroll; 6 | } 7 | *, 8 | *:before, 9 | *:after 10 | { 11 | box-sizing: inherit; 12 | } 13 | body 14 | { 15 | margin:0; 16 | } 17 | .main span.url, .topbar { 18 | display: none; 19 | } 20 | .swagger-ui .auth-wrapper { 21 | display: block; 22 | } 23 | .swagger-ui .scheme-container,.swagger-ui .global-server-container { 24 | box-shadow: none; 25 | padding: 0; 26 | } 27 | .swagger-ui .download-contents { 28 | width: 100px; 29 | } 30 | .swagger-ui .auth-container input[type=password], .swagger-ui .auth-container input[type=text] { 31 | width: 100%; 32 | } 33 | -------------------------------------------------------------------------------- /app.gae.yaml: -------------------------------------------------------------------------------- 1 | runtime: java 2 | env: flex 3 | handlers: 4 | - url: /.* 5 | script: scoold.jar 6 | env_variables: 7 | # --SCOOLD CONFIG VARS-- 8 | scoold_env: production 9 | scoold_para_endpoint: 10 | scoold_para_access_key: 11 | scoold_para_secret_key: 12 | scoold_host_url: 13 | 14 | scoold_fb_app_id: 15 | scoold_fb_secret: 16 | scoold_gp_app_id: 17 | scoold_gp_secret: 18 | 19 | scoold_app_secret_key: 20 | scoold_admins: 21 | scoold_password_auth_enabled: true 22 | scoold_is_default_space_public: true 23 | scoold_new_users_can_comment: true 24 | 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:eclipse-temurin AS deps 2 | 3 | ENV SCOOLD_HOME=/scoold 4 | WORKDIR ${SCOOLD_HOME} 5 | 6 | COPY pom.xml pom.xml 7 | RUN mvn -B dependency:go-offline --fail-never 8 | 9 | FROM deps AS build 10 | 11 | ENV SCOOLD_HOME=/scoold 12 | WORKDIR ${SCOOLD_HOME} 13 | 14 | COPY . . 15 | RUN mvn -B -am -DskipTests=true package && \ 16 | cp target/scoold-*.jar ${SCOOLD_HOME}/scoold.jar 17 | 18 | FROM eclipse-temurin:25-jre-alpine 19 | 20 | ENV SCOOLD_HOME=/scoold 21 | ENV BOOT_SLEEP=0 22 | ENV JAVA_OPTS="" 23 | WORKDIR ${SCOOLD_HOME} 24 | 25 | COPY --from=build ${SCOOLD_HOME}/scoold.jar ./scoold.jar 26 | 27 | EXPOSE 8000 28 | 29 | ENTRYPOINT ["sh", "-c", "sleep $BOOT_SLEEP && exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar scoold.jar \"$@\"", "scoold"] 30 | CMD [] 31 | 32 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Security updates are applied only to the most recent releases. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | To securely report a vulnerability, please send a message to support@erudika.com 10 | 11 | Please **do not** file a GitHub issue for a vulnerability. 12 | 13 | ## Vulnerability Process 14 | 15 | 1. Your report will be acknowledged within two business days. 16 | 2. The team will investigate and update the issue with relevant information. 17 | 3. If the team does not confirm the report, no further action will be taken and the issue will be closed. 18 | 4. If the team confirms the report, the team will take action to fix it immediately. 19 | 20 | ## Bug bounty program 21 | 22 | We **do not have** a bug bounty program and no monetary prizes will be be given for reporting or providing a fix for a security issue. 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/templates/error.vm: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Error 5 |
6 | #if ($code == "403") 7 |

$!lang.get("forbidden")

8 | #elseif ($code == "400") 9 |

$!lang.get("badrequest")

10 | #elseif ($code == "401") 11 |

$!lang.get("msgcode.3")

12 | #elseif ($code == "500") 13 |

$!lang.get("servererror")

14 | #elseif ($code == "503") 15 |

$!lang.get("sitedown")

16 | #else 17 |

$!lang.get("error.title")

18 | #end 19 | 20 | #if($code == "500" && ($reason.contains("ConnectException") || $reason.contains("Connection refused"))) 21 |

No connection to Para backend.

22 | #else 23 | $!status $!reason 24 | #end 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/com.erudika/scoold/proxy-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "interfaces":["java.lang.reflect.ParameterizedType","org.springframework.core.SerializableTypeWrapper$SerializableTypeProxy","java.io.Serializable"] 4 | }, 5 | { 6 | "interfaces":["java.lang.reflect.WildcardType","org.springframework.core.SerializableTypeWrapper$SerializableTypeProxy","java.io.Serializable"] 7 | }, 8 | { 9 | "interfaces":["org.springframework.boot.context.properties.ConfigurationProperties"] 10 | }, 11 | { 12 | "interfaces":["org.springframework.web.bind.annotation.ControllerAdvice"] 13 | }, 14 | { 15 | "interfaces":["org.springframework.web.bind.annotation.PathVariable"] 16 | }, 17 | { 18 | "interfaces":["org.springframework.web.bind.annotation.RequestMapping"] 19 | }, 20 | { 21 | "interfaces":["org.springframework.web.bind.annotation.RequestParam"] 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/com.erudika/scoold/serialization-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "types":[ 3 | { 4 | "name":"ch.qos.logback.classic.model.ConfigurationModel" 5 | }, 6 | { 7 | "name":"ch.qos.logback.classic.model.LoggerModel" 8 | }, 9 | { 10 | "name":"ch.qos.logback.classic.model.RootLoggerModel" 11 | }, 12 | { 13 | "name":"ch.qos.logback.core.model.AppenderModel" 14 | }, 15 | { 16 | "name":"ch.qos.logback.core.model.AppenderRefModel" 17 | }, 18 | { 19 | "name":"ch.qos.logback.core.model.ComponentModel" 20 | }, 21 | { 22 | "name":"ch.qos.logback.core.model.ImplicitModel" 23 | }, 24 | { 25 | "name":"ch.qos.logback.core.model.Model" 26 | }, 27 | { 28 | "name":"ch.qos.logback.core.model.NamedComponentModel" 29 | }, 30 | { 31 | "name":"java.util.ArrayList" 32 | } 33 | ], 34 | "lambdaCapturingTypes":[ 35 | ], 36 | "proxies":[ 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/static/humans.txt: -------------------------------------------------------------------------------- 1 | ._ ._ 2 | ____ ____ ___ ___ / / ____/ / 3 | / ___/ / ___/ / __ \ / __ \ / / / __ / 4 | (__ ) / /__ / /_/ // /_/ / / / / /_/ / 5 | /____/ \___/ \____/ \____/ /_/ \__,_/ 6 | 7 | 8 | /* TEAM */ 9 | 10 | Creator: Alex Bogdanovski 11 | Contact: alex [at] erudika.com 12 | Twitter: @albogdano 13 | 14 | /* THANKS */ 15 | 16 | Inspiration & Help: StackOverflow 17 | Site: https://www.stackoverflow.com 18 | 19 | Logo design: Dimo Trifonov 20 | Twitter: @denull 21 | 22 | /* SITE */ 23 | 24 | Backend: Para - https://paraio.com 25 | Standards: HTML5, CSS3, OpenSearch, RSS 26 | Components: ParaIO.com, JS, CSS, HTML5 (clientside), Java (serverside) 27 | Software: Spring, Amazon DynamoDB, Elasticsearch, Heroku, jQuery 28 | Fonts: FontAwesome 29 | 30 | 31 | ___________ 32 | |.--. .--.| 33 | | || |-| () | 34 | |`--' `--'| 35 | | ___ | 36 | \___________/ 37 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/UnapprovedQuestion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.core; 19 | 20 | /** 21 | * 22 | * @author Alex Bogdanovski [alex@erudika.com] 23 | */ 24 | public class UnapprovedQuestion extends Question { 25 | private static final long serialVersionUID = 1L; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/UnapprovedReply.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright 2013-2022 Erudika. https://erudika.com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | * For issues and patches go to: https://github.com/erudika 18 | */ 19 | package com.erudika.scoold.core; 20 | 21 | /** 22 | * 23 | * @author Alex Bogdanovski [alex@erudika.com] 24 | */ 25 | public class UnapprovedReply extends Reply { 26 | private static final long serialVersionUID = 1L; 27 | } 28 | -------------------------------------------------------------------------------- /src/assembly/zip.xml: -------------------------------------------------------------------------------- 1 | 4 | native-zip 5 | 6 | zip 7 | 8 | 9 | 10 | 11 | src/main/resources 12 | / 13 | true 14 | 0775 15 | 16 | bootstrap 17 | 18 | 19 | 20 | target 21 | / 22 | true 23 | 0775 24 | 25 | scoold 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/Reply.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.core; 19 | 20 | /** 21 | * 22 | * @author Alex Bogdanovski [alex@erudika.com] 23 | */ 24 | public class Reply extends Post { 25 | private static final long serialVersionUID = 1L; 26 | 27 | public Reply() { 28 | super(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/static/styles/highlighting.css: -------------------------------------------------------------------------------- 1 | /*************************** 2 | * Syntax highlighting CSS * 3 | ***************************/ 4 | /* https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/github.min.css */ 5 | .hljs{display:block;overflow-x:auto;color:#333;background:inherit;}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:bold}.hljs-number,.hljs-literal,.hljs-variable,.hljs-template-variable,.hljs-tag .hljs-attr{color:#008080}.hljs-string,.hljs-doctag{color:#d14}.hljs-title,.hljs-section,.hljs-selector-id{color:#900;font-weight:bold}.hljs-subst{font-weight:normal}.hljs-type,.hljs-class .hljs-title{color:#458;font-weight:bold}.hljs-tag,.hljs-name,.hljs-attribute{color:#000080;font-weight:normal}.hljs-regexp,.hljs-link{color:#009926}.hljs-symbol,.hljs-bullet{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/avatars/AvatarRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.scoold.core.Profile; 21 | 22 | public interface AvatarRepository { 23 | String getLink(Profile profile, AvatarFormat format); 24 | String getAnonymizedLink(String data); 25 | 26 | boolean store(Profile profile, String url); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/Sticky.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.core; 19 | 20 | /** 21 | * Sticky post - a question which is pinned to the top of the page. 22 | * @author Alex Bogdanovski [alex@erudika.com] 23 | */ 24 | public class Sticky extends Post { 25 | private static final long serialVersionUID = 1L; 26 | 27 | public Sticky() { 28 | super(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/templates/pagination.vm: -------------------------------------------------------------------------------- 1 | #set($Integer = 0) 2 | #evaluate($scooldUtils.getMacroCode($!request.getParameter('pageMacroCode'))) 3 | #if($request.getParameter("page")) 4 | #set($page = $scooldUtils.getPager("page", $request).getPage()) 5 | #if($page > $MAX_PAGES) #set($pnext = 1) #else #set($pnext = $page + 1) #end 6 | 7 | #stop 8 | #end 9 | #if($request.getParameter("page1")) 10 | #set($page1 = $scooldUtils.getPager("page1", $request).getPage()) 11 | #if($page1 > $MAX_PAGES) #set($pnext = 1) #else #set($pnext = $page1 + 1) #end 12 | 13 | #stop 14 | #end 15 | #if($request.getParameter("page2")) 16 | #set($page2 = $scooldUtils.getPager("page2", $request).getPage()) 17 | #if($page2 > $MAX_PAGES) #set($pnext = 1) #else #set($pnext = $page2 + 1) #end 18 | 19 | #stop 20 | #end 21 | #if($request.getParameter("page3")) 22 | #set($page3 = $scooldUtils.getPager("page3", $request).getPage()) 23 | #if($page3 > $MAX_PAGES) #set($pnext = 1) #else #set($pnext = $page3 + 1) #end 24 | 25 | #stop 26 | #end -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/Question.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.core; 19 | 20 | import java.util.LinkedList; 21 | 22 | /** 23 | * 24 | * @author Alex Bogdanovski [alex@erudika.com] 25 | */ 26 | public class Question extends Post { 27 | private static final long serialVersionUID = 1L; 28 | 29 | public Question() { 30 | super(); 31 | setTags(new LinkedList()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/UnauthorizedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils; 19 | 20 | /** 21 | * 22 | * @author Alex Bogdanovski [alex@erudika.com] 23 | */ 24 | public class UnauthorizedException extends RuntimeException { 25 | 26 | private static final long serialVersionUID = 1L; 27 | 28 | public UnauthorizedException() { 29 | super("401 Unauthorized"); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/avatars/AvatarFormat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | public enum AvatarFormat { 21 | Square25(25), 22 | Square32(32), 23 | Square50(50), 24 | Square127(127), 25 | Profile(404); 26 | 27 | private final int size; 28 | 29 | AvatarFormat(int size) { 30 | this.size = size; 31 | } 32 | 33 | public int getSize() { 34 | return this.size; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/BadRequestException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils; 19 | 20 | /** 21 | * 22 | * @author Alex Bogdanovski [alex@erudika.com] 23 | */ 24 | public class BadRequestException extends RuntimeException { 25 | 26 | private static final long serialVersionUID = 1L; 27 | 28 | public BadRequestException() { 29 | super("400 Bad request"); 30 | } 31 | 32 | public BadRequestException(String msg) { 33 | super(msg); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/templates/languages.vm: -------------------------------------------------------------------------------- 1 |
2 |

$!lang.get("translate.select")

3 | 4 |
5 | #foreach($key in $allLocales.keySet()) 6 |
7 | #set($value = $allLocales.get($key) ) 8 | #set($showLangKey = $key) 9 | #set($showLangName = $!value.getDisplayName($!value)) 10 | #set($progress = $!langProgressMap.getOrDefault($!key, 0)) 11 | 12 | #if ($showLangKey.equals($currentLocale.toString())) #set($selclass = "green white-text") #else #set($selclass = "") #end 13 |
14 |
15 | #sectoken(false) 16 | 19 |
20 |
21 | 28 |
29 | #end 30 |
31 |
-------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/Feedback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.core; 19 | 20 | /** 21 | * 22 | * @author Alex Bogdanovski [alex@erudika.com] 23 | */ 24 | public class Feedback extends Post { 25 | private static final long serialVersionUID = 1L; 26 | 27 | public Feedback() { 28 | super(); 29 | } 30 | 31 | public enum FeedbackType { 32 | BUG, 33 | QUESTION, 34 | SUGGESTION; 35 | 36 | public String toString() { 37 | return super.toString().toLowerCase(); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | para: 5 | image: erudikaltd/para:latest_stable 6 | ports: 7 | - "8080:8080" 8 | volumes: 9 | - type: volume 10 | source: paraData 11 | target: /para/data 12 | - type: volume 13 | source: paraLib 14 | target: /para/lib 15 | - type: bind 16 | source: ./para-application.conf 17 | target: /para/application.conf 18 | restart: always 19 | environment: 20 | - JAVA_OPTS=-Dconfig.file=/para/application.conf -Dloader.path=/para/lib 21 | 22 | scoold: 23 | depends_on: 24 | - para 25 | image: erudikaltd/scoold:latest_stable 26 | ports: 27 | - "8000:8000" 28 | volumes: 29 | - type: bind 30 | source: ./scoold-application.conf 31 | target: /scoold/application.conf 32 | - type: bind 33 | source: ./para-application.conf 34 | target: /scoold/para-application.conf 35 | restart: always 36 | environment: 37 | - JAVA_OPTS=-Dconfig.file=/scoold/application.conf -Dscoold.autoinit.para_config_file=/scoold/para-application.conf -Dscoold.para_endpoint=http://para:8080 38 | - BOOT_SLEEP=5 39 | volumes: 40 | paraData: 41 | paraLib: 42 | -------------------------------------------------------------------------------- /src/main/resources/static/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | http://www.scoold.com/ 11 | daily 12 | 13 | 14 | http://www.scoold.com/about 15 | daily 16 | 17 | 18 | http://www.scoold.com/questions 19 | daily 20 | 21 | 22 | http://www.scoold.com/people 23 | daily 24 | 25 | 26 | http://www.scoold.com/feedback 27 | daily 28 | 29 | 30 | http://www.scoold.com/privacy 31 | daily 32 | 33 | 34 | http://www.scoold.com/terms 35 | daily 36 | 37 | 38 | http://www.scoold.com/languages 39 | daily 40 | 41 | -------------------------------------------------------------------------------- /helm/scoold/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: {{ include "scoold.fullname" . }} 6 | labels: 7 | {{- include "scoold.labels" . | nindent 4 }} 8 | {{- with .Values.ingress.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | {{- with .Values.ingress.className }} 14 | ingressClassName: {{ . }} 15 | {{- end }} 16 | {{- if .Values.ingress.tls }} 17 | tls: 18 | {{- range .Values.ingress.tls }} 19 | - hosts: 20 | {{- range .hosts }} 21 | - {{ . | quote }} 22 | {{- end }} 23 | secretName: {{ .secretName }} 24 | {{- end }} 25 | {{- end }} 26 | rules: 27 | {{- $fullName := include "scoold.fullname" . }} 28 | {{- range .Values.ingress.hosts }} 29 | - host: {{ .host | default $fullName | quote }} 30 | http: 31 | paths: 32 | {{- range .paths }} 33 | - path: {{ .path }} 34 | pathType: {{ .pathType | default "Prefix" }} 35 | backend: 36 | service: 37 | name: {{ $fullName }} 38 | port: 39 | number: {{ $.Values.service.port }} 40 | {{- end }} 41 | {{- end }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /src/main/resources/static/scripts/emoji-picker.js: -------------------------------------------------------------------------------- 1 | /* global picmoPopup */ 2 | 3 | const emojiData = await (await fetch(CONTEXT_PATH + "/scripts/emojibase/data.json")).json(); 4 | const messages = await (await fetch(CONTEXT_PATH + "/scripts/emojibase/messages.json")).json(); 5 | 6 | $(document).on("click", ".emoji-button", function () { 7 | var cont = $(this).closest(".emoji-picker-container"); 8 | if (cont.length) { 9 | if (!cont.data("emoji-picker")) { 10 | //var rootElem = $(this).closest("form").get(0) || cont.get(0); 11 | var picker = picmoPopup.createPopup({ 12 | emojiData: emojiData, 13 | messages: messages, 14 | rootElement: cont.get(0), 15 | showCloseButton: false, 16 | showPreview: false, 17 | showSearch: true, 18 | hideOnClickOutside: true, 19 | showCategoryTabs: false, 20 | hideOnEmojiSelect: false, 21 | autoFocusSearch: true, 22 | emojisPerRow: 10, 23 | visibleRows: 7 24 | }, { 25 | className: "", 26 | position: "auto-end", 27 | triggerElement: this, 28 | referenceElement: this 29 | }); 30 | picker.addEventListener("emoji:select", function (e) { 31 | cont.trigger("emoji:select", e); 32 | }); 33 | cont.data("emoji-picker", picker); 34 | } 35 | cont.data("emoji-picker").toggle(); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /src/main/resources/themes/default.css: -------------------------------------------------------------------------------- 1 | /**** DEFAULT THEME ****/ 2 | /* buttons, headings and links */ 3 | a, .dropdown-content li>a, .dropdown-content li>span { color: #039be5; } 4 | .btn, .btn-large, .navbtn-hover { background-color: #ec407a; } 5 | .btn:focus, .btn-large:focus, .btn-floating:focus { background-color: #ec407a; opacity: 0.7; } 6 | .filter-active { background-color: #039be5; color: white; } 7 | 8 | /* nav bar and footer */ 9 | body, footer.page-footer { background-color: #444444; } 10 | .navbar-color { background-color: #03a9f4; } 11 | 12 | /* switches and inputs */ 13 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:after { background-color: #039be5; } 14 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:before, 15 | [type="radio"].with-gap:checked+span:after { border: 2px solid #039be5; } 16 | .switch label input[type=checkbox]:checked+.lever { background-color: #93dafa; } 17 | .switch label input[type=checkbox]:checked+.lever:after { background-color: #03a9f4; } 18 | 19 | /* progress bars */ 20 | .progress .indeterminate { background-color: #039be5; } 21 | .progress { background-color: #93dafa; } 22 | 23 | /* tabs */ 24 | .tabs .tab a:hover, .tabs .tab a.active, .tabs .tab a { color: #039be5; } 25 | .tabs .indicator { background-color: #039be5; } 26 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/velocity/VelocityConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.erudika.scoold.velocity; 18 | 19 | import org.apache.velocity.app.VelocityEngine; 20 | 21 | /** 22 | * Interface to be implemented by objects that configure and manage a 23 | * VelocityEngine for automatic lookup in a web environment. Detected 24 | * and used by VelocityView. 25 | * 26 | * @author Rod Johnson 27 | * @see VelocityConfigurer 28 | * @see VelocityView 29 | */ 30 | public interface VelocityConfig { 31 | 32 | /** 33 | * Return the VelocityEngine for the current web application context. 34 | * May be unique to one servlet, or shared in the root context. 35 | * @return the VelocityEngine 36 | */ 37 | VelocityEngine getVelocityEngine(); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/themes/red.css: -------------------------------------------------------------------------------- 1 | /**** RED THEME ****/ 2 | /* buttons, headings and links */ 3 | a, .dropdown-content li>a, .dropdown-content li>span { color: #c62828; } 4 | .btn, .btn-large, .navbtn-hover { background-color: #37474f; } 5 | .btn:focus, .btn-large:focus, .btn-floating:focus { background-color: #37474f; opacity: 0.7; } 6 | .filter-active { background-color: #c62828; color: white; } 7 | 8 | /* nav bar and footer */ 9 | body, footer.page-footer { background-color: #333333; } 10 | .navbar-color { background-color: #c62828; } 11 | 12 | /* switches and inputs */ 13 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:after { background-color: #c62828; } 14 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:before, 15 | [type="radio"].with-gap:checked+span:after { border: 2px solid #c62828; } 16 | .switch label input[type=checkbox]:checked+.lever { background-color: #ff5f52; } 17 | .switch label input[type=checkbox]:checked+.lever:after { background-color: #8e0000; } 18 | [type="checkbox"].filled-in:checked+span:not(.lever):after { border: 2px solid #c62828; background-color: #c62828; } 19 | [type="checkbox"]:checked+span:not(.lever):before { border-right: 2px solid #c62828; border-bottom: 2px solid #c62828; } 20 | 21 | /* progress bars */ 22 | .progress .indeterminate { background-color: #c62828; } 23 | .progress { background-color: #ff5f52; } 24 | 25 | /* tabs */ 26 | .tabs .tab a:hover, .tabs .tab a.active, .tabs .tab a { color: #c62828; } 27 | .tabs .indicator { background-color: #c62828; } -------------------------------------------------------------------------------- /src/main/resources/themes/green.css: -------------------------------------------------------------------------------- 1 | /**** GREEN THEME ****/ 2 | /* buttons, headings and links */ 3 | a, .dropdown-content li>a, .dropdown-content li>span { color: #2e7d32; } 4 | .btn, .btn-large, .navbtn-hover { background-color: #4e342e; } 5 | .btn:focus, .btn-large:focus, .btn-floating:focus { background-color: #4e342e; opacity: 0.7; } 6 | .filter-active { background-color: #2e7d32; color: white; } 7 | 8 | /* nav bar and footer */ 9 | body, footer.page-footer { background-color: #260e04; } 10 | .navbar-color { background-color: #2e7d32; } 11 | 12 | /* switches and inputs */ 13 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:after { background-color: #2e7d32; } 14 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:before, 15 | [type="radio"].with-gap:checked+span:after { border: 2px solid #2e7d32; } 16 | .switch label input[type=checkbox]:checked+.lever { background-color: #60ad5e; } 17 | .switch label input[type=checkbox]:checked+.lever:after { background-color: #005005; } 18 | [type="checkbox"].filled-in:checked+span:not(.lever):after { border: 2px solid #2e7d32; background-color: #2e7d32; } 19 | [type="checkbox"]:checked+span:not(.lever):before { border-right: 2px solid #2e7d32; border-bottom: 2px solid #2e7d32; } 20 | 21 | /* progress bars */ 22 | .progress .indeterminate { background-color: #2e7d32; } 23 | .progress { background-color: #60ad5e; } 24 | 25 | /* tabs */ 26 | .tabs .tab a:hover, .tabs .tab a.active, .tabs .tab a { color: #2e7d32; } 27 | .tabs .indicator { background-color: #2e7d32; } -------------------------------------------------------------------------------- /src/main/resources/themes/orange.css: -------------------------------------------------------------------------------- 1 | /**** ORANGE THEME ****/ 2 | /* buttons, headings and links */ 3 | a, .dropdown-content li>a, .dropdown-content li>span { color: #ff9800;} 4 | a, button { font-weight: 400; } 5 | .btn, .btn-large, .navbtn-hover { background-color: #d84315; } 6 | .btn:focus, .btn-large:focus, .btn-floating:focus { background-color: #d84315; } 7 | .filter-active { background-color: #ff9800; color: white; } 8 | 9 | /* nav bar and footer */ 10 | body, footer.page-footer { background-color: #333333; } 11 | .navbar-color { background-color: #ff9800; } 12 | 13 | /* switches and inputs */ 14 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:after { background-color: #ff9800; } 15 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:before, 16 | [type="radio"].with-gap:checked+span:after { border: 2px solid #ff9800; } 17 | .switch label input[type=checkbox]:checked+.lever { background-color: #ff7543; } 18 | .switch label input[type=checkbox]:checked+.lever:after { background-color: #9f0000; } 19 | [type="checkbox"].filled-in:checked+span:not(.lever):after { border: 2px solid #ff9800; background-color: #ff9800; } 20 | [type="checkbox"]:checked+span:not(.lever):before { border-right: 2px solid #ff9800; border-bottom: 2px solid #ff9800; } 21 | 22 | /* progress bars */ 23 | .progress .indeterminate { background-color: #ff9800; } 24 | .progress { background-color: #ff7543; } 25 | 26 | /* tabs */ 27 | .tabs .tab a:hover, .tabs .tab a.active, .tabs .tab a { color: #ff9800; } 28 | .tabs .indicator { background-color: #ff9800; } 29 | -------------------------------------------------------------------------------- /src/main/resources/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/themes/blue.css: -------------------------------------------------------------------------------- 1 | /**** BLUE THEME ****/ 2 | /* buttons, headings and links */ 3 | a, .dropdown-content li>a, .dropdown-content li>span { color: #1565c0; } 4 | .btn, .btn-large, .navbtn-hover { background-color: #4fc3f7; } 5 | .btn:focus, .btn-large:focus, .btn-floating:focus { background-color: #4fc3f7; } 6 | .filter-active { background-color: #1565c0; color: white; } 7 | 8 | /* nav bar and footer */ 9 | body, footer.page-footer { background-color: #efefef; } 10 | .navbar-color { background-color: #1565c0; } 11 | .page-footer .footer-copyright { color: #333333; } 12 | .page-footer .btn-flat { color: white; } 13 | 14 | /* switches and inputs */ 15 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:after { background-color: #1565c0; } 16 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:before, 17 | [type="radio"].with-gap:checked+span:after { border: 2px solid #1565c0; } 18 | .switch label input[type=checkbox]:checked+.lever { background-color: #93dafa; } 19 | .switch label input[type=checkbox]:checked+.lever:after { background-color: #1565c0; } 20 | [type="checkbox"].filled-in:checked+span:not(.lever):after { border: 2px solid #1565c0; background-color: #1565c0; } 21 | [type="checkbox"]:checked+span:not(.lever):before { border-right: 2px solid #1565c0; border-bottom: 2px solid #1565c0; } 22 | 23 | /* progress bars */ 24 | .progress .indeterminate { background-color: #1565c0; } 25 | .progress { background-color: #93dafa; } 26 | 27 | /* tabs */ 28 | .tabs .tab a:hover, .tabs .tab a.active, .tabs .tab a { color: #1565c0; } 29 | .tabs .indicator { background-color: #1565c0; } 30 | -------------------------------------------------------------------------------- /src/main/resources/themes/mono.css: -------------------------------------------------------------------------------- 1 | /**** MONO THEME ****/ 2 | /* buttons, headings and links */ 3 | .dropdown-content li>a, .dropdown-content li>span { color: #424242; } 4 | a { color: #039be5; } 5 | .btn, .btn-large { background-color: #424242; } 6 | .navbtn-hover { background-color: #6d6d6d; } 7 | .btn:focus, .btn-large:focus, .btn-floating:focus { background-color: #424242; } 8 | .filter-active { background-color: #424242; color: white; } 9 | 10 | /* nav bar and footer */ 11 | body, footer.page-footer { background-color: #444444; } 12 | .navbar-color { background-color: #424242; } 13 | .page-footer .footer-copyright { color: white; } 14 | .page-footer .btn-flat { color: white; } 15 | 16 | /* switches and inputs */ 17 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:after { background-color: #424242; } 18 | [type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:before, 19 | [type="radio"].with-gap:checked+span:after { border: 2px solid #424242; } 20 | .switch label input[type=checkbox]:checked+.lever { background-color: #efefef; } 21 | .switch label input[type=checkbox]:checked+.lever:after { background-color: #8d8d8d; } 22 | [type="checkbox"].filled-in:checked+span:not(.lever):after { border: 2px solid #424242; background-color: #424242; } 23 | [type="checkbox"]:checked+span:not(.lever):before { border-right: 2px solid #424242; border-bottom: 2px solid #424242; } 24 | 25 | /* progress bars */ 26 | .progress .indeterminate { background-color: #efefef; } 27 | .progress { background-color: #6d6d6d; } 28 | 29 | /* tabs */ 30 | .tabs .tab a:hover, .tabs .tab a.active, .tabs .tab a { color: #424242; } 31 | .tabs .indicator { background-color: #424242; } 32 | -------------------------------------------------------------------------------- /installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | # Lightsail/DigitalOcean installer script for Ubuntu 5 | VERSION="1.65.0" 6 | PORT="8000" 7 | WORKDIR="/home/ubuntu" 8 | JARURL="https://github.com/Erudika/scoold/releases/download/${VERSION}/scoold-${VERSION}.jar" 9 | sfile="/etc/systemd/system/scoold.service" 10 | 11 | apt-get update && apt-get install -y wget openjdk-21-jre && 12 | wget -O scoold.jar ${JARURL} && \ 13 | mv scoold.jar $WORKDIR && \ 14 | chown ubuntu:ubuntu ${WORKDIR}/scoold.jar && \ 15 | chmod +x ${WORKDIR}/scoold.jar 16 | touch ${WORKDIR}/application.conf && \ 17 | chown ubuntu:ubuntu ${WORKDIR}/application.conf 18 | 19 | # Feel free to modify the Scoold configuration here 20 | cat << EOF > ${WORKDIR}/application.conf 21 | scoold.app_name = "Scoold" 22 | scoold.port = 8000 23 | scoold.env = "production" 24 | scoold.host_url = "http://localhost:8000" 25 | scoold.para_endpoint = "https://paraio.com" 26 | scoold.para_access_key = "app:scoold" 27 | scoold.para_secret_key = "" 28 | scoold.admins = "admin@example.com" 29 | EOF 30 | 31 | touch $sfile 32 | cat << EOF > $sfile 33 | [Unit] 34 | Description=Scoold 35 | After=syslog.target 36 | StartLimitIntervalSec=30 37 | StartLimitBurst=2 38 | [Service] 39 | WorkingDirectory=${WORKDIR} 40 | SyslogIdentifier=Scoold 41 | ExecStart=java -jar -Dconfig.file=application.conf scoold.jar 42 | User=ubuntu 43 | Restart=on-failure 44 | RestartSec=1s 45 | [Install] 46 | WantedBy=multi-user.target 47 | EOF 48 | 49 | # This is optional. These rules might interfere with other web server configurations like nginx and certbot. 50 | #iptables -t nat -A PREROUTING -p tcp -m tcp --dport 80 -j REDIRECT --to-port ${PORT} && \ 51 | #iptables -t nat -A OUTPUT -p tcp --dport 80 -o lo -j REDIRECT --to-port ${PORT} 52 | 53 | systemctl enable scoold.service && \ 54 | systemctl start scoold.service 55 | -------------------------------------------------------------------------------- /helm/scoold/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "scoold.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "scoold.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "scoold.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "scoold.labels" -}} 38 | helm.sh/chart: {{ include "scoold.chart" . }} 39 | app.kubernetes.io/name: {{ include "scoold.name" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | app.kubernetes.io/managed-by: {{ .Release.Service }} 42 | {{- with .Chart.AppVersion }} 43 | app.kubernetes.io/version: {{ . | quote }} 44 | {{- end }} 45 | {{- end -}} 46 | 47 | {{/* 48 | Selector labels 49 | */}} 50 | {{- define "scoold.selectorLabels" -}} 51 | app.kubernetes.io/name: {{ include "scoold.name" . }} 52 | app.kubernetes.io/instance: {{ .Release.Name }} 53 | {{- end -}} 54 | 55 | -------------------------------------------------------------------------------- /src/main/java-templates/com/erudika/scoold/utils/Version.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils; 19 | 20 | /** 21 | * GENERATED CLASS. DO NOT MODIFY! 22 | * @author Alex Bogdanovski [alex@erudika.com] 23 | */ 24 | public final class Version { 25 | 26 | private static final String VERSION = "${project.version}"; 27 | private static final String GROUPID = "${project.groupId}"; 28 | private static final String ARTIFACTID = "${project.artifactId}"; 29 | private static final String GIT = "${project.scm.developerConnection}"; 30 | private static final String GIT_BRANCH = "${scmBranch}"; 31 | private static final String REVISION = "${buildNumber}"; 32 | 33 | public static String getVersion() { 34 | return VERSION; 35 | } 36 | 37 | public static String getArtifactId() { 38 | return ARTIFACTID; 39 | } 40 | 41 | public static String getGroupId() { 42 | return GROUPID; 43 | } 44 | 45 | public static String getGIT() { 46 | return GIT; 47 | } 48 | 49 | public static String getRevision() { 50 | return REVISION; 51 | } 52 | 53 | public static String getGITBranch() { 54 | return GIT_BRANCH; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/controllers/NotFoundController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.controllers; 19 | 20 | import com.erudika.scoold.utils.ScooldUtils; 21 | import jakarta.inject.Inject; 22 | import jakarta.servlet.http.HttpServletRequest; 23 | import jakarta.servlet.http.HttpServletResponse; 24 | import org.springframework.stereotype.Controller; 25 | import org.springframework.ui.Model; 26 | import org.springframework.web.bind.annotation.GetMapping; 27 | import org.springframework.web.bind.annotation.RequestMapping; 28 | 29 | /** 30 | * 31 | * @author Alex Bogdanovski [alex@erudika.com] 32 | */ 33 | @Controller 34 | @RequestMapping("/not-found") 35 | public class NotFoundController { 36 | 37 | private final ScooldUtils utils; 38 | 39 | @Inject 40 | public NotFoundController(ScooldUtils utils) { 41 | this.utils = utils; 42 | } 43 | 44 | @GetMapping 45 | public String get(HttpServletRequest req, HttpServletResponse res, Model model) { 46 | model.addAttribute("path", "notfound.vm"); 47 | model.addAttribute("title", utils.getLang(req).get("notfound.title")); 48 | res.setStatus(404); 49 | return "base"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/velocity/VelocityViewResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.erudika.scoold.velocity; 17 | 18 | import org.springframework.web.servlet.view.AbstractTemplateViewResolver; 19 | import org.springframework.web.servlet.view.AbstractUrlBasedView; 20 | 21 | /** 22 | *

23 | * The view class for all views generated by this resolver can be specified via the "viewClass" property. See 24 | * UrlBasedViewResolver's javadoc for details. 25 | * 26 | *

27 | * Note: When chaining ViewResolvers, a VelocityViewResolver will check for the existence of the specified 28 | * template resources and only return a non-null View object if the template was actually found. 29 | * 30 | * @author Juergen Hoeller 31 | * @since 13.12.2003 32 | */ 33 | public class VelocityViewResolver extends AbstractTemplateViewResolver { 34 | 35 | public VelocityViewResolver() { 36 | setViewClass(requiredViewClass()); 37 | } 38 | 39 | @Override 40 | protected Class requiredViewClass() { 41 | return VelocityView.class; 42 | } 43 | 44 | 45 | @Override 46 | protected AbstractUrlBasedView buildView(String viewName) throws Exception { 47 | VelocityView view = (VelocityView) super.buildView(viewName); 48 | return view; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /helm/scoold/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | __ __ 2 | ______________ ____ / /___/ / 3 | / ___/ ___/ __ \/ __ \/ / __ / 4 | (__ ) /__/ /_/ / /_/ / / /_/ / 5 | /____/\___/\____/\____/_/\__,_/ 6 | 7 | 1. Get the application URL by running these commands: 8 | {{- if .Values.ingress.enabled }} 9 | {{- range $host := .Values.ingress.hosts }} 10 | {{- $hostName := $host.host | default (include "scoold.fullname" $) }} 11 | {{- range $path := $host.paths }} 12 | Visit http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $hostName }}{{ $path.path }} to use your application 13 | {{- end }} 14 | {{- end }} 15 | {{- else if contains "NodePort" .Values.service.type }} 16 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "scoold.fullname" . }}) 17 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 18 | echo http://$NODE_IP:$NODE_PORT 19 | {{- else if contains "LoadBalancer" .Values.service.type }} 20 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 21 | You can watch the status of by running 'kubectl get svc -w {{ include "scoold.fullname" . }}' 22 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "scoold.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 23 | echo http://$SERVICE_IP:{{ .Values.service.port }} 24 | {{- else if contains "ClusterIP" .Values.service.type }} 25 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "scoold.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 26 | kubectl port-forward $POD_NAME {{ .Values.service.port }}:{{ .Values.service.port }} 27 | {{- end }} 28 | 2. Visit http://localhost:{{ .Values.service.port }} to use your application 29 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/avatars/DefaultAvatarRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.scoold.core.Profile; 21 | import com.erudika.scoold.utils.ScooldUtils; 22 | import org.apache.commons.lang3.StringUtils; 23 | import org.apache.commons.lang3.Strings; 24 | 25 | public class DefaultAvatarRepository implements AvatarRepository { 26 | 27 | public DefaultAvatarRepository() { 28 | } 29 | 30 | @Override 31 | public String getLink(Profile profile, AvatarFormat format) { 32 | return (profile == null || StringUtils.isBlank(profile.getPicture())) ? ScooldUtils.getDefaultAvatar() : profile.getPicture(); 33 | } 34 | 35 | @Override 36 | public String getAnonymizedLink(String data) { 37 | return ScooldUtils.getDefaultAvatar(); 38 | } 39 | 40 | @Override 41 | public boolean store(Profile profile, String url) { 42 | if (StringUtils.isBlank(url) || !url.equalsIgnoreCase(profile.getOriginalPicture())) { 43 | if (Strings.CI.startsWith(url, ScooldUtils.getConfig().serverUrl())) { 44 | profile.setPicture(url); 45 | } else { 46 | profile.setPicture(ScooldUtils.getDefaultAvatar()); 47 | } 48 | } else { 49 | profile.setPicture(profile.getOriginalPicture()); 50 | } 51 | return true; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/latest.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Docker Hub - latest 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - name: Check Out Repo 24 | uses: actions/checkout@v4 25 | 26 | - name: Login to Docker Hub 27 | uses: docker/login-action@v3 28 | with: 29 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 30 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 31 | 32 | - name: Set up Docker Buildx 33 | id: buildx 34 | uses: docker/setup-buildx-action@v3 35 | 36 | - name: Build and push 37 | id: docker_build 38 | uses: docker/build-push-action@v6 39 | with: 40 | context: ./ 41 | file: ./Dockerfile 42 | builder: ${{ steps.buildx.outputs.name }} 43 | push: true 44 | tags: erudikaltd/scoold:latest 45 | cache-from: type=local,src=/tmp/.buildx-cache 46 | cache-to: type=local,dest=/tmp/.buildx-cache 47 | 48 | - name: Image digest 49 | run: echo ${{ steps.docker_build.outputs.digest }} 50 | 51 | - name: Cache Docker layers 52 | uses: actions/cache@v4 53 | with: 54 | path: /tmp/.buildx-cache 55 | key: ${{ runner.os }}-buildx-${{ github.sha }} 56 | restore-keys: | 57 | ${{ runner.os }}-buildx- 58 | -------------------------------------------------------------------------------- /src/test/java/com/erudika/scoold/utils/avatars/DefaultAvatarRepositoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.scoold.core.Profile; 21 | import com.erudika.scoold.utils.ScooldUtils; 22 | import static org.junit.Assert.*; 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | 26 | public class DefaultAvatarRepositoryTest { 27 | private DefaultAvatarRepository repository; 28 | 29 | @Before 30 | public void setUp(){ 31 | this.repository = new DefaultAvatarRepository(); 32 | } 33 | 34 | @Test 35 | public void getLink_should_return_always_default_avatar() { 36 | Profile profile = new Profile(); 37 | assertEquals(ScooldUtils.getDefaultAvatar(), repository.getLink(profile, AvatarFormat.Profile)); 38 | assertEquals(repository.getLink(new Profile(), AvatarFormat.Square32), repository.getLink(new Profile(), AvatarFormat.Profile)); 39 | } 40 | 41 | @Test 42 | public void getAnonymizedLink_should_always_return_default_avatar() { 43 | assertEquals(ScooldUtils.getDefaultAvatar(), repository.getAnonymizedLink("A")); 44 | assertEquals(repository.getAnonymizedLink("A"), repository.getAnonymizedLink("B")); 45 | } 46 | 47 | @Test 48 | public void store_should_nothing() { 49 | Profile profile = new Profile(); 50 | 51 | boolean result = repository.store(profile, "https://avatar"); 52 | assertEquals(ScooldUtils.getDefaultAvatar(), profile.getPicture()); 53 | assertEquals(true, result); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/avatars/ImgurAvatarRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.scoold.core.Profile; 21 | import com.erudika.scoold.utils.ScooldUtils; 22 | import org.apache.commons.lang3.StringUtils; 23 | import org.apache.commons.lang3.Strings; 24 | 25 | 26 | public class ImgurAvatarRepository implements AvatarRepository { 27 | 28 | private final AvatarRepository nextRepository; 29 | 30 | public ImgurAvatarRepository(AvatarRepository nextRepository) { 31 | this.nextRepository = nextRepository; 32 | } 33 | 34 | @Override 35 | public String getLink(Profile profile, AvatarFormat format) { 36 | if (profile == null || StringUtils.isBlank(profile.getPicture())) { 37 | return nextRepository.getLink(profile, format); 38 | } 39 | String picture = profile.getPicture(); 40 | if (isImgurLink(picture)) { 41 | return picture; 42 | } 43 | return ScooldUtils.getDefaultAvatar(); 44 | } 45 | 46 | private boolean isImgurLink(String picture) { 47 | return Strings.CI.startsWith(picture, "https://i.imgur.com/"); 48 | } 49 | 50 | @Override 51 | public String getAnonymizedLink(String data) { 52 | return nextRepository.getAnonymizedLink(data); 53 | } 54 | 55 | @Override 56 | public boolean store(Profile profile, String url) { 57 | if (!isImgurLink(url)) { 58 | return nextRepository.store(profile, url); 59 | } 60 | profile.setPicture(url); 61 | return true; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Docker Hub - tagged 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | tags: 10 | - "*.*.*" 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - name: Check Out Repo 25 | uses: actions/checkout@v4 26 | - name: Set env 27 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 28 | 29 | - name: Login to Docker Hub 30 | uses: docker/login-action@v3 31 | with: 32 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 33 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 34 | 35 | - name: Set up Docker Buildx 36 | id: buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: Build and push 40 | id: docker_build 41 | uses: docker/build-push-action@v6 42 | with: 43 | context: ./ 44 | file: ./Dockerfile 45 | builder: ${{ steps.buildx.outputs.name }} 46 | push: true 47 | tags: erudikaltd/scoold:${{ env.RELEASE_VERSION }}, erudikaltd/scoold:latest_stable 48 | cache-from: type=local,src=/tmp/.buildx-cache 49 | cache-to: type=local,dest=/tmp/.buildx-cache 50 | 51 | - name: Image digest 52 | run: echo ${{ steps.docker_build.outputs.digest }} 53 | 54 | - name: Cache Docker layers 55 | uses: actions/cache@v4 56 | with: 57 | path: /tmp/.buildx-cache 58 | key: ${{ runner.os }}-buildx-${{ github.sha }} 59 | restore-keys: | 60 | ${{ runner.os }}-buildx- 61 | -------------------------------------------------------------------------------- /gencerts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NAME=$1 # Use your own domain name 4 | SECRET=$2 # Keystore password 5 | CAFILE=$3 # File name of the CA cert and key 6 | 7 | read -e -p "Certificate alias: " alias 8 | alias=$(echo "$alias" | awk '{print tolower($0)}') 9 | ###################### 10 | # Become a CA or use existing one 11 | ###################### 12 | 13 | if [[ -z "$SECRET" ]]; then 14 | SECRET="secret" 15 | fi 16 | 17 | if [[ -z "$CAFILE" ]]; then 18 | CAFILE="${alias^}RootCA" 19 | # Generate root certificate 20 | openssl req -x509 -new -nodes -sha256 -days 1024 -newkey rsa:2048 -keyout $CAFILE.key -out $CAFILE.pem -subj "/C=BG/CN=$CAFILE" 21 | # Create a Windows-compatible crt file 22 | openssl x509 -outform pem -in $CAFILE.pem -out $CAFILE.crt 23 | fi 24 | 25 | ###################### 26 | # Create CA-signed certs 27 | ###################### 28 | 29 | # Create a certificate-signing request 30 | openssl req -new -nodes -newkey rsa:2048 -keyout $NAME.key -out $NAME.csr -subj "/C=BG/ST=EU/L=Sofia/O=Erudika/CN=$NAME" 31 | # Create a config file for the extensions 32 | >$NAME.ext cat <<-EOF 33 | authorityKeyIdentifier=keyid,issuer 34 | basicConstraints=CA:FALSE 35 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 36 | subjectAltName = @alt_names 37 | [alt_names] 38 | DNS.1 = $NAME # Be sure to include the domain name here because Common Name is not so commonly honoured by itself 39 | #IP.1 = 192.168.0.10 # Optionally, add an IP address (if the connection which you have planned requires it) 40 | EOF 41 | # Create the signed certificate 42 | openssl x509 -req -sha256 -days 1024 -in $NAME.csr -CA $CAFILE.pem -CAkey $CAFILE.key -CAcreateserial -extfile $NAME.ext -out $NAME.pem 43 | # Create a Windows-compatible crt file 44 | openssl x509 -outform pem -in $NAME.pem -out $NAME.crt 45 | # Clean up 46 | rm $NAME.csr $NAME.ext 47 | 48 | ###################### 49 | # Create Java Keystore 50 | ###################### 51 | openssl pkcs12 -export -out ${alias}-keystore.p12 -in $NAME.pem -inkey $NAME.key -name ${alias} -passin pass:$SECRET -passout pass:$SECRET 52 | 53 | ###################### 54 | # Create Java Truststore 55 | ###################### 56 | keytool -v -importcert -file $CAFILE.pem -alias root-ca -keystore ${alias}-truststore.p12 -storepass $SECRET -noprompt 57 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/avatars/GravatarAvatarGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.para.core.utils.Utils; 21 | import com.erudika.scoold.core.Profile; 22 | import com.erudika.scoold.utils.ScooldUtils; 23 | import jakarta.inject.Singleton; 24 | import org.apache.commons.lang3.StringUtils; 25 | import org.apache.commons.lang3.Strings; 26 | import org.springframework.stereotype.Component; 27 | 28 | @Component 29 | @Singleton 30 | public class GravatarAvatarGenerator { 31 | private static final String URL_BASE = "https://www.gravatar.com/avatar/"; 32 | 33 | public GravatarAvatarGenerator() { 34 | } 35 | 36 | public String getLink(Profile profile, AvatarFormat format) { 37 | return configureLink(getRawLink(profile), format); 38 | } 39 | 40 | public String getRawLink(Profile profile) { 41 | String email = (profile == null || profile.getUser() == null) ? "" : profile.getUser().getEmail(); 42 | return getRawLink(email); 43 | } 44 | 45 | public String getRawLink(String email) { 46 | return URL_BASE + computeToken(email); 47 | } 48 | 49 | private String computeToken(String email) { 50 | if (StringUtils.isBlank(email)) { 51 | return ""; 52 | } 53 | 54 | return Utils.md5(email.toLowerCase()); 55 | } 56 | 57 | public String configureLink(String url, AvatarFormat format) { 58 | return url + (url.endsWith("?") ? "&" : "?") + "s=" + format.getSize() + "&r=g&d=" + ScooldUtils.gravatarPattern(); 59 | } 60 | 61 | public boolean isLink(String link) { 62 | return Strings.CS.contains(link, "gravatar.com"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "name": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "Name for the container group" 9 | }, 10 | "defaultValue": "scoold" 11 | }, 12 | "image": { 13 | "type": "string", 14 | "metadata": { 15 | "description": "Container image to deploy." 16 | }, 17 | "defaultValue": "erudikaltd/scoold:latest_stable" 18 | }, 19 | "port": { 20 | "type": "string", 21 | "metadata": { 22 | "description": "Port to open on the container and the public IP address." 23 | }, 24 | "defaultValue": "8000" 25 | }, 26 | "cpuCores": { 27 | "type": "string", 28 | "metadata": { 29 | "description": "The number of CPU cores to allocate to the container. Must be an integer." 30 | }, 31 | "defaultValue": "1.0" 32 | }, 33 | "memoryInGb": { 34 | "type": "string", 35 | "metadata": { 36 | "description": "The amount of memory to allocate to the container in gigabytes." 37 | }, 38 | "defaultValue": "1.0" 39 | } 40 | }, 41 | "variables": {}, 42 | "resources": [ 43 | { 44 | "name": "[parameters('name')]", 45 | "type": "Microsoft.ContainerInstance/containerGroups", 46 | "apiVersion": "2018-10-01", 47 | "location": "[resourceGroup().location]", 48 | "properties": { 49 | "containers": [ 50 | { 51 | "name": "[parameters('name')]", 52 | "properties": { 53 | "image": "[parameters('image')]", 54 | "ports": [ 55 | { 56 | "port": "[parameters('port')]" 57 | } 58 | ], 59 | "resources": { 60 | "requests": { 61 | "cpu": "[parameters('cpuCores')]", 62 | "memoryInGb": "[parameters('memoryInGb')]" 63 | } 64 | } 65 | } 66 | } 67 | ], 68 | "osType": "Linux", 69 | "ipAddress": { 70 | "type": "Public", 71 | "ports": [ 72 | { 73 | "protocol": "tcp", 74 | "port": "[parameters('port')]" 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | ], 81 | "outputs": { 82 | "containerIPv4Address": { 83 | "type": "string", 84 | "value": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups/', parameters('name'))).ipAddress.ip]" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /helm/scoold/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for Scoold. 2 | # Declare variables to be passed into your templates. 3 | 4 | replicaCount: 1 5 | 6 | nameOverride: "" 7 | fullnameOverride: "" 8 | 9 | ######################################### 10 | # Docker image - use "latest" for current 11 | # snapshot of the master branch 12 | ######################################### 13 | image: 14 | repository: erudikaltd/scoold 15 | tag: "1.65.0" 16 | pullPolicy: IfNotPresent 17 | pullSecrets: [] 18 | 19 | service: 20 | name: http 21 | type: ClusterIP 22 | port: 8000 23 | 24 | ###################### 25 | # Scoold configuration 26 | ###################### 27 | applicationConf: | 28 | scoold.env = production 29 | scoold.para_endpoint = "https://paraio.com" 30 | scoold.para_access_key = "app:scoold" 31 | scoold.para_secret_key = "secret" 32 | #scoold.password_auth_enabled = true 33 | #scoold.is_default_space_public = true 34 | #################################### 35 | # Add more config properties here... 36 | #################################### 37 | 38 | ################################################### 39 | # JVM command line arguments, exported as JAVA_OPTS 40 | ################################################### 41 | javaOpts: "-Xmx512m -Xms512m -Dconfig.file=/scoold/config/application.conf" 42 | 43 | podAnnotations: {} 44 | 45 | ##################### 46 | # Extra ENV variables 47 | ##################### 48 | extraEnvs: [] 49 | 50 | updateStrategy: RollingUpdate 51 | 52 | ingress: 53 | enabled: false 54 | className: "" 55 | annotations: 56 | kubernetes.io/ingress.class: nginx 57 | # kubernetes.io/tls-acme: "true" 58 | hosts: 59 | - host: scoold.local 60 | paths: 61 | - path: / 62 | pathType: Prefix 63 | tls: [] 64 | # - secretName: scoold-tls-secret 65 | # hosts: 66 | # - scoold.local 67 | 68 | resources: {} 69 | # We usually recommend not to specify default resources and to leave this as a conscious 70 | # choice for the user. This also increases chances charts run on environments with little 71 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 72 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 73 | # limits: 74 | # cpu: 100m 75 | # memory: 128Mi 76 | # requests: 77 | # cpu: 100m 78 | # memory: 128Mi 79 | 80 | nodeSelector: {} 81 | 82 | tolerations: [] 83 | 84 | affinity: {} 85 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/avatars/CloudinaryAvatarRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.scoold.core.Profile; 21 | import org.apache.commons.lang3.Strings; 22 | 23 | public class CloudinaryAvatarRepository implements AvatarRepository { 24 | private static final String BASE_URL = "https://res.cloudinary.com/"; 25 | 26 | private final AvatarRepository nextRepository; 27 | 28 | public CloudinaryAvatarRepository(AvatarRepository nextRepository) { 29 | this.nextRepository = nextRepository; 30 | } 31 | 32 | @Override 33 | public String getLink(Profile profile, AvatarFormat format) { 34 | if (profile == null) { 35 | return nextRepository.getLink(profile, format); 36 | } 37 | 38 | String picture = profile.getPicture(); 39 | if (!isCloudinaryLink(picture)) { 40 | return nextRepository.getLink(profile, format); 41 | } 42 | 43 | return configureLink(picture, format); 44 | } 45 | 46 | @Override 47 | public String getAnonymizedLink(String data) { 48 | return nextRepository.getAnonymizedLink(data); 49 | } 50 | 51 | @Override 52 | public boolean store(Profile profile, String url) { 53 | if (!isCloudinaryLink(url)) { 54 | return nextRepository.store(profile, url); 55 | } 56 | 57 | return applyChange(profile, url); 58 | } 59 | 60 | private boolean applyChange(Profile profile, String url) { 61 | profile.setPicture(url); 62 | 63 | return true; 64 | } 65 | 66 | private boolean isCloudinaryLink(String url) { 67 | return Strings.CS.startsWith(url, BASE_URL); 68 | } 69 | 70 | private String configureLink(String url, AvatarFormat format) { 71 | return url.replace("/upload/", "/upload/t_" + getTransformName(format) + "/"); 72 | } 73 | 74 | private String getTransformName(AvatarFormat format) { 75 | return format.name().toLowerCase(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/avatars/GravatarAvatarRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.scoold.core.Profile; 21 | import org.apache.commons.lang3.StringUtils; 22 | 23 | public class GravatarAvatarRepository implements AvatarRepository { 24 | private final GravatarAvatarGenerator gravatarAvatarGenerator; 25 | private final AvatarRepository nextRepository; 26 | 27 | public GravatarAvatarRepository(GravatarAvatarGenerator gravatarAvatarGenerator, AvatarRepository nextRepository) { 28 | this.gravatarAvatarGenerator = gravatarAvatarGenerator; 29 | this.nextRepository = nextRepository; 30 | } 31 | 32 | @Override 33 | public String getLink(Profile profile, AvatarFormat format) { 34 | if (profile == null) { 35 | return nextRepository.getLink(profile, format); 36 | } 37 | 38 | String picture = profile.getPicture(); 39 | if (StringUtils.isBlank(picture)) { 40 | return gravatarAvatarGenerator.getLink(profile, format); 41 | } 42 | 43 | if (!gravatarAvatarGenerator.isLink(picture)) { 44 | return nextRepository.getLink(profile, format); 45 | } 46 | 47 | return gravatarAvatarGenerator.configureLink(picture, format); 48 | } 49 | 50 | @Override 51 | public String getAnonymizedLink(String data) { 52 | return gravatarAvatarGenerator.getRawLink(data); 53 | } 54 | 55 | @Override 56 | public boolean store(Profile profile, String url) { 57 | if (StringUtils.isBlank(url)) { 58 | String gravatarUrl = gravatarAvatarGenerator.getRawLink(profile); 59 | return applyChange(profile, gravatarUrl); 60 | } 61 | 62 | if (!gravatarAvatarGenerator.isLink(url)) { 63 | return nextRepository.store(profile, url); 64 | } 65 | 66 | return applyChange(profile, url); 67 | } 68 | 69 | private boolean applyChange(Profile profile, String url) { 70 | profile.setPicture(url); 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/controllers/ErrorController.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright 2013-2022 Erudika. https://erudika.com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | * For issues and patches go to: https://github.com/erudika 18 | */ 19 | package com.erudika.scoold.controllers; 20 | 21 | import com.erudika.para.core.utils.ParaObjectUtils; 22 | import com.erudika.scoold.utils.ScooldUtils; 23 | import jakarta.inject.Inject; 24 | import jakarta.servlet.http.HttpServletRequest; 25 | import jakarta.servlet.http.HttpServletResponse; 26 | import java.io.IOException; 27 | import java.util.Collections; 28 | import org.apache.commons.lang3.Strings; 29 | import org.springframework.http.MediaType; 30 | import org.springframework.stereotype.Controller; 31 | import org.springframework.ui.Model; 32 | import org.springframework.web.bind.annotation.GetMapping; 33 | import org.springframework.web.bind.annotation.PathVariable; 34 | 35 | /** 36 | * 37 | * @author Alex Bogdanovski [alex@erudika.com] 38 | */ 39 | @Controller 40 | public class ErrorController { 41 | 42 | private final ScooldUtils utils; 43 | 44 | @Inject 45 | public ErrorController(ScooldUtils utils) { 46 | this.utils = utils; 47 | } 48 | 49 | @GetMapping("/error/{code}") 50 | public String get(@PathVariable String code, HttpServletRequest req, HttpServletResponse res, Model model) throws IOException { 51 | model.addAttribute("path", "error.vm"); 52 | model.addAttribute("title", utils.getLang(req).get("error.title")); 53 | model.addAttribute("status", req.getAttribute("jakarta.servlet.error.status_code")); 54 | model.addAttribute("reason", req.getAttribute("jakarta.servlet.error.message")); 55 | model.addAttribute("code", code); 56 | 57 | if (Strings.CS.startsWith((CharSequence) req.getAttribute("jakarta.servlet.forward.request_uri"), "/api/")) { 58 | res.setContentType(MediaType.APPLICATION_JSON_VALUE); 59 | ParaObjectUtils.getJsonWriterNoIdent().writeValue(res.getOutputStream(), 60 | Collections.singletonMap("error", code + " - " + req.getAttribute("jakarta.servlet.error.message"))); 61 | } 62 | return "base"; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/avatars/AvatarRepositoryProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.scoold.core.Profile; 21 | import com.erudika.scoold.utils.ScooldUtils; 22 | import jakarta.inject.Singleton; 23 | import org.apache.commons.lang3.Strings; 24 | import org.springframework.stereotype.Component; 25 | 26 | @Component 27 | @Singleton 28 | public class AvatarRepositoryProxy implements AvatarRepository { 29 | private final AvatarRepository repository; 30 | 31 | public AvatarRepositoryProxy(GravatarAvatarGenerator gravatarAvatarGenerator) { 32 | this.repository = addGravatarIfEnabled(addCloudinaryIfEnabled(addImgurIfEnabled(getDefault())), gravatarAvatarGenerator); 33 | } 34 | 35 | private AvatarRepository addGravatarIfEnabled(AvatarRepository repo, GravatarAvatarGenerator gravatarAvatarGenerator) { 36 | return ScooldUtils.isGravatarEnabled() ? new GravatarAvatarRepository(gravatarAvatarGenerator, repo) : repo; 37 | } 38 | 39 | private AvatarRepository addImgurIfEnabled(AvatarRepository repo) { 40 | return ScooldUtils.isImgurAvatarRepositoryEnabled() ? new ImgurAvatarRepository(repo) : repo; 41 | } 42 | 43 | private AvatarRepository addCloudinaryIfEnabled(AvatarRepository repo) { 44 | return ScooldUtils.isCloudinaryAvatarRepositoryEnabled() ? new CloudinaryAvatarRepository(repo) : repo; 45 | } 46 | 47 | private AvatarRepository getDefault() { 48 | return new DefaultAvatarRepository(); 49 | } 50 | 51 | @Override 52 | public String getLink(Profile profile, AvatarFormat format) { 53 | return repository.getLink(profile, format); 54 | } 55 | 56 | @Override 57 | public String getAnonymizedLink(String data) { 58 | return repository.getAnonymizedLink(data); 59 | } 60 | 61 | @Override 62 | public boolean store(Profile profile, String url) { 63 | if (profile != null && Strings.CS.equals(profile.getPicture(), url)) { 64 | return false; 65 | } 66 | return repository.store(profile, url); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/RequestNotSupportedExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils; 19 | 20 | import java.net.URI; 21 | import java.util.Set; 22 | import org.springframework.core.Ordered; 23 | import org.springframework.core.annotation.Order; 24 | import org.springframework.http.HttpHeaders; 25 | import org.springframework.http.HttpMethod; 26 | import org.springframework.http.HttpStatus; 27 | import org.springframework.http.HttpStatusCode; 28 | import org.springframework.http.ResponseEntity; 29 | import org.springframework.util.CollectionUtils; 30 | import org.springframework.web.HttpRequestMethodNotSupportedException; 31 | import org.springframework.web.bind.annotation.ControllerAdvice; 32 | import org.springframework.web.context.request.WebRequest; 33 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 34 | import org.springframework.web.servlet.resource.NoResourceFoundException; 35 | 36 | /** 37 | * HttpRequestMethodNotSupportedException handler - suppress spammy log messages. 38 | * @author Alex Bogdanovski [alex@erudika.com] 39 | */ 40 | @Order(Ordered.HIGHEST_PRECEDENCE) 41 | @ControllerAdvice 42 | public class RequestNotSupportedExceptionHandler extends ResponseEntityExceptionHandler { 43 | 44 | @Override 45 | protected ResponseEntity handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, 46 | HttpHeaders headers, HttpStatusCode status, WebRequest request) { 47 | Set supportedMethods = ex.getSupportedHttpMethods(); 48 | if (!CollectionUtils.isEmpty(supportedMethods)) { 49 | headers.setAllow(supportedMethods); 50 | } 51 | return new ResponseEntity<>(null, headers, status); 52 | } 53 | 54 | @Override 55 | protected ResponseEntity handleNoResourceFoundException( 56 | NoResourceFoundException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { 57 | return ResponseEntity.status(HttpStatus.PERMANENT_REDIRECT).location(URI.create("/not-found")).build(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/controllers/TermsController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.controllers; 19 | 20 | import com.erudika.para.core.Sysprop; 21 | import com.erudika.para.core.utils.Para; 22 | import static com.erudika.scoold.ScooldServer.TERMSLINK; 23 | import com.erudika.scoold.utils.ScooldUtils; 24 | import jakarta.inject.Inject; 25 | import jakarta.servlet.http.HttpServletRequest; 26 | import org.apache.commons.lang3.StringUtils; 27 | import org.springframework.stereotype.Controller; 28 | import org.springframework.ui.Model; 29 | import org.springframework.web.bind.annotation.GetMapping; 30 | import org.springframework.web.bind.annotation.PostMapping; 31 | import org.springframework.web.bind.annotation.RequestMapping; 32 | import org.springframework.web.bind.annotation.RequestParam; 33 | 34 | /** 35 | * 36 | * @author Alex Bogdanovski [alex@erudika.com] 37 | */ 38 | @Controller 39 | @RequestMapping("/terms") 40 | public class TermsController { 41 | 42 | private final ScooldUtils utils; 43 | 44 | @Inject 45 | public TermsController(ScooldUtils utils) { 46 | this.utils = utils; 47 | } 48 | 49 | @GetMapping 50 | public String get(HttpServletRequest req, Model model) { 51 | model.addAttribute("path", "terms.vm"); 52 | model.addAttribute("title", utils.getLang(req).get("terms.title")); 53 | model.addAttribute("termshtml", utils.getParaClient().read("template" + Para.getConfig().separator() + "terms")); 54 | return "base"; 55 | } 56 | 57 | @PostMapping 58 | public String edit(@RequestParam String termshtml, HttpServletRequest req, Model model) { 59 | if (!utils.isAuthenticated(req) || !utils.isAdmin(utils.getAuthUser(req))) { 60 | return "redirect:" + TERMSLINK; 61 | } 62 | Sysprop terms = new Sysprop("template" + Para.getConfig().separator() + "terms"); 63 | if (StringUtils.isBlank(termshtml)) { 64 | utils.getParaClient().delete(terms); 65 | } else { 66 | terms.addProperty("html", termshtml); 67 | utils.getParaClient().create(terms); 68 | } 69 | return "redirect:" + TERMSLINK; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/controllers/PrivacyController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.controllers; 19 | 20 | import com.erudika.para.core.Sysprop; 21 | import com.erudika.para.core.utils.Para; 22 | import static com.erudika.scoold.ScooldServer.PRIVACYLINK; 23 | import com.erudika.scoold.utils.ScooldUtils; 24 | import jakarta.inject.Inject; 25 | import jakarta.servlet.http.HttpServletRequest; 26 | import org.apache.commons.lang3.StringUtils; 27 | import org.springframework.stereotype.Controller; 28 | import org.springframework.ui.Model; 29 | import org.springframework.web.bind.annotation.GetMapping; 30 | import org.springframework.web.bind.annotation.PostMapping; 31 | import org.springframework.web.bind.annotation.RequestMapping; 32 | import org.springframework.web.bind.annotation.RequestParam; 33 | 34 | /** 35 | * 36 | * @author Alex Bogdanovski [alex@erudika.com] 37 | */ 38 | @Controller 39 | @RequestMapping("/privacy") 40 | public class PrivacyController { 41 | 42 | private final ScooldUtils utils; 43 | 44 | @Inject 45 | public PrivacyController(ScooldUtils utils) { 46 | this.utils = utils; 47 | } 48 | 49 | @GetMapping 50 | public String get(HttpServletRequest req, Model model) { 51 | model.addAttribute("path", "privacy.vm"); 52 | model.addAttribute("title", utils.getLang(req).get("privacy.title")); 53 | model.addAttribute("privacyhtml", utils.getParaClient().read("template" + Para.getConfig().separator() + "privacy")); 54 | return "base"; 55 | } 56 | 57 | @PostMapping 58 | public String edit(@RequestParam String privacyhtml, HttpServletRequest req, Model model) { 59 | if (!utils.isAuthenticated(req) || !utils.isAdmin(utils.getAuthUser(req))) { 60 | return "redirect:" + PRIVACYLINK; 61 | } 62 | Sysprop privacy = new Sysprop("template" + Para.getConfig().separator() + "privacy"); 63 | if (StringUtils.isBlank(privacyhtml)) { 64 | utils.getParaClient().delete(privacy); 65 | } else { 66 | privacy.addProperty("html", privacyhtml); 67 | utils.getParaClient().create(privacy); 68 | } 69 | return "redirect:" + PRIVACYLINK; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/slack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 16 | 19 | 20 | 22 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/Badge.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.core; 19 | 20 | import com.erudika.para.core.Sysprop; 21 | import com.erudika.para.core.annotations.Stored; 22 | import com.erudika.para.core.utils.Para; 23 | import com.erudika.para.core.utils.Utils; 24 | import org.apache.commons.lang3.StringUtils; 25 | import org.apache.commons.lang3.Strings; 26 | import org.apache.commons.text.StringEscapeUtils; 27 | 28 | /** 29 | * 30 | * @author Alex Bogdanovski [alex@erudika.com] 31 | */ 32 | public class Badge extends Sysprop { 33 | 34 | private static final long serialVersionUID = 1L; 35 | private static final String PREFIX = Utils.type(Badge.class).concat(Para.getConfig().separator()); 36 | 37 | @Stored private String style; 38 | @Stored private String icon; 39 | @Stored private String description; 40 | @Stored private String tag; 41 | 42 | public Badge() { 43 | this(null); 44 | } 45 | 46 | public Badge(String id) { 47 | if (Strings.CS.startsWith(id, PREFIX)) { 48 | setName(id); 49 | setTag(id.replaceAll(PREFIX, "")); 50 | setId(PREFIX.concat(getTag())); 51 | } else if (id != null) { 52 | setName(id); 53 | setTag(id); 54 | setId(PREFIX.concat(getTag())); 55 | } 56 | } 57 | 58 | public String getStyle() { 59 | return StringEscapeUtils.escapeHtml4(StringUtils.trimToEmpty(style)); 60 | } 61 | 62 | public void setStyle(String style) { 63 | this.style = style; 64 | } 65 | 66 | public String getIcon() { 67 | if (Utils.isValidURL(icon)) { 68 | return icon; 69 | } 70 | if (StringUtils.trimToEmpty(icon).startsWith(":")) { 71 | return ":" + StringUtils.substringBetween(icon, ":") + ":"; 72 | } 73 | return icon; 74 | } 75 | 76 | public void setIcon(String icon) { 77 | this.icon = icon; 78 | } 79 | 80 | public String getDescription() { 81 | return description; 82 | } 83 | 84 | public void setDescription(String description) { 85 | this.description = description; 86 | } 87 | 88 | public String getTag() { 89 | return tag; 90 | } 91 | 92 | public void setTag(String tag) { 93 | this.tag = Utils.noSpaces(Utils.stripAndTrim(tag, " "), "-"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/resources/static/images/amazon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %-20(%d{yyyy-MM-dd HH:mm:ss} [%-5level]) %msg%n 7 | 8 | 9 | 10 | 11 | LOCAL0 12 | [%-5level] %msg%n 13 | 14 | 15 | 16 | ${para.logs_dir:-.}/${para.logs_name:-scoold}.log 17 | 18 | 19 | ${para.logs_name:-scoold}-%d{yyyy-MM}.log.zip 20 | 12 21 | 3GB 22 | 23 | 24 | %-26(%d [%-5level]) %logger{35} - %msg%n 25 | 26 | 27 | ${scoold.file_logger_level:-INFO} 28 | 29 | 30 | 31 | 32 | ${para.logs_dir:-.}/${para.logs_name:-scoold}-metrics.log 33 | 34 | ${para.logs_name:-scoold}-metrics-%d{yyyy-MM-dd}.log.zip 35 | 7 36 | 1GB 37 | 38 | 39 | %-26(%d [%-5level]) %logger{35} - %msg%n 40 | 41 | 42 | ${scoold.file_logger_level:-INFO} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /helm/scoold/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "scoold.fullname" . }} 5 | labels: 6 | {{- include "scoold.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | strategy: 10 | type: {{ .Values.updateStrategy | default "RollingUpdate" }} 11 | selector: 12 | matchLabels: 13 | {{- include "scoold.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | labels: 17 | {{- include "scoold.selectorLabels" . | nindent 8 }} 18 | {{- $podAnnotations := .Values.podAnnotations }} 19 | {{- if or $podAnnotations .Values.applicationConf }} 20 | annotations: 21 | {{- with $podAnnotations }} 22 | {{- toYaml . | nindent 8 }} 23 | {{- end }} 24 | {{- if .Values.applicationConf }} 25 | configchecksum: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum | trunc 63 }} 26 | {{- end }} 27 | {{- end }} 28 | spec: 29 | {{- with .Values.image.pullSecrets }} 30 | imagePullSecrets: 31 | {{- range . }} 32 | - name: {{ . }} 33 | {{- end }} 34 | {{- end }} 35 | {{- if .Values.applicationConf }} 36 | volumes: 37 | - name: appconfig 38 | configMap: 39 | name: {{ include "scoold.fullname" . }}-config 40 | items: 41 | - key: "application.conf" 42 | path: "application.conf" 43 | {{- end }} 44 | containers: 45 | - name: {{ .Chart.Name }} 46 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 47 | imagePullPolicy: {{ .Values.image.pullPolicy }} 48 | ports: 49 | - name: http 50 | containerPort: {{ .Values.service.port }} 51 | protocol: TCP 52 | # livenessProbe: 53 | # httpGet: 54 | # path: / 55 | # port: {{ .Values.service.port }} 56 | # readinessProbe: 57 | # httpGet: 58 | # path: / 59 | # port: {{ .Values.service.port }} 60 | {{- with .Values.resources }} 61 | resources: 62 | {{- toYaml . | nindent 12 }} 63 | {{- end }} 64 | env: 65 | - name: JAVA_OPTS 66 | value: "{{ .Values.javaOpts }}" 67 | {{- with .Values.extraEnvs }} 68 | {{- toYaml . | nindent 12 }} 69 | {{- end }} 70 | {{- if .Values.applicationConf }} 71 | volumeMounts: 72 | - name: appconfig 73 | mountPath: /scoold/config 74 | {{- end }} 75 | {{- with .Values.nodeSelector }} 76 | nodeSelector: 77 | {{- toYaml . | nindent 8 }} 78 | {{- end }} 79 | {{- with .Values.affinity }} 80 | affinity: 81 | {{- toYaml . | nindent 8 }} 82 | {{- end }} 83 | {{- with .Values.tolerations }} 84 | tolerations: 85 | {{- toYaml . | nindent 8 }} 86 | {{- end }} 87 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/controllers/LanguagesController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.controllers; 19 | 20 | import static com.erudika.scoold.ScooldServer.LANGUAGESLINK; 21 | import com.erudika.scoold.utils.HttpUtils; 22 | import com.erudika.scoold.utils.ScooldUtils; 23 | import jakarta.inject.Inject; 24 | import jakarta.servlet.http.HttpServletRequest; 25 | import jakarta.servlet.http.HttpServletResponse; 26 | import java.util.Locale; 27 | import java.util.Map; 28 | import java.util.TreeMap; 29 | import java.util.stream.Collectors; 30 | import org.springframework.stereotype.Controller; 31 | import org.springframework.ui.Model; 32 | import org.springframework.web.bind.annotation.GetMapping; 33 | import org.springframework.web.bind.annotation.PathVariable; 34 | import org.springframework.web.bind.annotation.PostMapping; 35 | import org.springframework.web.bind.annotation.RequestMapping; 36 | 37 | /** 38 | * 39 | * @author Alex Bogdanovski [alex@erudika.com] 40 | */ 41 | @Controller 42 | @RequestMapping("/languages") 43 | public class LanguagesController { 44 | 45 | private final ScooldUtils utils; 46 | 47 | @Inject 48 | public LanguagesController(ScooldUtils utils) { 49 | this.utils = utils; 50 | } 51 | 52 | @GetMapping 53 | public String get(HttpServletRequest req, Model model) { 54 | model.addAttribute("path", "languages.vm"); 55 | model.addAttribute("title", utils.getLang(req).get("translate.select")); 56 | Map langProgressMap = utils.getLangutils().getTranslationProgressMap(); 57 | model.addAttribute("langProgressMap", langProgressMap); 58 | model.addAttribute("allLocales", new TreeMap<>(utils.getLangutils().getAllLocales().entrySet().stream(). 59 | filter(e -> langProgressMap.getOrDefault(e.getKey(), 0) > 70). 60 | collect(Collectors.toMap(k -> k.getKey(), v -> v.getValue())))); 61 | return "base"; 62 | } 63 | 64 | @PostMapping("/{langkey}") 65 | public String post(@PathVariable String langkey, HttpServletRequest req, HttpServletResponse res) { 66 | Locale locale = utils.getCurrentLocale(langkey); 67 | if (locale != null) { 68 | int maxAge = 60 * 60 * 24 * 365; //1 year 69 | HttpUtils.setRawCookie(ScooldUtils.getConfig().localeCookie(), locale.toString(), req, res, "Strict", maxAge); 70 | } 71 | return "redirect:" + LANGUAGESLINK; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/resources/templates/revisions.vm: -------------------------------------------------------------------------------- 1 | #macro(tagbox $tags) 2 | #foreach($showtag in $tags) 3 | 4 | #$!showtag.trim() 5 | 6 | #end 7 | #end 8 | 9 | #macro(revisionbox $showrev $showorigrev $showpost) 10 | #set($actionlink = "#getpostlink($showpost false true)") 11 | #set($actionlink2 = "#getpostlink($showpost true true)") 12 | #set($canrestore = $scooldUtils.canEdit($showpost, $authUser)) 13 |
14 |
15 |
16 |
17 | #if ($showpost.revisionid == $showrev.id) 18 | 19 | #elseif ($canrestore) 20 |
21 | #sectoken(false) 22 | 25 |
26 | #end 27 |
28 |
29 |
30 | #formatdate($showrev.timestamp "") 31 |
#if ($showrev.original) $!lang.get("original") #end
32 |
33 |
34 | #smallpersonbox($showrev.author) 35 |
36 |
37 |
38 | #if ($showrev.title) 39 |
$showrev.title
40 | #if ($showorigrev.title) 41 | $!showorigrev.title 42 | #end 43 |
44 | #end 45 | #if ($showrev.body) 46 |
$!showrev.body
47 | #if ($showorigrev.body) 48 | $!showorigrev.body 49 | #end 50 | #end 51 | #if ($showrev.tags) 52 |
53 | #tagbox($showrev.tags) 54 |
55 | #if ($showorigrev.tags) 56 | #tagbox($!showorigrev.tags) 57 | #end 58 | #end 59 |
60 |
61 | #end 62 | 63 | #macro(revisionspage $revisions $showpost) 64 | #if ($revisions && !$revisions.isEmpty()) 65 | #set($lastIndex = $revisions.size() - 1) 66 | #set($lastrev = $revisions.get($lastIndex)) 67 | 68 | #if ($revisions.size() == 1) 69 | #revisionbox($lastrev $lastrev $showpost) 70 | #else 71 | #foreach($revision in $revisions) 72 | #if ($foreach.count < $revisions.size()) 73 | #set($prevrev = $revisions.get($foreach.count)) 74 | #else 75 | #set($prevrev = $lastOnPage) 76 | #end 77 | #revisionbox($revision $prevrev $showpost) 78 | #end 79 | #end 80 | #end 81 | #end 82 | 83 | 84 | $!lang.get("backtopost") 85 |
86 |

$!lang.get("revisions.title") #showcount($itemcount.count)

87 | 88 | #paginate("revisions" $itemcount "" "page") 89 |
-------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/CoreUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2024 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils; 19 | 20 | import com.erudika.para.core.Address; 21 | import com.erudika.para.core.Sysprop; 22 | import com.erudika.para.core.Tag; 23 | import com.erudika.para.core.User; 24 | import com.erudika.para.core.Vote; 25 | import com.erudika.para.core.utils.Para; 26 | import com.erudika.para.core.utils.Utils; 27 | import com.erudika.scoold.core.Badge; 28 | import com.erudika.scoold.core.Comment; 29 | import com.erudika.scoold.core.Feedback; 30 | import com.erudika.scoold.core.Profile; 31 | import com.erudika.scoold.core.Question; 32 | import com.erudika.scoold.core.Reply; 33 | import com.erudika.scoold.core.Report; 34 | import com.erudika.scoold.core.Revision; 35 | import com.erudika.scoold.core.Sticky; 36 | import com.erudika.scoold.core.UnapprovedQuestion; 37 | import com.erudika.scoold.core.UnapprovedReply; 38 | import java.util.Arrays; 39 | import java.util.HashSet; 40 | import java.util.Set; 41 | 42 | /** 43 | * Core utils. 44 | * @author Alex Bogdanovski [alex@erudika.com] 45 | */ 46 | @SuppressWarnings("unchecked") 47 | public final class CoreUtils { 48 | 49 | private static final Set CORE_TYPES; 50 | 51 | private CoreUtils() { } 52 | 53 | static { 54 | CORE_TYPES = new HashSet<>(Arrays.asList( 55 | Utils.type(Badge.class), 56 | Utils.type(Comment.class), 57 | Utils.type(Feedback.class), 58 | Utils.type(Profile.class), 59 | Utils.type(Question.class), 60 | Utils.type(Reply.class), 61 | Utils.type(Report.class), 62 | Utils.type(Revision.class), 63 | //Utils.type(Sticky.class), 64 | Utils.type(UnapprovedQuestion.class), 65 | Utils.type(UnapprovedReply.class), 66 | // Para core types 67 | Utils.type(Address.class), 68 | Utils.type(Sysprop.class), 69 | Utils.type(Tag.class), 70 | Utils.type(User.class), 71 | Utils.type(Vote.class) 72 | )); 73 | } 74 | 75 | public static void registerCoreClasses() { 76 | Para.registerCoreClasses( 77 | Badge.class, 78 | Comment.class, 79 | Feedback.class, 80 | Profile.class, 81 | Question.class, 82 | Reply.class, 83 | Report.class, 84 | Revision.class, 85 | Sticky.class, 86 | UnapprovedQuestion.class, 87 | UnapprovedReply.class); 88 | } 89 | 90 | public static Set getCoreTypes() { 91 | return Set.copyOf(CORE_TYPES); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/controllers/RevisionsController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.controllers; 19 | 20 | import com.erudika.para.core.utils.Pager; 21 | import static com.erudika.scoold.ScooldServer.QUESTIONSLINK; 22 | import com.erudika.scoold.core.Post; 23 | import com.erudika.scoold.core.Profile; 24 | import com.erudika.scoold.core.Revision; 25 | import com.erudika.scoold.utils.ScooldUtils; 26 | import jakarta.inject.Inject; 27 | import jakarta.servlet.http.HttpServletRequest; 28 | import jakarta.servlet.http.HttpServletResponse; 29 | import java.util.List; 30 | import org.springframework.stereotype.Controller; 31 | import org.springframework.ui.Model; 32 | import org.springframework.web.bind.annotation.GetMapping; 33 | import org.springframework.web.bind.annotation.PathVariable; 34 | import org.springframework.web.bind.annotation.RequestMapping; 35 | 36 | /** 37 | * 38 | * @author Alex Bogdanovski [alex@erudika.com] 39 | */ 40 | @Controller 41 | @RequestMapping("/revisions") 42 | public class RevisionsController { 43 | 44 | private final ScooldUtils utils; 45 | 46 | @Inject 47 | public RevisionsController(ScooldUtils utils) { 48 | this.utils = utils; 49 | } 50 | 51 | @GetMapping("/{postid}") 52 | public String get(@PathVariable String postid, HttpServletRequest req, HttpServletResponse res, Model model) { 53 | Post showPost = utils.getParaClient().read(postid); 54 | if (showPost == null) { 55 | return "redirect:" + QUESTIONSLINK; 56 | } 57 | res.setHeader("X-Robots-Tag", "noindex, nofollow"); // https://github.com/Erudika/scoold/issues/254 58 | Profile authUser = utils.getAuthUser(req); 59 | if (!utils.canAccessSpace(authUser, showPost.getSpace())) { 60 | return "redirect:" + QUESTIONSLINK; 61 | } 62 | Pager itemcount = utils.getPager("page", req); 63 | List revisionslist = showPost.getRevisions(itemcount); 64 | // we need the first revision on the next page for diffing 65 | List nextPage = showPost.getRevisions(new Pager(itemcount.getPage() + 1, itemcount.getLimit())); 66 | utils.getProfiles(revisionslist); 67 | model.addAttribute("path", "revisions.vm"); 68 | model.addAttribute("title", utils.getLang(req).get("revisions.title")); 69 | model.addAttribute("showPost", showPost); 70 | model.addAttribute("itemcount", itemcount); 71 | model.addAttribute("revisionslist", revisionslist); 72 | model.addAttribute("lastOnPage", nextPage.isEmpty() ? null : nextPage.get(0)); 73 | return "base"; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/controllers/AboutController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.controllers; 19 | 20 | import com.erudika.para.core.Sysprop; 21 | import com.erudika.para.core.utils.Para; 22 | import static com.erudika.scoold.ScooldServer.ABOUTLINK; 23 | import com.erudika.scoold.core.Profile; 24 | import com.erudika.scoold.utils.ScooldUtils; 25 | import jakarta.inject.Inject; 26 | import jakarta.servlet.http.HttpServletRequest; 27 | import org.apache.commons.lang3.StringUtils; 28 | import org.springframework.stereotype.Controller; 29 | import org.springframework.ui.Model; 30 | import org.springframework.web.bind.annotation.GetMapping; 31 | import org.springframework.web.bind.annotation.PostMapping; 32 | import org.springframework.web.bind.annotation.RequestMapping; 33 | import org.springframework.web.bind.annotation.RequestParam; 34 | 35 | /** 36 | * 37 | * @author Alex Bogdanovski [alex@erudika.com] 38 | */ 39 | @Controller 40 | @RequestMapping("/about") 41 | public class AboutController { 42 | 43 | private final ScooldUtils utils; 44 | 45 | @Inject 46 | public AboutController(ScooldUtils utils) { 47 | this.utils = utils; 48 | } 49 | 50 | @GetMapping 51 | public String get(HttpServletRequest req, Model model) { 52 | model.addAttribute("path", "about.vm"); 53 | model.addAttribute("title", utils.getLang(req).get("about.title")); 54 | model.addAttribute("abouthtml", utils.getParaClient().read("template" + Para.getConfig().separator() + "about")); 55 | 56 | model.addAttribute("NICEPROFILE_BONUS", Profile.Badge.NICEPROFILE.getReward()); 57 | model.addAttribute("SUPPORTER_BONUS", Profile.Badge.SUPPORTER.getReward()); 58 | model.addAttribute("NOOB_BONUS", Profile.Badge.NOOB.getReward()); 59 | model.addAttribute("GOODQUESTION_BONUS", Profile.Badge.GOODQUESTION.getReward()); 60 | model.addAttribute("GOODANSWER_BONUS", Profile.Badge.GOODANSWER.getReward()); 61 | return "base"; 62 | } 63 | 64 | @PostMapping 65 | public String edit(@RequestParam String abouthtml, HttpServletRequest req, Model model) { 66 | if (!utils.isAuthenticated(req) || !utils.isAdmin(utils.getAuthUser(req))) { 67 | return "redirect:" + ABOUTLINK; 68 | } 69 | Sysprop about = new Sysprop("template" + Para.getConfig().separator() + "about"); 70 | if (StringUtils.isBlank(abouthtml)) { 71 | utils.getParaClient().delete(about); 72 | } else { 73 | about.addProperty("html", abouthtml); 74 | utils.getParaClient().create(about); 75 | } 76 | return "redirect:" + ABOUTLINK; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/controllers/ApiDocsController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.controllers; 19 | 20 | import com.erudika.para.core.utils.ParaObjectUtils; 21 | import com.erudika.para.core.utils.Utils; 22 | import com.erudika.scoold.ScooldServer; 23 | import com.erudika.scoold.utils.ScooldUtils; 24 | import com.fasterxml.jackson.core.JsonProcessingException; 25 | import jakarta.inject.Inject; 26 | import jakarta.servlet.http.HttpServletRequest; 27 | import jakarta.servlet.http.HttpServletResponse; 28 | import java.util.concurrent.TimeUnit; 29 | import org.apache.commons.lang3.Strings; 30 | import org.springframework.http.CacheControl; 31 | import org.springframework.http.HttpStatus; 32 | import org.springframework.http.ResponseEntity; 33 | import org.springframework.stereotype.Controller; 34 | import org.springframework.ui.Model; 35 | import org.springframework.web.bind.annotation.GetMapping; 36 | import org.springframework.web.bind.annotation.ResponseBody; 37 | import org.yaml.snakeyaml.Yaml; 38 | 39 | /** 40 | * 41 | * @author Alex Bogdanovski [alex@erudika.com] 42 | */ 43 | @Controller 44 | public class ApiDocsController { 45 | 46 | private final ScooldUtils utils; 47 | 48 | @Inject 49 | public ApiDocsController(ScooldUtils utils) { 50 | this.utils = utils; 51 | } 52 | 53 | @GetMapping({"/apidocs", "/api.html"}) 54 | public String get(HttpServletRequest req, Model model) { 55 | if (!utils.isApiEnabled()) { 56 | return "redirect:" + ScooldServer.HOMEPAGE; 57 | } 58 | if (req.getServletPath().endsWith(".html")) { 59 | return "redirect:" + ScooldServer.APIDOCSLINK; 60 | } 61 | model.addAttribute("path", "apidocs.vm"); 62 | model.addAttribute("title", "API documentation"); 63 | return "base"; 64 | } 65 | 66 | @ResponseBody 67 | @GetMapping(path = "/api.json", produces = "text/javascript") 68 | public ResponseEntity json(HttpServletRequest req, HttpServletResponse res) throws JsonProcessingException { 69 | if (!utils.isApiEnabled()) { 70 | return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); 71 | } 72 | Yaml yaml = new Yaml(); 73 | String yml = utils.loadResource("templates/api.yaml"); 74 | yml = Strings.CS.replaceOnce(yml, "{{serverUrl}}", ScooldUtils.getConfig().serverUrl()); 75 | yml = Strings.CS.replaceOnce(yml, "{{contextPath}}", ScooldUtils.getConfig().serverContextPath()); 76 | String result = ParaObjectUtils.getJsonWriter().writeValueAsString(yaml.load(yml)); 77 | return ResponseEntity.ok().cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)).eTag(Utils.md5(result)).body(result); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/resources/static/service-worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015, 2019 Google Inc. All Rights Reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | // Incrementing OFFLINE_VERSION will kick off the install event and force 15 | // previously cached resources to be updated from the network. 16 | const OFFLINE_VERSION = 1; 17 | const CACHE_NAME = 'offline'; 18 | // Customize this with a different URL if needed. 19 | const OFFLINE_URL = 'offline.html'; 20 | 21 | self.addEventListener('install', (event) => { 22 | event.waitUntil((async () => { 23 | const cache = await caches.open(CACHE_NAME); 24 | // Setting {cache: 'reload'} in the new request will ensure that the response 25 | // isn't fulfilled from the HTTP cache; i.e., it will be from the network. 26 | await cache.add(new Request(OFFLINE_URL, {cache: 'reload'})).catch(() => {}); 27 | })()); 28 | }); 29 | 30 | self.addEventListener('activate', (event) => { 31 | event.waitUntil((async () => { 32 | // Enable navigation preload if it's supported. 33 | // See https://developers.google.com/web/updates/2017/02/navigation-preload 34 | if ('navigationPreload' in self.registration) { 35 | await self.registration.navigationPreload.enable(); 36 | } 37 | })()); 38 | 39 | // Tell the active service worker to take control of the page immediately. 40 | self.clients.claim(); 41 | }); 42 | 43 | self.addEventListener('fetch', (event) => { 44 | // We only want to call event.respondWith() if this is a navigation request 45 | // for an HTML page. 46 | if (event.request.mode === 'navigate') { 47 | event.respondWith((async () => { 48 | try { 49 | // First, try to use the navigation preload response if it's supported. 50 | const preloadResponse = await event.preloadResponse; 51 | if (preloadResponse) { 52 | return preloadResponse; 53 | } 54 | 55 | const networkResponse = await fetch(event.request); 56 | return networkResponse; 57 | } catch (error) { 58 | // catch is only triggered if an exception is thrown, which is likely 59 | // due to a network error. 60 | // If fetch() returns a valid HTTP response with a response code in 61 | // the 4xx or 5xx range, the catch() will NOT be called. 62 | console.log('Fetch failed; returning offline page instead.', error); 63 | 64 | const cache = await caches.open(CACHE_NAME); 65 | const cachedResponse = await cache.match(OFFLINE_URL); 66 | return cachedResponse; 67 | } 68 | })()); 69 | } 70 | 71 | // If our if() condition is false, then this fetch handler won't intercept the 72 | // request. If there are any other fetch handlers registered, they will get a 73 | // chance to call event.respondWith(). If no fetch handlers call 74 | // event.respondWith(), the request will be handled by the browser as if there 75 | // were no service worker involvement. 76 | }); -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/api/WebhooksController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.api; 19 | 20 | import com.erudika.para.client.ParaClient; 21 | import com.erudika.para.core.utils.Config; 22 | import com.erudika.para.core.utils.ParaObjectUtils; 23 | import com.erudika.para.core.utils.Utils; 24 | import com.erudika.scoold.ScooldConfig; 25 | import static com.erudika.scoold.api.ApiController.logger; 26 | import com.erudika.scoold.utils.ScooldUtils; 27 | import com.fasterxml.jackson.core.JsonProcessingException; 28 | import jakarta.inject.Inject; 29 | import jakarta.servlet.http.HttpServletRequest; 30 | import jakarta.servlet.http.HttpServletResponse; 31 | import java.util.Collections; 32 | import java.util.Map; 33 | import org.apache.commons.lang3.Strings; 34 | import org.springframework.web.bind.annotation.PostMapping; 35 | import org.springframework.web.bind.annotation.RequestMapping; 36 | import org.springframework.web.bind.annotation.RestController; 37 | 38 | /** 39 | * Handles various webhook events. 40 | * @author Alex Bogdanovski [alex@erudika.com] 41 | */ 42 | @RestController 43 | @RequestMapping(value = "/webhooks", produces = "application/json") 44 | public class WebhooksController { 45 | 46 | private final ScooldUtils utils; 47 | private final ParaClient pc; 48 | private static final ScooldConfig CONF = ScooldUtils.getConfig(); 49 | private static String lastConfigUpdate = null; 50 | 51 | @Inject 52 | public WebhooksController(ScooldUtils utils) { 53 | this.utils = utils; 54 | this.pc = utils.getParaClient(); 55 | } 56 | 57 | @PostMapping("/config") 58 | public void updateConfig(HttpServletRequest req, HttpServletResponse res) throws JsonProcessingException { 59 | Map entity = readEntity(req); 60 | if (entity.containsKey("signature") && entity.containsKey("payload") && 61 | entity.getOrDefault("event", "").equals("config.update")) { 62 | String payload = (String) entity.get("payload"); 63 | String signature = (String) entity.get("signature"); 64 | String id = (String) entity.get(Config._ID); 65 | boolean alreadyUpdated = id.equals(lastConfigUpdate); 66 | if (Strings.CS.equals(signature, Utils.hmacSHA256(payload, CONF.paraSecretKey())) && !alreadyUpdated) { 67 | Map configMap = ParaObjectUtils.getJsonReader(Map.class).readValue(payload); 68 | configMap.entrySet().forEach((entry) -> { 69 | System.setProperty(entry.getKey(), entry.getValue().toString()); 70 | }); 71 | CONF.store(); 72 | lastConfigUpdate = id; 73 | } 74 | } 75 | } 76 | 77 | private Map readEntity(HttpServletRequest req) { 78 | try { 79 | return ParaObjectUtils.getJsonReader(Map.class).readValue(req.getInputStream()); 80 | } catch (Exception ex) { 81 | logger.error(null, ex); 82 | } 83 | return Collections.emptyMap(); 84 | } 85 | 86 | public static void setLastConfigUpdate(String id) { 87 | lastConfigUpdate = id; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/resources/templates/search.vm: -------------------------------------------------------------------------------- 1 | #if($currentSpace && !$currentSpace.isEmpty()) 2 | #spacelabel($currentSpace) 3 | #end 4 |
5 |

6 | #if ($showParam) 7 | #if ($showParam == "questions") 8 | $!lang.get("questions.title") #showcount($itemcount.count) 9 | #elseif ($showParam == "answers") 10 | $!lang.get("answers.title") #showcount($itemcount.count) 11 | #elseif ($showParam == "feedback") 12 | $!lang.get("feedback.title") #showcount($itemcount.count) 13 | #elseif ($showParam == "people") 14 | $!lang.get("people.title") #showcount($itemcount.count) 15 | #elseif ($showParam == "comments") 16 | $!lang.get("comments.title") #showcount($itemcount.count) 17 | #end 18 | #else 19 | $!lang.get("search.title") 20 | #end 21 | $!searchQuery 22 |

23 | 24 | #if ($showParam) 25 | #if ($itemcount.count == 0) 26 |
27 |
28 | $!lang.get("search.notfound") 29 |
30 |
31 | #end 32 | 33 | #if ($showParam == "questions") 34 | #paginate("questions" $itemcount $params "page") 35 | #elseif ($showParam == "answers") 36 | #paginate("answers" $itemcount $params "page") 37 | #elseif ($showParam == "feedback") 38 | #paginate("feedback" $itemcount $params "page") 39 | #elseif ($showParam == "comments") 40 | #paginate("simplecomments" $itemcount $params "page") 41 | #elseif ($showParam == "people") 42 |
43 | #paginate("people" $itemcount $params "page") 44 |
45 | #end 46 | #else 47 | 48 | #if ($questionslist.isEmpty() && $answerslist.isEmpty() && $feedbacklist.isEmpty() && $userlist.isEmpty() && $commentslist.isEmpty()) 49 |
50 |
51 | $!lang.get("search.notfound") 52 |
53 |
54 | #end 55 | 56 | #if (!$questionslist.isEmpty()) 57 |
$!lang.get("questions.title")
58 | #questionspage($questionslist) 59 |
60 | $!lang.get("more") 61 |
62 | #end 63 | 64 | #if (!$answerslist.isEmpty()) 65 |
$!lang.get("answers.title")
66 | #compactanswerspage($answerslist) 67 |
68 | $!lang.get("more") 69 |
70 | #end 71 | 72 | #if (!$userlist.isEmpty()) 73 |
$!lang.get("people.title")
74 |
75 | #peoplepage($userlist) 76 |
77 |
78 | $!lang.get("more") 79 |
80 | #end 81 | 82 | #if (!$commentslist.isEmpty()) 83 |
$!lang.get("comments.title")
84 | #simplecommentspage($commentslist) 85 |
86 | $!lang.get("more") 87 |
88 | #end 89 | 90 | #if (!$feedbacklist.isEmpty()) 91 |
$!lang.get("feedback.title")
92 | #questionspage($feedbacklist) 93 |
94 | $!lang.get("more") 95 |
96 | #end 97 | 98 | #end 99 |
100 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/Comment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.core; 19 | 20 | import com.erudika.para.client.ParaClient; 21 | import com.erudika.para.core.Sysprop; 22 | import com.erudika.para.core.annotations.Stored; 23 | import com.erudika.para.core.utils.Config; 24 | import com.erudika.para.core.utils.Utils; 25 | import com.erudika.scoold.utils.ScooldUtils; 26 | import java.util.Collections; 27 | import java.util.Objects; 28 | import org.apache.commons.lang3.StringUtils; 29 | 30 | /** 31 | * 32 | * @author Alex Bogdanovski [alex@erudika.com] 33 | */ 34 | public class Comment extends Sysprop { 35 | 36 | private static final long serialVersionUID = 1L; 37 | 38 | @Stored private String comment; 39 | @Stored private Boolean hidden; 40 | @Stored private String authorName; 41 | 42 | public Comment() { 43 | this(null, null, null); 44 | } 45 | 46 | public Comment(String creatorid, String comment, String parentid) { 47 | setCreatorid(creatorid); 48 | this.comment = comment; 49 | setParentid(parentid); 50 | setTimestamp(System.currentTimeMillis()); //now 51 | } 52 | 53 | private ParaClient client() { 54 | return ScooldUtils.getInstance().getParaClient(); 55 | } 56 | 57 | public String getAuthorName() { 58 | return authorName; 59 | } 60 | 61 | public void setAuthorName(String authorName) { 62 | this.authorName = authorName; 63 | } 64 | 65 | public Boolean getHidden() { 66 | return hidden; 67 | } 68 | 69 | public void setHidden(Boolean hidden) { 70 | this.hidden = hidden; 71 | } 72 | 73 | public String getComment() { 74 | return comment; 75 | } 76 | 77 | public void setComment(String comment) { 78 | this.comment = comment; 79 | } 80 | 81 | public String create() { 82 | if (StringUtils.isBlank(comment) || StringUtils.isBlank(getParentid())) { 83 | return null; 84 | } 85 | int count = client().getCount(getType(), Collections.singletonMap(Config._PARENTID, getParentid())).intValue(); 86 | if (count > ScooldUtils.getConfig().maxCommentsPerPost()) { 87 | return null; 88 | } 89 | this.comment = Utils.abbreviate(this.comment, ScooldUtils.getConfig().maxCommentLength()); 90 | Comment c = client().create(this); 91 | if (c != null) { 92 | setId(c.getId()); 93 | setTimestamp(c.getTimestamp()); 94 | return c.getId(); 95 | } 96 | return null; 97 | } 98 | 99 | public void update() { 100 | client().update(this); 101 | } 102 | 103 | public void delete() { 104 | client().delete(this); 105 | } 106 | 107 | public boolean equals(Object obj) { 108 | if (obj == null || getClass() != obj.getClass()) { 109 | return false; 110 | } 111 | return Objects.equals(getComment(), ((Comment) obj).getComment()) 112 | && Objects.equals(getCreatorid(), ((Comment) obj).getCreatorid()); 113 | } 114 | 115 | public int hashCode() { 116 | return Objects.hashCode(getComment()) + Objects.hashCode(getCreatorid()); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/resources/static/scripts/inline-attachment.min.js: -------------------------------------------------------------------------------- 1 | !function(l,e){"use strict";var a=function(e,t){this.settings=a.util.merge(e,a.defaults),this.editor=t,this.filenameTag="{filename}",this.lastValue=null};a.editors={},a.util={merge:function(){for(var e={},t=arguments.length-1;0<=t;t--){var a=arguments[t];for(var i in a)a.hasOwnProperty(i)&&(e[i]=a[i])}return e},appendInItsOwnLine:function(e,t){return(e+"\n\n[[D]]"+t).replace(/(\n{2,})\[\[D\]\]/,"\n\n").replace(/^(\n*)/,"")},insertTextAtCursor:function(e,t){var a,i=e.scrollTop,n=0,r=!1;e.selectionStart||"0"===e.selectionStart?r="ff":l.selection&&(r="ie"),"ie"===r?(e.focus(),(a=l.selection.createRange()).moveStart("character",-e.value.length),n=a.text.length):"ff"===r&&(n=e.selectionStart);var s=e.value.substring(0,n),o=e.value.substring(n,e.value.length);e.value=s+t+o,n+=t.length,"ie"===r?(e.focus(),(a=l.selection.createRange()).moveStart("character",-e.value.length),a.moveStart("character",n),a.moveEnd("character",0),a.select()):"ff"===r&&(e.selectionStart=n,e.selectionEnd=n,e.focus()),e.scrollTop=i}},a.defaults={uploadUrl:"upload_attachment.php",uploadMethod:"POST",uploadFieldName:"file",defaultExtension:"png",jsonFieldName:"filename",allowedTypes:["image/jpeg","image/png","image/jpg","image/gif"],progressText:"![Uploading file...]()",urlText:"![file]({filename})",errorText:"Error uploading file",extraParams:{},extraHeaders:{},beforeFileUpload:function(){return!0},onFileReceived:function(){},onFileUploadResponse:function(){return!0},onFileUploadError:function(){return!0},onFileUploaded:function(){}},a.prototype.uploadFile=function(e){var t=this,a=new FormData,i=new XMLHttpRequest,n=this.settings,r=n.defaultExtension||n.defualtExtension;if("function"==typeof n.setupFormData&&n.setupFormData(a,e),e.name){var s=e.name.match(/\.(.+)$/);s&&(r=s[1])}var o="image-"+Date.now()+"."+r;if("function"==typeof n.remoteFilename&&(o=n.remoteFilename(e)),a.append(n.uploadFieldName,e,o),"object"==typeof n.extraParams)for(var l in n.extraParams)n.extraParams.hasOwnProperty(l)&&a.append(l,n.extraParams[l]);if(i.open("POST",n.uploadUrl),"object"==typeof n.extraHeaders)for(var p in n.extraHeaders)n.extraHeaders.hasOwnProperty(p)&&i.setRequestHeader(p,n.extraHeaders[p]);return!(i.onload=function(){200===i.status||201===i.status?t.onFileUploadResponse(i):t.onFileUploadError(i)})!==n.beforeFileUpload(i)&&i.send(a),i},a.prototype.isFileAllowed=function(e){return"string"!==e.kind&&(0===this.settings.allowedTypes.indexOf("*")||0<=this.settings.allowedTypes.indexOf(e.type))},a.prototype.onFileUploadResponse=function(e){if(!1!==this.settings.onFileUploadResponse.call(this,e)){var t=JSON.parse(e.responseText),a=t[this.settings.jsonFieldName];if(t&&a){var i;i="function"==typeof this.settings.urlText?this.settings.urlText.call(this,a,t):this.settings.urlText.replace(this.filenameTag,a);var n=this.editor.getValue().replace(this.lastValue,i);this.editor.setValue(n),this.settings.onFileUploaded.call(this,a)}}},a.prototype.onFileUploadError=function(e){if(!1!==this.settings.onFileUploadError.call(this,e)){var t=this.editor.getValue().replace(this.lastValue,this.settings.errorText);this.editor.setValue(t)}},a.prototype.onFileInserted=function(e){!1!==this.settings.onFileReceived.call(this,e)&&(this.lastValue=this.settings.progressText,this.editor.insertValue(this.lastValue))},a.prototype.onPaste=function(e){var t,a=!1,i=e.clipboardData;if("object"==typeof i){t=i.items||i.files||[];for(var n=0;n 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /src/main/resources/static/styles/rtl.css: -------------------------------------------------------------------------------- 1 | 2 | .input-field input[type=search]+label { 3 | right: 1rem; 4 | } 5 | 6 | nav .input-field label { 7 | right: 0; 8 | left: auto; 9 | } 10 | .input-field input[type=search]+label { 11 | left: auto; 12 | } 13 | 14 | .input-field input[type=search], .nav-wrapper .input-field input[type=search] { 15 | padding-left: 0rem; 16 | padding-right: 4rem; 17 | color: #444; 18 | } 19 | 20 | i.toggle-drawer { 21 | cursor: pointer; 22 | position: absolute; 23 | left: 0; 24 | } 25 | 26 | ul:not(.browser-default) { 27 | padding-right: 0; 28 | list-style-type: none; 29 | } 30 | i.left { 31 | float: right; 32 | margin-left: 15px; 33 | margin-right: 0; 34 | } 35 | i.right { 36 | float: left; 37 | margin-right: 15px; 38 | margin-left: 0; 39 | } 40 | td, th { 41 | text-align: right; 42 | } 43 | 44 | .userbox .col:first-child, .page-footer .col:first-child, .questionbox .col:first-child, 45 | .questionpage .col:first-child, .profilepage .col:first-child, .compactanswerbox .col:first-child { 46 | float: right; 47 | } 48 | 49 | .user-card, .user-card .lastseen { 50 | padding-right: 12px; 51 | } 52 | 53 | .aboutme { 54 | padding-right: 70px; 55 | padding-left: 0; 56 | } 57 | 58 | .CodeMirror-scroll { 59 | margin-left: -30px; 60 | margin-right: 0; 61 | } 62 | 63 | .CodeMirror { 64 | direction: rtl; 65 | } 66 | 67 | .CodeMirror-sizer { 68 | border-right: none; 69 | border-left: 30px solid transparent; 70 | } 71 | 72 | .editor-preview { 73 | left: auto; 74 | right: 0; 75 | } 76 | .editor-preview p { 77 | text-align: right; 78 | } 79 | 80 | .input-field .prefix ~ input, .input-field .prefix ~ textarea, .input-field .prefix ~ label, 81 | .input-field .prefix ~ .validate ~ label, .input-field .prefix ~ .autocomplete-content { 82 | margin-right: 3rem; 83 | margin-left: 0; 84 | } 85 | .input-field label { 86 | left: auto; 87 | right: 0; 88 | } 89 | 90 | .dropdown-content li { 91 | text-align: right; 92 | } 93 | 94 | .scoold-user-dropdown i { 95 | margin: -7px 5px 0 0; 96 | } 97 | .scoold-user-dropdown { 98 | padding: 7px 7px 7px 2px; 99 | } 100 | 101 | .row .col.offset-s6 { 102 | margin-right: 50%; 103 | margin-left: 0; 104 | } 105 | 106 | .chips-container { 107 | float: left; 108 | } 109 | 110 | .prefix ~ .chips { 111 | margin-left: 0; 112 | margin-right: 3rem; 113 | } 114 | 115 | .input-field .prefix { 116 | right: 0; 117 | } 118 | 119 | .input-field input[type=search]+.label-icon { 120 | left: auto; 121 | right: 1rem; 122 | } 123 | 124 | @media (max-width: 990px) { 125 | .chips-container { 126 | float: none; 127 | } 128 | .aboutme { 129 | padding-right: 0; 130 | } 131 | } 132 | @media (max-width: 600px) { 133 | .votecount { 134 | padding: 4px 0 0 5px; 135 | } 136 | } 137 | 138 | .r{ text-align: left;} 139 | .l{ text-align: right;} 140 | .right{ float: left !important; } 141 | .left{ float: right !important; } 142 | .left-align { text-align: right; } 143 | .right-align { text-align: left; } 144 | 145 | .prn{padding-left:0} 146 | .prs{padding-left:5px;padding-right:0} 147 | .prm{padding-left:10px;padding-right:0} 148 | .prl{padding-left:20px;padding-right:0} 149 | .pln{padding-right:0} 150 | .pls{padding-right:5px;padding-left:0} 151 | .plm{padding-right:10px;padding-left:0} 152 | .pll{padding-right:20px;padding-left:0} 153 | .mrn{margin-left:0} 154 | .mrs{margin-left:5px;margin-right:0} 155 | .mrm{margin-left:10px;margin-right:0} 156 | .mrl{margin-left:20px;margin-right:0} 157 | .mln{margin-right:0} 158 | .mls{margin-right:5px;margin-left:0} 159 | .mlm{margin-right:10px;margin-left:0} 160 | .mll{margin-right:20px;margin-left:0} 161 | -------------------------------------------------------------------------------- /src/main/resources/templates/tags.vm: -------------------------------------------------------------------------------- 1 |
2 |

$!lang.get("tags.title") #showcount($itemcount.count)

3 | 4 |
5 | #set($sortarr = {'popular': "", 'tag': ""} ) 6 | #setsortbyselection($sortarr 'popular') 7 | #sortordericon() $!lang.get("posts.mostpopular") 8 | #sortordericon("tag") $!lang.get("name") 9 | #if($isMod) 10 | $!lang.get('add') 11 | #end 12 |
13 | 14 |

$!lang.get("tags.title") #showcount($itemcount.count)

15 | 16 |
17 |
18 | ##sectoken(false "CREATE_TAGS") ## BREAKS AJAX - NOT USED 19 |
20 |
21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 | #if($tagslist.isEmpty()) 36 |
37 |
38 | $!{lang.get("search.notfound")} 39 |
40 |
41 | #else 42 | #macro(tagspage ) 43 | #foreach($showtag in $tagslist) 44 |
45 |
46 |
47 |
48 | × $!showtag.count 49 |   50 | 51 | $!showtag.tag 52 | 53 | #if($isMod) 54 | 55 | #end 56 | #if($showtag.description) 57 |
$showtag.description
58 | #end 59 |
60 |
61 | 62 | #if($isMod) 63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 | $!lang.get('delete') 76 |
77 |
78 | 79 |
80 |
81 |
82 | #end 83 | 84 |
85 |
86 | #end 87 | #end 88 |
89 | #paginate("tags" $itemcount "" "page") 90 |
91 | #end 92 |
-------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/velocity/VelocityLayoutViewResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.erudika.scoold.velocity; 18 | 19 | import org.springframework.web.servlet.view.AbstractUrlBasedView; 20 | 21 | /** 22 | * Convenience subclass of VelocityViewResolver, adding support 23 | * for VelocityLayoutView and its properties. 24 | * 25 | *

See VelocityViewResolver's javadoc for general usage info. 26 | * 27 | * @author Juergen Hoeller 28 | * @since 1.2.7 29 | * @see VelocityViewResolver 30 | * @see VelocityLayoutView 31 | * @see #setLayoutUrl 32 | * @see #setLayoutKey 33 | * @see #setScreenContentKey 34 | */ 35 | public class VelocityLayoutViewResolver extends VelocityViewResolver { 36 | 37 | private String layoutUrl; 38 | 39 | private String layoutKey; 40 | 41 | private String screenContentKey; 42 | 43 | 44 | /** 45 | * Requires VelocityLayoutView. 46 | * @see VelocityLayoutView 47 | */ 48 | @Override 49 | protected Class requiredViewClass() { 50 | return VelocityLayoutView.class; 51 | } 52 | 53 | /** 54 | * Set the layout template to use. Default is "layout.vm". 55 | * @param layoutUrl the template location (relative to the template 56 | * root directory) 57 | * @see VelocityLayoutView#setLayoutUrl 58 | */ 59 | public void setLayoutUrl(String layoutUrl) { 60 | this.layoutUrl = layoutUrl; 61 | } 62 | 63 | /** 64 | * Set the context key used to specify an alternate layout to be used instead 65 | * of the default layout. Screen content templates can override the layout 66 | * template that they wish to be wrapped with by setting this value in the 67 | * template, for example:
68 | * {@code #set($layout = "MyLayout.vm" )} 69 | *

The default key is "layout", as illustrated above. 70 | * @param layoutKey the name of the key you wish to use in your 71 | * screen content templates to override the layout template 72 | * @see VelocityLayoutView#setLayoutKey 73 | */ 74 | public void setLayoutKey(String layoutKey) { 75 | this.layoutKey = layoutKey; 76 | } 77 | 78 | /** 79 | * Set the name of the context key that will hold the content of 80 | * the screen within the layout template. This key must be present 81 | * in the layout template for the current screen to be rendered. 82 | *

Default is "screen_content": accessed in VTL as 83 | * {@code $screen_content}. 84 | * @param screenContentKey the name of the screen content key to use 85 | * @see VelocityLayoutView#setScreenContentKey 86 | */ 87 | public void setScreenContentKey(String screenContentKey) { 88 | this.screenContentKey = screenContentKey; 89 | } 90 | 91 | 92 | @Override 93 | protected AbstractUrlBasedView buildView(String viewName) throws Exception { 94 | VelocityLayoutView view = (VelocityLayoutView) super.buildView(viewName); 95 | // Use not-null checks to preserve VelocityLayoutView's defaults. 96 | if (this.layoutUrl != null) { 97 | view.setLayoutUrl(this.layoutUrl); 98 | } 99 | if (this.layoutKey != null) { 100 | view.setLayoutKey(this.layoutKey); 101 | } 102 | if (this.screenContentKey != null) { 103 | view.setScreenContentKey(this.screenContentKey); 104 | } 105 | return view; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/utils/ScooldEmailer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils; 19 | 20 | import com.erudika.para.core.email.Emailer; 21 | import com.erudika.para.core.utils.Para; 22 | import com.erudika.scoold.ScooldConfig; 23 | import jakarta.mail.internet.MimeMessage; 24 | import jakarta.mail.util.ByteArrayDataSource; 25 | import java.io.InputStream; 26 | import java.util.Iterator; 27 | import java.util.List; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | import org.springframework.mail.MailException; 31 | import org.springframework.mail.javamail.JavaMailSender; 32 | import org.springframework.mail.javamail.MimeMessageHelper; 33 | import org.springframework.mail.javamail.MimeMessagePreparator; 34 | 35 | /** 36 | * A simple JavaMail implementation of {@link Emailer}. 37 | * @author Alex Bogdanovski [alex@erudika.com] 38 | */ 39 | public class ScooldEmailer implements Emailer { 40 | 41 | private static final Logger logger = LoggerFactory.getLogger(ScooldEmailer.class); 42 | private static final ScooldConfig CONF = ScooldUtils.getConfig(); 43 | private JavaMailSender mailSender; 44 | 45 | public ScooldEmailer(JavaMailSender mailSender) { 46 | this.mailSender = mailSender; 47 | } 48 | 49 | @Override 50 | public boolean sendEmail(final List emails, final String subject, final String body) { 51 | if (emails == null || emails.isEmpty()) { 52 | return false; 53 | } 54 | Para.asyncExecute(() -> { 55 | emails.forEach(email -> { 56 | try { 57 | mailSender.send((MimeMessage mimeMessage) -> { 58 | MimeMessageHelper msg = new MimeMessageHelper(mimeMessage); 59 | msg.setTo(email); 60 | msg.setSubject(subject); 61 | msg.setFrom(CONF.supportEmail(), CONF.appName()); 62 | msg.setText(body, true); // body is assumed to be HTML 63 | }); 64 | logger.debug("Email sent to {}, {}", email, subject); 65 | } catch (MailException ex) { 66 | logger.error("Failed to send email to {} with body [{}]. {}", email, body, ex.getMessage()); 67 | } 68 | }); 69 | }); 70 | return true; 71 | } 72 | 73 | 74 | @Override 75 | public boolean sendEmail(List emails, String subject, String body, InputStream attachment, String mimeType, String fileName) { 76 | if (emails == null || emails.isEmpty()) { 77 | return false; 78 | } 79 | Para.asyncExecute(() -> { 80 | MimeMessagePreparator preparator = (MimeMessage mimeMessage) -> { 81 | MimeMessageHelper msg = new MimeMessageHelper(mimeMessage); 82 | Iterator emailz = emails.iterator(); 83 | msg.setTo(emailz.next()); 84 | while (emailz.hasNext()) { 85 | msg.addBcc(emailz.next()); 86 | } 87 | msg.setSubject(subject); 88 | msg.setFrom(Para.getConfig().supportEmail()); 89 | msg.setText(body, true); // body is assumed to be HTML 90 | if (attachment != null) { 91 | msg.addAttachment(fileName, new ByteArrayDataSource(attachment, mimeType)); 92 | } 93 | }; 94 | try { 95 | mailSender.send(preparator); 96 | logger.debug("Email sent to {}, {}", emails, subject); 97 | } catch (MailException ex) { 98 | logger.error("Failed to send email. {}", ex.getMessage()); 99 | } 100 | }); 101 | return true; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/erudika/scoold/utils/avatars/ImgurAvatarRepositoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.para.core.User; 21 | import com.erudika.scoold.core.Profile; 22 | import static org.junit.Assert.assertEquals; 23 | import static org.junit.Assert.assertNotEquals; 24 | import org.junit.Before; 25 | import org.junit.Test; 26 | import static org.mockito.Mockito.mock; 27 | import static org.mockito.Mockito.verify; 28 | import static org.mockito.Mockito.when; 29 | 30 | public class ImgurAvatarRepositoryTest { 31 | 32 | private ImgurAvatarRepository repository; 33 | private AvatarRepository defaultRepository; 34 | private Profile profile; 35 | 36 | @Before 37 | public void setUp(){ 38 | this.profile = new Profile(); 39 | this.profile.setUser(new User()); 40 | this.defaultRepository = new DefaultAvatarRepository(); 41 | this.repository = new ImgurAvatarRepository(defaultRepository); 42 | } 43 | 44 | @Test 45 | public void getLink_should_return_default_if_no_profile() { 46 | String avatar = repository.getLink(null, AvatarFormat.Profile); 47 | 48 | assertEquals(defaultRepository.getLink(null, AvatarFormat.Profile), avatar); 49 | } 50 | 51 | @Test 52 | public void getLink_should_return_default_if_no_avatar() { 53 | profile.setPicture(""); 54 | 55 | String avatar = repository.getLink(profile, AvatarFormat.Profile); 56 | 57 | assertEquals(defaultRepository.getLink(null, AvatarFormat.Profile), avatar); 58 | } 59 | 60 | @Test 61 | public void getLink_should_return_default_if_picture_has_unknown_format() { 62 | profile.setPicture("bad:AvAtar"); 63 | 64 | String avatar = repository.getLink(profile, AvatarFormat.Profile); 65 | 66 | assertEquals(defaultRepository.getLink(null, AvatarFormat.Profile), avatar); 67 | } 68 | 69 | @Test 70 | public void getAnonymizedLink_should_use_default_repository() { 71 | AvatarRepository defaultRepository = mock(AvatarRepository.class); 72 | AvatarRepository repository = new ImgurAvatarRepository(defaultRepository); 73 | when(defaultRepository.getAnonymizedLink("A")).thenReturn("https://avatar"); 74 | 75 | String avatar = repository.getAnonymizedLink("A"); 76 | 77 | assertEquals("https://avatar", avatar); 78 | } 79 | 80 | @Test 81 | public void store_should_change_profile_picture_but_not_user_is_already_this_picture() { 82 | String avatar = "https://i.imgur.com/123"; 83 | profile.setOriginalPicture(avatar); 84 | 85 | boolean result = repository.store(profile, avatar); 86 | 87 | assertEquals(true, result); 88 | assertEquals(avatar, profile.getPicture()); 89 | assertEquals(avatar, profile.getOriginalPicture()); 90 | } 91 | 92 | @Test 93 | public void store_should_call_next_repository_if_bad_url() { 94 | AvatarRepository defaultRepository = mock(AvatarRepository.class); 95 | AvatarRepository repository = new ImgurAvatarRepository(defaultRepository); 96 | String avatar = "bad:avatar"; 97 | when(defaultRepository.store(profile, avatar)).thenReturn(false); 98 | 99 | boolean result = repository.store(profile, avatar); 100 | 101 | verify(defaultRepository).store(profile, avatar); 102 | assertEquals(false, result); 103 | assertNotEquals(avatar, profile.getPicture()); 104 | assertNotEquals(avatar, profile.getOriginalPicture()); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/Revision.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | 19 | package com.erudika.scoold.core; 20 | 21 | import com.erudika.para.client.ParaClient; 22 | import com.erudika.para.core.Sysprop; 23 | import com.erudika.para.core.annotations.Stored; 24 | import com.erudika.scoold.utils.ScooldUtils; 25 | import com.fasterxml.jackson.annotation.JsonIgnore; 26 | import java.util.Objects; 27 | 28 | /** 29 | * 30 | * @author Alex Bogdanovski [alex@erudika.com] 31 | */ 32 | public class Revision extends Sysprop { 33 | private static final long serialVersionUID = 1L; 34 | 35 | @Stored private String body; 36 | @Stored private String description; 37 | @Stored private String title; 38 | @Stored private Boolean original; 39 | 40 | private transient Profile author; 41 | 42 | public Revision() { 43 | this(null); 44 | } 45 | 46 | public Revision(String id) { 47 | setId(id); 48 | } 49 | 50 | private ParaClient client() { 51 | return ScooldUtils.getInstance().getParaClient(); 52 | } 53 | 54 | public Boolean getOriginal() { 55 | return original; 56 | } 57 | 58 | public void setOriginal(Boolean original) { 59 | this.original = original; 60 | } 61 | 62 | public String getTitle() { 63 | return title; 64 | } 65 | 66 | public void setTitle(String title) { 67 | setName(title); 68 | this.title = title; 69 | } 70 | 71 | public String getDescription() { 72 | return description; 73 | } 74 | 75 | public void setDescription(String description) { 76 | this.description = description; 77 | } 78 | 79 | public String getBody() { 80 | return body; 81 | } 82 | 83 | public void setBody(String body) { 84 | this.body = body; 85 | } 86 | 87 | @JsonIgnore 88 | public Profile getAuthor() { 89 | return author; 90 | } 91 | 92 | public void setAuthor(Profile author) { 93 | this.author = author; 94 | } 95 | 96 | public static void createRevisionFromPost(Post post, boolean orig) { 97 | if (post != null && post.getId() != null) { 98 | String revUserid = post.getLasteditby(); 99 | if (revUserid == null) { 100 | revUserid = post.getCreatorid(); 101 | } 102 | Revision postrev = new Revision(); 103 | postrev.setCreatorid(revUserid); 104 | postrev.setParentid(post.getId()); 105 | postrev.setTitle(post.getTitle()); 106 | postrev.setBody(post.getBody()); 107 | postrev.setTags(post.getTags()); 108 | postrev.setOriginal(orig); 109 | String rid = postrev.create(); 110 | if (rid != null) { 111 | post.setRevisionid(rid); 112 | } 113 | } 114 | } 115 | 116 | public void delete() { 117 | client().delete(this); 118 | } 119 | 120 | public void update() { 121 | client().update(this); 122 | } 123 | 124 | public String create() { 125 | Revision r = client().create(this); 126 | if (r != null) { 127 | setId(r.getId()); 128 | setTimestamp(r.getTimestamp()); 129 | return r.getId(); 130 | } 131 | return null; 132 | } 133 | 134 | public boolean equals(Object obj) { 135 | if (obj == null || getClass() != obj.getClass()) { 136 | return false; 137 | } 138 | return Objects.equals(getBody(), ((Revision) obj).getBody()) && 139 | Objects.equals(getDescription(), ((Revision) obj).getDescription()); 140 | } 141 | 142 | public int hashCode() { 143 | return Objects.hashCode(getBody()) + Objects.hashCode(getDescription()) + Objects.hashCode(getTimestamp()); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/test/java/com/erudika/scoold/utils/avatars/CloudinaryAvatarRepositoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.para.core.User; 21 | import com.erudika.scoold.core.Profile; 22 | import static org.junit.Assert.*; 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.verify; 27 | import static org.mockito.Mockito.when; 28 | 29 | public class CloudinaryAvatarRepositoryTest { 30 | private CloudinaryAvatarRepository repository; 31 | private AvatarRepository defaultRepository; 32 | private Profile profile; 33 | 34 | @Before 35 | public void setUp(){ 36 | this.profile = new Profile(); 37 | this.profile.setUser(new User()); 38 | this.defaultRepository = new DefaultAvatarRepository(); 39 | this.repository = new CloudinaryAvatarRepository(defaultRepository); 40 | } 41 | 42 | @Test 43 | public void getLink_should_return_default_if_no_profile() { 44 | String avatar = repository.getLink(null, AvatarFormat.Profile); 45 | 46 | assertEquals(defaultRepository.getLink(null, AvatarFormat.Profile), avatar); 47 | } 48 | 49 | @Test 50 | public void getLink_should_return_default_if_no_cloudinary_link() { 51 | profile.setPicture("https://avatar"); 52 | 53 | String avatar = repository.getLink(profile, AvatarFormat.Profile); 54 | 55 | assertEquals(defaultRepository.getLink(profile, AvatarFormat.Profile), avatar); 56 | } 57 | 58 | @Test 59 | public void getLink_should_return_formatted_link_if_cloudinary_link() { 60 | profile.setPicture("https://res.cloudinary.com/test/image/upload/avatar.jpg"); 61 | 62 | String avatar = repository.getLink(profile, AvatarFormat.Profile); 63 | assertEquals("https://res.cloudinary.com/test/image/upload/t_profile/avatar.jpg", avatar); 64 | 65 | String avatar25 = repository.getLink(profile, AvatarFormat.Square25); 66 | assertEquals("https://res.cloudinary.com/test/image/upload/t_square25/avatar.jpg", avatar25); 67 | } 68 | 69 | @Test 70 | public void getAnonymizedLink_should_use_default_repository() { 71 | AvatarRepository defaultRepository = mock(AvatarRepository.class); 72 | AvatarRepository repository = new CloudinaryAvatarRepository(defaultRepository); 73 | when(defaultRepository.getAnonymizedLink("A")).thenReturn("https://avatar"); 74 | 75 | String avatar = repository.getAnonymizedLink("A"); 76 | 77 | assertEquals("https://avatar", avatar); 78 | } 79 | 80 | @Test 81 | public void store_should_accept_picture_if_cloudinary_link() { 82 | String avatar = "https://res.cloudinary.com/test/image/upload/avatar.jpg"; 83 | 84 | boolean result = repository.store(profile, avatar); 85 | 86 | assertTrue(result); 87 | assertEquals(avatar, profile.getPicture()); 88 | } 89 | 90 | @Test 91 | public void store_should_call_next_repository_if_bad_url() { 92 | AvatarRepository defaultRepository = mock(AvatarRepository.class); 93 | AvatarRepository repository = new CloudinaryAvatarRepository(defaultRepository); 94 | String avatar = "https://avatar"; 95 | when(defaultRepository.store(profile, avatar)).thenReturn(false); 96 | 97 | boolean result = repository.store(profile, avatar); 98 | 99 | verify(defaultRepository).store(profile, avatar); 100 | assertFalse(result); 101 | assertNotEquals(avatar, profile.getPicture()); 102 | assertNotEquals(avatar, profile.getUser().getPicture()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/erudika/scoold/utils/avatars/GravatarAvatarGeneratorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.utils.avatars; 19 | 20 | import com.erudika.para.core.User; 21 | import com.erudika.scoold.core.Profile; 22 | import static org.junit.Assert.*; 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | 26 | public class GravatarAvatarGeneratorTest { 27 | private GravatarAvatarGenerator generator; 28 | 29 | @Before 30 | public void setUp(){ 31 | System.setProperty("scoold.gravatars_pattern", "retro"); 32 | this.generator = new GravatarAvatarGenerator(); 33 | } 34 | 35 | @Test 36 | public void getRawLink_should_return_url_for_specific_email() { 37 | String url1 = generator.getRawLink("toto@example.com"); 38 | assertEquals("https://www.gravatar.com/avatar/75a4c35602d5866368dd4c959e249aba", url1); 39 | 40 | String url2 = generator.getRawLink("titi@example.com"); 41 | assertEquals("https://www.gravatar.com/avatar/1ac9dec7bfff4aeb9bc9e625bfb9a4ce", url2); 42 | } 43 | 44 | @Test 45 | public void getRawLink_should_return_same_url_for_same_email() { 46 | String email = "toto@example.com"; 47 | 48 | String url1 = generator.getRawLink(email); 49 | String url2 = generator.getRawLink(email); 50 | 51 | assertEquals(url1, url2); 52 | } 53 | 54 | @Test 55 | public void getRawLink_should_return_with_empty_email_if_null() { 56 | assertEquals(generator.getRawLink((String)null), generator.getRawLink("")); 57 | } 58 | 59 | @Test 60 | public void getRawLink_should_use_email_of_profile() { 61 | String email = "toto@example.com"; 62 | Profile profile = getProfileWithEmail(email); 63 | AvatarFormat format = AvatarFormat.Square32; 64 | 65 | String urlOfEmail = generator.getRawLink(email); 66 | String urlOfProfile = generator.getRawLink(profile); 67 | 68 | assertEquals(urlOfEmail, urlOfProfile); 69 | } 70 | 71 | @Test 72 | public void getRawLink_should_return_with_empty_email_if_profile_is_null() { 73 | String urlOfEmpty = generator.getRawLink(getProfileWithEmail("")); 74 | 75 | assertEquals(generator.getRawLink((Profile)null), urlOfEmpty); 76 | } 77 | 78 | @Test 79 | public void configureLink_should_return_url_for_specific_format_size_and_config_pattern_and_all_public() { 80 | String rawLink = generator.getRawLink("toto@example.com"); 81 | String link1 = generator.configureLink(rawLink, AvatarFormat.Square32); 82 | assertEquals("https://www.gravatar.com/avatar/75a4c35602d5866368dd4c959e249aba?s=32&r=g&d=retro", link1); 83 | 84 | System.setProperty("scoold.gravatars_pattern", "identicon"); 85 | String link2 = generator.configureLink(rawLink, AvatarFormat.Square50); 86 | assertEquals("https://www.gravatar.com/avatar/75a4c35602d5866368dd4c959e249aba?s=50&r=g&d=identicon", link2); 87 | } 88 | 89 | @Test 90 | public void getLink_should_compture_and_configure_link() { 91 | AvatarFormat format = AvatarFormat.Square32; 92 | Profile profile = getProfileWithEmail("toto@example.com"); 93 | 94 | String result = generator.getLink(profile, format); 95 | 96 | String expected = generator.configureLink(generator.getRawLink(profile), format); 97 | assertEquals(expected, result); 98 | } 99 | 100 | @Test 101 | public void isLink_should_be_true_if_gravatar_domain() { 102 | assertTrue(generator.isLink("https://www.gravatar.com/avatar/1ac9dec7bfff4aeb9bc9e625bfb9a4ce?s=50&r=g&d=identicon")); 103 | assertFalse(generator.isLink("https://avatar.com")); 104 | } 105 | 106 | private Profile getProfileWithEmail(String email) { 107 | User user = new User(); 108 | Profile profile = new Profile(); 109 | profile.setUser(user); 110 | user.setEmail(email); 111 | return profile; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/resources/themes/dark.css: -------------------------------------------------------------------------------- 1 | .dropdown-content li>a, .dropdown-content li>span { color: #efefef; } 2 | .btn, .btn-large { background-color: #424242; } 3 | .navbtn-hover { background-color: #6d6d6d; } 4 | .btn:focus, .btn-large:focus, .btn-floating:focus { background-color: #424242; } 5 | 6 | /* nav bar and footer */ 7 | nav ul a:hover, .dropdown-content li:hover, .dropdown-content li.active { background-color: #444; } 8 | .navbar-color, .spacelabel { background-color: #424242; } 9 | .page-footer .footer-copyright, .spacelabel { color: white; } 10 | .page-footer .btn-flat { color: white; } 11 | 12 | /* switches and inputs */ 13 | 14 | /* progress bars */ 15 | .progress .indeterminate { background-color: #efefef; } 16 | .progress { background-color: #6d6d6d; } 17 | 18 | /* tabs */ 19 | /*.tabs .tab a:hover, .tabs .tab a.active, .tabs .tab a { color: #efefef; }*/ 20 | .tabs .indicator { background-color: #424242; } 21 | 22 | /** DARK OVERRIDES **/ 23 | main, .scoold-wrapper, nav, .sidenav, .sidenav li>a { background-color: #202020; color: #efefef; } 24 | body, footer.page-footer, .footer-copyright, .card-panel, .card, .tabs { background-color: #1A1A1A !important; } 25 | #search-box, .white, .navbar-color, .orange.lighten-5, .CodeMirror { background-color: #333 !important; color: #efefef; } 26 | .chip, .dropdown-content, .collapsible-header, code, select, .modal, .editor-preview { background-color: #333; color: #efefef; } 27 | .CodeMirror pre.CodeMirror-line, .CodeMirror pre.CodeMirror-line-like { background-color: #333; } 28 | span.CodeMirror-selectedtext, li.CodeMirror-hint-active, .cm-comment.CodeMirror-selectedtext { background-color: #777; } 29 | input, textarea, .grey-text.text-darken-1, .btn-flat, .input-field>label, .black-text { color: #efefef !important; } 30 | .tabs .tab a:focus, .tabs .tab a:focus.active { background-color: #333; } 31 | .hljs, .viewbox pre { background: #333; background-color: #333; color: #efefef; border-radius: 3px; } 32 | .hljs-tag, .hljs-name, .hljs-attribute { color: #9999ff; } 33 | code[class*="language-"], pre[class*="language-"] { color: #efefef; } 34 | #gp-login-btn { background: white !important; color: #222 !important; } 35 | .grey { background-color: #777 !important; } 36 | .lightborder { border: 1px solid #777; } 37 | .editor-toolbar button, .CodeMirror-hint { color: #efefef; } 38 | .editor-toolbar.fullscreen, .CodeMirror-hints { background-color: #202020; color: #efefef; } 39 | .editor-toolbar button.active, .editor-toolbar button:hover { color: #333; } 40 | .editor-toolbar.fullscreen::before, .editor-toolbar.fullscreen::after { width: 0; } 41 | tr { border-bottom: 1px solid #777; } 42 | .autocomplete-content li .highlight { color: white; background-color: #202020; } 43 | .select-wrapper .caret { fill: white; } 44 | table.striped>tbody>tr:nth-child(odd) { background-color: #333; } 45 | .input-field input[type=search]:focus:not(.browser-default) { background-color: transparent; } 46 | .filter-active { background-color: #efefef; color: black; } 47 | .diff-ins { background-color: #9fff9f; color: black; } 48 | .diff-del { background-color: #ffa2a2; color: black; } 49 | .droptarget{ opacity: 0.5; outline: 5px dashed #777; } 50 | .swagger-ui *, .swagger-ui .opblock .opblock-summary-description, .swagger-ui .info .title, 51 | .swagger-ui .info li, .swagger-ui .info p, .swagger-ui .info table, .swagger-ui .dialog-ux .modal-ux-header h3, 52 | .swagger-ui .dialog-ux .modal-ux-content h4, .swagger-ui .dialog-ux .modal-ux-content p, .swagger-ui label, 53 | .swagger-ui .btn, .swagger-ui .opblock-description-wrapper p, .swagger-ui .opblock-external-docs-wrapper p, 54 | .swagger-ui .opblock-title_normal p, .swagger-ui table thead tr td, .swagger-ui table thead tr th, 55 | .swagger-ui .parameter__name, .swagger-ui .parameter__type, .swagger-ui .response-col_status, 56 | .swagger-ui .opblock .opblock-section-header h4, .swagger-ui .model-title, 57 | .swagger-ui .responses-inner h4, .swagger-ui .responses-inner h5, 58 | .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #efefef; } 59 | .swagger-ui .scheme-container { background-color: transparent; } 60 | .swagger-ui .dialog-ux .modal-ux, .swagger-ui input[type=email], .swagger-ui input[type=file], 61 | .swagger-ui input[type=password], .swagger-ui input[type=search], .swagger-ui input[type=text], 62 | .swagger-ui textarea, .swagger-ui .opblock .opblock-section-header, .swagger-ui select, 63 | .swagger-ui input[disabled], .swagger-ui select[disabled], .swagger-ui textarea[disabled] 64 | { background-color: #333; color: #efefef; } 65 | .swagger-ui section.models .model-container:hover { background-color: black; color: #efefef; } 66 | -------------------------------------------------------------------------------- /src/main/resources/static/styles/dark.css: -------------------------------------------------------------------------------- 1 | .dropdown-content li>a, .dropdown-content li>span { color: #efefef; } 2 | .btn, .btn-large { background-color: #424242; } 3 | .navbtn-hover { background-color: #6d6d6d; } 4 | .btn:focus, .btn-large:focus, .btn-floating:focus { background-color: #424242; } 5 | 6 | /* nav bar and footer */ 7 | nav ul a:hover, .dropdown-content li:hover, .dropdown-content li.active { background-color: #444; } 8 | .navbar-color, .spacelabel { background-color: #424242; } 9 | .page-footer .footer-copyright, .spacelabel { color: white; } 10 | .page-footer .btn-flat { color: white; } 11 | 12 | /* switches and inputs */ 13 | 14 | /* progress bars */ 15 | .progress .indeterminate { background-color: #efefef; } 16 | .progress { background-color: #6d6d6d; } 17 | 18 | /* tabs */ 19 | /*.tabs .tab a:hover, .tabs .tab a.active, .tabs .tab a { color: #efefef; }*/ 20 | .tabs .indicator { background-color: #424242; } 21 | 22 | /** DARK OVERRIDES **/ 23 | main, .scoold-wrapper, nav, .sidenav, .sidenav li>a { background-color: #202020; color: #efefef; } 24 | body, footer.page-footer, .footer-copyright, .card-panel, .card, .tabs { background-color: #1A1A1A !important; } 25 | #search-box, .white, .navbar-color, .orange.lighten-5, .CodeMirror { background-color: #333 !important; color: #efefef; } 26 | .chip, .dropdown-content, .collapsible-header, code, select, .modal, .editor-preview { background-color: #333; color: #efefef; } 27 | .CodeMirror pre.CodeMirror-line, .CodeMirror pre.CodeMirror-line-like { background-color: #333; } 28 | span.CodeMirror-selectedtext, li.CodeMirror-hint-active, .cm-comment.CodeMirror-selectedtext { background-color: #777; } 29 | input, textarea, .grey-text.text-darken-1, .btn-flat, .input-field>label, .black-text { color: #efefef !important; } 30 | .tabs .tab a:focus, .tabs .tab a:focus.active { background-color: #333; } 31 | .hljs, .viewbox pre { background: #333; background-color: #333; color: #efefef; border-radius: 3px; } 32 | .hljs-tag, .hljs-name, .hljs-attribute { color: #9999ff; } 33 | code[class*="language-"], pre[class*="language-"] { color: #efefef; } 34 | #gp-login-btn { background: white !important; color: #222 !important; } 35 | .grey { background-color: #777 !important; } 36 | .lightborder { border: 1px solid #777; } 37 | .editor-toolbar button, .CodeMirror-hint { color: #efefef; } 38 | .editor-toolbar.fullscreen, .CodeMirror-hints { background-color: #202020; color: #efefef; } 39 | .editor-toolbar button.active, .editor-toolbar button:hover { color: #333; } 40 | .editor-toolbar.fullscreen::before, .editor-toolbar.fullscreen::after { width: 0; } 41 | tr { border-bottom: 1px solid #777; } 42 | .autocomplete-content li .highlight { color: white; background-color: #202020; } 43 | .select-wrapper .caret { fill: white; } 44 | table.striped>tbody>tr:nth-child(odd) { background-color: #333; } 45 | .input-field input[type=search]:focus:not(.browser-default) { background-color: transparent; } 46 | .filter-active { background-color: #efefef; color: black; } 47 | .diff-ins { background-color: #9fff9f; color: black; } 48 | .diff-del { background-color: #ffa2a2; color: black; } 49 | .droptarget{ opacity: 0.5; outline: 5px dashed #777; } 50 | .swagger-ui *, .swagger-ui .opblock .opblock-summary-description, .swagger-ui .info .title, 51 | .swagger-ui .info li, .swagger-ui .info p, .swagger-ui .info table, .swagger-ui .dialog-ux .modal-ux-header h3, 52 | .swagger-ui .dialog-ux .modal-ux-content h4, .swagger-ui .dialog-ux .modal-ux-content p, .swagger-ui label, 53 | .swagger-ui .btn, .swagger-ui .opblock-description-wrapper p, .swagger-ui .opblock-external-docs-wrapper p, 54 | .swagger-ui .opblock-title_normal p, .swagger-ui table thead tr td, .swagger-ui table thead tr th, 55 | .swagger-ui .parameter__name, .swagger-ui .parameter__type, .swagger-ui .response-col_status, 56 | .swagger-ui .opblock .opblock-section-header h4, .swagger-ui .model-title, 57 | .swagger-ui .responses-inner h4, .swagger-ui .responses-inner h5, 58 | .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #efefef; } 59 | .swagger-ui .scheme-container { background-color: transparent; } 60 | .swagger-ui .dialog-ux .modal-ux, .swagger-ui input[type=email], .swagger-ui input[type=file], 61 | .swagger-ui input[type=password], .swagger-ui input[type=search], .swagger-ui input[type=text], 62 | .swagger-ui textarea, .swagger-ui .opblock .opblock-section-header, .swagger-ui select, 63 | .swagger-ui input[disabled], .swagger-ui select[disabled], .swagger-ui textarea[disabled] 64 | { background-color: #333; color: #efefef; } 65 | .swagger-ui section.models .model-container:hover { background-color: black; color: #efefef; } 66 | -------------------------------------------------------------------------------- /src/main/java/com/erudika/scoold/core/Report.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2022 Erudika. https://erudika.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * For issues and patches go to: https://github.com/erudika 17 | */ 18 | package com.erudika.scoold.core; 19 | 20 | import com.erudika.para.client.ParaClient; 21 | import com.erudika.para.core.Sysprop; 22 | import com.erudika.para.core.annotations.Stored; 23 | import com.erudika.scoold.utils.ScooldUtils; 24 | import java.util.Objects; 25 | 26 | /** 27 | * 28 | * @author Alex Bogdanovski [alex@erudika.com] 29 | */ 30 | public class Report extends Sysprop { 31 | private static final long serialVersionUID = 1L; 32 | 33 | @Stored private String subType; 34 | @Stored private String description; 35 | @Stored private String authorName; 36 | @Stored private String link; 37 | @Stored private String solution; 38 | @Stored private String content; 39 | @Stored private Boolean closed; 40 | 41 | public enum ReportType { 42 | SPAM, OFFENSIVE, DUPLICATE, INCORRECT, OTHER; 43 | 44 | public String toString() { 45 | return super.toString().toLowerCase(); 46 | } 47 | } 48 | 49 | public Report() { 50 | this(null, null, null, null); 51 | } 52 | 53 | public Report(String id) { 54 | this(null, null, null, null); 55 | setId(id); 56 | } 57 | 58 | public Report(String parentid, String type, String description, String creatorid) { 59 | setParentid(parentid); 60 | setCreatorid(creatorid); 61 | this.subType = ReportType.OTHER.toString(); 62 | this.description = subType; 63 | this.closed = false; 64 | } 65 | 66 | private ParaClient client() { 67 | return ScooldUtils.getInstance().getParaClient(); 68 | } 69 | 70 | public String getContent() { 71 | return content; 72 | } 73 | 74 | public void setContent(String content) { 75 | this.content = content; 76 | } 77 | 78 | public Boolean getClosed() { 79 | return closed; 80 | } 81 | 82 | public void setClosed(Boolean closed) { 83 | this.closed = closed; 84 | } 85 | 86 | public String getSolution() { 87 | return solution; 88 | } 89 | 90 | public void setSolution(String solution) { 91 | this.solution = solution; 92 | } 93 | 94 | public String getLink() { 95 | return link; 96 | } 97 | 98 | public void setLink(String link) { 99 | this.link = link; 100 | } 101 | 102 | public String getAuthorName() { 103 | return authorName; 104 | } 105 | 106 | public void setAuthorName(String authorName) { 107 | this.authorName = authorName; 108 | } 109 | 110 | public String getDescription() { 111 | return description; 112 | } 113 | 114 | public void setDescription(String description) { 115 | this.description = description; 116 | } 117 | 118 | public String getSubType() { 119 | if (subType == null) { 120 | subType = ReportType.OTHER.toString(); 121 | } 122 | return subType; 123 | } 124 | 125 | public void setSubType(String subType) { 126 | this.subType = subType; 127 | } 128 | 129 | public void setSubType(ReportType subType) { 130 | if (subType != null) { 131 | this.subType = subType.name(); 132 | } 133 | } 134 | 135 | public void delete() { 136 | client().delete(this); 137 | } 138 | 139 | public void update() { 140 | client().update(this); 141 | } 142 | 143 | public String create() { 144 | Report r = client().create(this); 145 | if (r != null) { 146 | ScooldUtils.getInstance().triggerHookEvent("report.create", this); 147 | setId(r.getId()); 148 | setTimestamp(r.getTimestamp()); 149 | return r.getId(); 150 | } 151 | return null; 152 | } 153 | 154 | public boolean equals(Object obj) { 155 | if (obj == null || getClass() != obj.getClass()) { 156 | return false; 157 | } 158 | return Objects.equals(getSubType(), ((Report) obj).getSubType()) && 159 | Objects.equals(getDescription(), ((Report) obj).getDescription()) && 160 | Objects.equals(getCreatorid(), ((Report) obj).getCreatorid()); 161 | } 162 | 163 | public int hashCode() { 164 | return Objects.hashCode(getSubType()) + Objects.hashCode(getDescription()) + 165 | Objects.hashCode(getCreatorid()) + Objects.hashCode(getParentid()); 166 | } 167 | 168 | } 169 | --------------------------------------------------------------------------------