├── .dockerignore
├── .github
└── workflows
│ ├── release_docker.yml
│ └── release_sources.yml
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cmd
└── lenpaste
│ └── lenpaste.go
├── entrypoint.sh
├── go.mod
├── go.sum
├── internal
├── apiv1
│ ├── api.go
│ ├── api_error.go
│ ├── api_get.go
│ ├── api_main.go
│ ├── api_new.go
│ └── api_server.go
├── cli
│ ├── cli.go
│ ├── duration.go
│ └── duration_test.go
├── config
│ └── config.go
├── lenpasswd
│ └── lenpasswd.go
├── lineend
│ ├── lineend.go
│ └── lineend_test.go
├── logger
│ └── logger.go
├── netshare
│ ├── netshare.go
│ ├── netshare_host.go
│ ├── netshare_paste.go
│ └── netshare_ratelimit.go
├── raw
│ ├── raw.go
│ ├── raw_error.go
│ └── raw_raw.go
├── storage
│ ├── share.go
│ ├── storage.go
│ └── storage_paste.go
└── web
│ ├── data
│ ├── about.tmpl
│ ├── authors.tmpl
│ ├── base.tmpl
│ ├── code.js
│ ├── docs.tmpl
│ ├── docs_api_libs.tmpl
│ ├── docs_apiv1.tmpl
│ ├── emb.tmpl
│ ├── emb_help.tmpl
│ ├── error.tmpl
│ ├── history.js
│ ├── license.tmpl
│ ├── locale
│ │ ├── bn_IN.json
│ │ ├── de.json
│ │ ├── en.json
│ │ └── ru.json
│ ├── main.js
│ ├── main.tmpl
│ ├── paste.js
│ ├── paste.tmpl
│ ├── paste_continue.tmpl
│ ├── settings.tmpl
│ ├── source_code.tmpl
│ ├── style.css
│ ├── terms.tmpl
│ └── theme
│ │ ├── dark.theme
│ │ └── light.theme
│ ├── kvcfg.go
│ ├── share.go
│ ├── web.go
│ ├── web_about.go
│ ├── web_dl.go
│ ├── web_docs.go
│ ├── web_embedded.go
│ ├── web_embedded_help.go
│ ├── web_error.go
│ ├── web_get.go
│ ├── web_highlight.go
│ ├── web_new.go
│ ├── web_other.go
│ ├── web_redirect.go
│ ├── web_settings.go
│ ├── web_sitemap.go
│ ├── web_terms.go
│ ├── web_themes.go
│ └── web_translate.go
└── tools
├── kvcfg-to-json
├── kvcfg.go
└── main.go
└── localefmt.sh
/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !.git/**
3 | !cmd/**/*.go
4 | !internal/**/*.go
5 | !internal/web/data/**
6 | !vendor/**
7 | !entrypoint.sh
8 | !go.mod
9 | !go.sum
10 | !Makefile
11 |
--------------------------------------------------------------------------------
/.github/workflows/release_docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker Image
2 | on:
3 | push:
4 | tags:
5 | - v**
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: read
12 | packages: write
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 |
19 | - name: Get version
20 | shell: bash
21 | run: echo "version=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" >> $GITHUB_ENV
22 |
23 | - name: Login to GitHub Container Registry
24 | uses: docker/login-action@v3
25 | with:
26 | registry: ghcr.io
27 | username: ${{github.repository_owner}}
28 | password: ${{secrets.GITHUB_TOKEN}}
29 |
30 | - name: Set up QEMU
31 | uses: docker/setup-qemu-action@v3
32 |
33 | - name: Set up Docker Buildx
34 | uses: docker/setup-buildx-action@v3
35 |
36 | - name: Build and push
37 | id: docker_build
38 | uses: docker/build-push-action@v5
39 | with:
40 | context: ./
41 | file: ./Dockerfile
42 | push: true
43 | tags: ghcr.io/${{github.repository_owner}}/lenpaste:${{env.version}}
44 | platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
45 |
46 | - name: Image digest
47 | run: echo ${{ steps.docker_build.outputs.digest }}
48 |
--------------------------------------------------------------------------------
/.github/workflows/release_sources.yml:
--------------------------------------------------------------------------------
1 | name: Tarball
2 | on:
3 | push:
4 | tags:
5 | - v**
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: write
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Get version
19 | shell: bash
20 | run: echo "version=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" >> $GITHUB_ENV
21 |
22 | - name: Install dependencies
23 | shell: bash
24 | run: sudo apt-get install -y make golang
25 |
26 | - name: Create tarball
27 | shell: bash
28 | run: |-
29 | make tarball
30 | sha256sum ./dist/sources/lenpaste-${{env.version}}.tar.gz
31 |
32 | - name: Attach tarball to release
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 | run: gh release upload v${{env.version}} ./dist/sources/lenpaste-${{env.version}}.tar.gz
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode/
2 | !/.vscode/extensions.json
3 |
4 | /dist/
5 | /vendor/
6 | **.bak
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | Semantic versioning is used (https://semver.org/).
3 |
4 |
5 | ## v1.3.1
6 | - Fixed a problem with building Lenpaste from source code.
7 | - Revised documentation.
8 | - Minor improvements were made.
9 |
10 | ## v1.3
11 | - UI: Added custom themes support. Added light theme.
12 | - UI: Added translations into Bengali and German (thanks Pardesi_Cat and Hiajen).
13 | - UI: Check boxes and spoilers now have a custom design.
14 | - Admin: Added support for `X-Real-IP` header for reverse proxy.
15 | - Admin: Added Server response header (for example: `Lenpaste/1.3`).
16 | - Fix: many bugs and errors.
17 | - Dev: Improved quality of `Dockerfile` and `entrypoint.sh`
18 |
19 | ## v1.2
20 | - UI: Add history tab.
21 | - UI: Add copy to clipboard button.
22 | - Admin: Rate-limits on paste creation (`LENPASTE_NEW_PASTES_PER_5MIN` or `-new-pastes-per-5min`).
23 | - Admin: Add terms of use support (`/data/terms` or `-server-terms`).
24 | - Admin: Add default paste life time for WEB interface (`LENPASTE_UI_DEFAULT_LIFETIME` or `-ui-default-lifetime`).
25 | - Admin: Private servers - password request to create paste (`/data/lenpasswd` or `-lenpasswd-file`).
26 | - Fix: **Critical security fix!**
27 | - Fix: not saving cookies.
28 | - Fix: display language name in WEB.
29 | - Fix: compatibility with WebKit (Gnome WEB).
30 | - Dev: Drop Go 1.15 support. Update dependencies.
31 |
32 |
33 | ## v1.1.1
34 | - Fixed: Incorrect operation of the maximum paste life parameter.
35 | - Updated README.
36 |
37 |
38 | ## v1.1
39 | - You can now specify author, author email and author URL for paste.
40 | - Full localization into Russian.
41 | - Added settings menu.
42 | - Paste creation and expiration times are now displayed in the user's time zone.
43 | - Add PostgreSQL DB support.
44 |
45 |
46 | ## v1.0
47 | This is the first stable release of Lenpaste🎉
48 |
49 | Compared to the previous unstable versions, everything has been drastically improved:
50 | design, loading speed of the pages, API, work with the database.
51 | Plus added syntax highlighting in the web interface.
52 |
53 |
54 | ## v0.2
55 | Features:
56 | - Paste title
57 | - About server information
58 | - Improved documentation
59 | - Logging and log rotation
60 | - Storage configuration
61 | - Code optimization
62 |
63 | Bug fixes:
64 | - Added `./version.json` to Docker image
65 | - Added paste expiration check before opening
66 | - Fixed incorrect error of expired pastes
67 | - API errors now return in JSON
68 |
69 |
70 | ## v0.1
71 | Features:
72 | - Alternative to pastebin.com
73 | - Creating expiration pastes
74 | - Web interface
75 | - API
76 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # BUILD
2 | FROM docker.io/library/debian:bookworm-20231120-slim as build
3 |
4 | WORKDIR /build
5 |
6 | RUN sed -i '/^URIs:/d' /etc/apt/sources.list.d/debian.sources && \
7 | sed -i 's/^# http/URIs: http/' /etc/apt/sources.list.d/debian.sources && \
8 | apt-get update -o Acquire::Check-Valid-Until=false && \
9 | apt-get install --no-install-recommends -y make git golang gcc libc6-dev ca-certificates && \
10 | apt-get clean
11 |
12 | COPY ./go.mod ./go.sum ./
13 | RUN go mod download
14 |
15 | COPY . ./
16 | RUN make
17 |
18 | # RUN
19 | FROM docker.io/library/debian:bookworm-20231120-slim
20 |
21 | COPY ./entrypoint.sh /
22 |
23 | RUN mkdir /data/ && chmod +x /entrypoint.sh
24 |
25 | VOLUME /data
26 | EXPOSE 80/tcp
27 |
28 | COPY --from=build /build/dist/bin/* /usr/local/bin/
29 |
30 | CMD [ "/entrypoint.sh" ]
31 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | GO ?= go
2 | GOFMT ?= gofmt
3 |
4 | NAME = lenpaste
5 | VERSION = $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
6 |
7 | MAIN_GO = ./cmd/$(NAME)/*.go
8 | LDFLAGS = -w -s -X "main.Version=$(VERSION)"
9 |
10 | .PHONY: all tarball fmt clean
11 |
12 | all:
13 | mkdir -p ./dist/bin/
14 |
15 | $(GO) build -trimpath -ldflags="$(LDFLAGS)" -o ./dist/bin/$(NAME) $(MAIN_GO)
16 | chmod +x ./dist/bin/$(NAME)
17 |
18 | tarball:
19 | mkdir -p ./dist/tmp/$(NAME)-$(VERSION)/
20 | cp -r $(filter-out ./. ./.. ./.git ./dist,$(shell echo ./* ./.*)) ./dist/tmp/$(NAME)-$(VERSION)/
21 |
22 | go mod vendor -o ./dist/tmp/$(NAME)-$(VERSION)/vendor/
23 |
24 | sed -i '/^COPY .*go.mod/d' ./dist/tmp/$(NAME)-$(VERSION)/Dockerfile
25 | sed -i '/^RUN go mod download/d' ./dist/tmp/$(NAME)-$(VERSION)/Dockerfile
26 | sed -i "s/ git / /" ./dist/tmp/$(NAME)-$(VERSION)/Dockerfile
27 | sed -i "s/ ca-certificates / /" ./dist/tmp/$(NAME)-$(VERSION)/Dockerfile
28 |
29 | sed -i "/^VERSION[[:space:]]*=/c\VERSION=$(VERSION)" ./dist/tmp/$(NAME)-$(VERSION)/Makefile
30 | sed -i "s/\$$(GO) build/\$$(GO) build -mod=vendor/" ./dist/tmp/$(NAME)-$(VERSION)/Makefile
31 |
32 | mkdir -p ./dist/sources/
33 | tar --mtime="$(git log -1 --format=%ai)" \
34 | -C ./dist/tmp/ \
35 | -zcf ./dist/sources/$(NAME)-$(VERSION).tar.gz $(NAME)-$(VERSION)/
36 | rm -rf ./dist/tmp/
37 |
38 | fmt:
39 | @$(GOFMT) -w $(shell find ./ -type f -name '*.go')
40 |
41 | clean:
42 | rm -rf ./dist/
43 |
--------------------------------------------------------------------------------
/cmd/lenpaste/lenpaste.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package main
20 |
21 | import (
22 | "errors"
23 | "fmt"
24 | "io"
25 | "net/http"
26 | "os"
27 | "strconv"
28 | "time"
29 |
30 | "github.com/lcomrade/lenpaste/internal/apiv1"
31 | "github.com/lcomrade/lenpaste/internal/cli"
32 | "github.com/lcomrade/lenpaste/internal/config"
33 | "github.com/lcomrade/lenpaste/internal/logger"
34 | "github.com/lcomrade/lenpaste/internal/netshare"
35 | "github.com/lcomrade/lenpaste/internal/raw"
36 | "github.com/lcomrade/lenpaste/internal/storage"
37 | "github.com/lcomrade/lenpaste/internal/web"
38 | )
39 |
40 | var Version = "unknown"
41 |
42 | func readFile(path string) (string, error) {
43 | // Open file
44 | file, err := os.Open(path)
45 | if err != nil {
46 | return "", err
47 | }
48 | defer file.Close()
49 |
50 | // Read file
51 | fileByte, err := io.ReadAll(file)
52 | if err != nil {
53 | return "", err
54 | }
55 |
56 | // Return result
57 | return string(fileByte), nil
58 | }
59 |
60 | func exitOnError(e error) {
61 | fmt.Fprintln(os.Stderr, "error:", e.Error())
62 | os.Exit(1)
63 | }
64 |
65 | func main() {
66 | var err error
67 |
68 | // Read environment variables and CLI flags
69 | c := cli.New(Version)
70 |
71 | flagAddress := c.AddStringVar("address", ":80", "HTTP server ADDRESS:PORT.", nil)
72 |
73 | flagDbDriver := c.AddStringVar("db-driver", "sqlite3", "Currently supported drivers: \"sqlite3\" and \"postgres\".", nil)
74 | flagDbSource := c.AddStringVar("db-source", "", "DB source.", &cli.FlagOptions{Required: true})
75 | flagDbMaxOpenConns := c.AddIntVar("db-max-open-conns", 25, "Maximum number of connections to the database.", nil)
76 | flagDbMaxIdleConns := c.AddIntVar("db-max-idle-conns", 5, "Maximum number of idle connections to the database.", nil)
77 | flagDbCleanupPeriod := c.AddDurationVar("db-cleanup-period", "1m", "Interval at which the DB is cleared of expired but not yet deleted pastes.", nil)
78 |
79 | flagRobotsDisallow := c.AddBoolVar("robots-disallow", "Prohibits search engine crawlers from indexing site using robots.txt file.")
80 |
81 | flagTitleMaxLen := c.AddIntVar("title-max-length", 100, "Maximum length of the paste title. If 0 disable title, if -1 disable length limit.", nil)
82 | flagBodyMaxLen := c.AddIntVar("body-max-length", 20000, "Maximum length of the paste body. If -1 disable length limit. Can't be -1.", nil)
83 | flagMaxLifetime := c.AddDurationVar("max-paste-lifetime", "unlimited", "Maximum lifetime of the paste. Examples: 10m, 1h 30m, 12h, 1w, 30d, 365d.", &cli.FlagOptions{
84 | PreHook: func(s string) (string, error) {
85 | if s == "never" || s == "unlimited" {
86 | return "", nil
87 | }
88 |
89 | return s, nil
90 | },
91 | })
92 |
93 | flagGetPastesPer5Min := c.AddUintVar("get-pastes-per-5min", 50, "Maximum number of pastes that can be VIEWED in 5 minutes from one IP. If 0 disable rate-limit.", nil)
94 | flagGetPastesPer15Min := c.AddUintVar("get-pastes-per-15min", 100, "Maximum number of pastes that can be VIEWED in 15 minutes from one IP. If 0 disable rate-limit.", nil)
95 | flagGetPastesPer1Hour := c.AddUintVar("get-pastes-per-1hour", 500, "Maximum number of pastes that can be VIEWED in 1 hour from one IP. If 0 disable rate-limit.", nil)
96 | flagNewPastesPer5Min := c.AddUintVar("new-pastes-per-5min", 15, "Maximum number of pastes that can be CREATED in 5 minutes from one IP. If 0 disable rate-limit.", nil)
97 | flagNewPastesPer15Min := c.AddUintVar("new-pastes-per-15min", 30, "Maximum number of pastes that can be CREATED in 15 minutes from one IP. If 0 disable rate-limit.", nil)
98 | flagNewPastesPer1Hour := c.AddUintVar("new-pastes-per-1hour", 40, "Maximum number of pastes that can be CREATED in 1 hour from one IP. If 0 disable rate-limit.", nil)
99 |
100 | flagServerAbout := c.AddStringVar("server-about", "", "Path to the TXT file that contains the server description.", nil)
101 | flagServerRules := c.AddStringVar("server-rules", "", "Path to the TXT file that contains the server rules.", nil)
102 | flagServerTerms := c.AddStringVar("server-terms", "", "Path to the TXT file that contains the server terms of use.", nil)
103 |
104 | flagAdminName := c.AddStringVar("admin-name", "", "Name of the administrator of this server.", nil)
105 | flagAdminMail := c.AddStringVar("admin-mail", "", "Email of the administrator of this server.", nil)
106 |
107 | flagUiDefaultLifetime := c.AddStringVar("ui-default-lifetime", "", "Lifetime of paste will be set by default in WEB interface. Examples: 10min, 1h, 1d, 2w, 6mon, 1y.", nil)
108 | flagUiDefaultTheme := c.AddStringVar("ui-default-theme", "dark", "Sets the default theme for the WEB interface. Examples: dark, light, my_theme.", nil)
109 | flagUiThemesDir := c.AddStringVar("ui-themes-dir", "", "Loads external WEB interface themes from directory.", nil)
110 |
111 | flagLenPasswdFile := c.AddStringVar("lenpasswd-file", "", "File in LenPasswd format. If set, authorization will be required to create pastes.", nil)
112 |
113 | c.Parse()
114 |
115 | // -body-max-length flag
116 | if *flagBodyMaxLen == 0 {
117 | exitOnError(errors.New("maximum body length cannot be 0"))
118 | }
119 |
120 | // -max-paste-lifetime
121 | maxLifeTime := int64(-1)
122 |
123 | if *flagMaxLifetime != 0 && *flagMaxLifetime < 600 {
124 | exitOnError(errors.New("maximum paste lifetime flag cannot have a value less than 10 minutes"))
125 | maxLifeTime = int64(*flagMaxLifetime / time.Second)
126 | }
127 |
128 | // Load server about
129 | serverAbout := ""
130 | if *flagServerAbout != "" {
131 | serverAbout, err = readFile(*flagServerAbout)
132 | if err != nil {
133 | exitOnError(err)
134 | }
135 | }
136 |
137 | // Load server rules
138 | serverRules := ""
139 | if *flagServerRules != "" {
140 | serverRules, err = readFile(*flagServerRules)
141 | if err != nil {
142 | exitOnError(err)
143 | }
144 | }
145 |
146 | // Load server "terms of use"
147 | serverTermsOfUse := ""
148 | if *flagServerTerms != "" {
149 | if serverRules == "" {
150 | exitOnError(errors.New("in order to set the Terms of Use you must also specify the Server Rules"))
151 | }
152 |
153 | serverTermsOfUse, err = readFile(*flagServerTerms)
154 | if err != nil {
155 | exitOnError(err)
156 | }
157 | }
158 |
159 | // Settings
160 | log := logger.New("2006/01/02 15:04:05")
161 |
162 | db, err := storage.NewPool(*flagDbDriver, *flagDbSource, *flagDbMaxOpenConns, *flagDbMaxIdleConns)
163 | if err != nil {
164 | exitOnError(err)
165 | }
166 |
167 | cfg := config.Config{
168 | Log: log,
169 | RateLimitGet: netshare.NewRateLimitSystem(*flagGetPastesPer5Min, *flagGetPastesPer15Min, *flagGetPastesPer1Hour),
170 | RateLimitNew: netshare.NewRateLimitSystem(*flagNewPastesPer5Min, *flagNewPastesPer15Min, *flagNewPastesPer1Hour),
171 | Version: Version,
172 | TitleMaxLen: *flagTitleMaxLen,
173 | BodyMaxLen: *flagBodyMaxLen,
174 | MaxLifeTime: maxLifeTime,
175 | ServerAbout: serverAbout,
176 | ServerRules: serverRules,
177 | ServerTermsOfUse: serverTermsOfUse,
178 | AdminName: *flagAdminName,
179 | AdminMail: *flagAdminMail,
180 | RobotsDisallow: *flagRobotsDisallow,
181 | UiDefaultLifetime: *flagUiDefaultLifetime,
182 | UiDefaultTheme: *flagUiDefaultTheme,
183 | UiThemesDir: *flagUiThemesDir,
184 | LenPasswdFile: *flagLenPasswdFile,
185 | }
186 |
187 | apiv1Data := apiv1.Load(db, cfg)
188 |
189 | rawData := raw.Load(db, cfg)
190 |
191 | // Init data base
192 | err = storage.InitDB(*flagDbDriver, *flagDbSource)
193 | if err != nil {
194 | exitOnError(err)
195 | }
196 |
197 | // Load pages
198 | webData, err := web.Load(db, cfg)
199 | if err != nil {
200 | exitOnError(err)
201 | }
202 |
203 | // Handlers
204 | http.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
205 | webData.Handler(rw, req)
206 | })
207 | http.HandleFunc("/raw/", func(rw http.ResponseWriter, req *http.Request) {
208 | rawData.Hand(rw, req)
209 | })
210 | http.HandleFunc("/api/", func(rw http.ResponseWriter, req *http.Request) {
211 | apiv1Data.Hand(rw, req)
212 | })
213 |
214 | // Run background job
215 | go func(cleanJobPeriod time.Duration) {
216 | for {
217 | // Delete expired pastes
218 | count, err := db.PasteDeleteExpired()
219 | if err != nil {
220 | log.Error(errors.New("Delete expired: " + err.Error()))
221 | }
222 |
223 | log.Info("Delete " + strconv.FormatInt(count, 10) + " expired pastes")
224 |
225 | // Wait
226 | time.Sleep(cleanJobPeriod)
227 | }
228 | }(*flagDbCleanupPeriod)
229 |
230 | // Run HTTP server
231 | log.Info("Run HTTP server on " + *flagAddress)
232 | err = http.ListenAndServe(*flagAddress, nil)
233 | if err != nil {
234 | exitOnError(err)
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | RUN_CMD="lenpaste"
5 |
6 |
7 | # LENPASTE_ADDRESS
8 | if [ -n "$LENPASTE_ADDRESS" ]; then
9 | RUN_CMD="$RUN_CMD -address $LENPASTE_ADDRESS"
10 | fi
11 |
12 |
13 | # LENPASTE_DB_DRIVER
14 | if [ -n "$LENPASTE_DB_DRIVER" ]; then
15 | RUN_CMD="$RUN_CMD -db-driver '$LENPASTE_DB_DRIVER'"
16 | fi
17 |
18 |
19 | # LENPASTE_DB_SOURCE
20 | if [ -z "$LENPASTE_DB_DRIVER" ] || [ "$LENPASTE_DB_DRIVER" = "sqlite3" ]; then
21 | RUN_CMD="$RUN_CMD -db-source /data/lenpaste.db"
22 |
23 | else
24 | RUN_CMD="$RUN_CMD -db-source '$LENPASTE_DB_SOURCE'"
25 | fi
26 |
27 |
28 | # LENPASTE_DB_MAX_OPEN_CONNS
29 | if [ -n "$LENPASTE_DB_MAX_OPEN_CONNS" ]; then
30 | RUN_CMD="$RUN_CMD -db-max-open-conns '$LENPASTE_DB_MAX_OPEN_CONNS'"
31 | fi
32 |
33 |
34 | # LENPASTE_DB_MAX_IDLE_CONNS
35 | if [ -n "$LENPASTE_DB_MAX_IDLE_CONNS" ]; then
36 | RUN_CMD="$RUN_CMD -db-max-idle-conns '$LENPASTE_DB_MAX_IDLE_CONNS'"
37 | fi
38 |
39 |
40 | # LENPASTE_DB_CLEANUP_PERIOD
41 | if [ -n "$LENPASTE_DB_CLEANUP_PERIOD" ]; then
42 | RUN_CMD="$RUN_CMD -db-cleanup-period '$LENPASTE_DB_CLEANUP_PERIOD'"
43 | fi
44 |
45 | # LENPASTE_ROBOTS_DISALLOW
46 | if [ "$LENPASTE_ROBOTS_DISALLOW" = "true" ]; then
47 | RUN_CMD="$RUN_CMD -robots-disallow"
48 |
49 | else
50 | if [ "$LENPASTE_ROBOTS_DISALLOW" != "" ] && [ "$LENPASTE_ROBOTS_DISALLOW" != "false" ]; then
51 | echo "[ENTRYPOINT] Error: unknown: LENPASTE_ROBOTS_DISALLOW = $LENPASTE_ROBOTS_DISALLOW"
52 | exit 2
53 | fi
54 | fi
55 |
56 |
57 | # LENPASTE_TITLE_MAX_LENGTH
58 | if [ -n "$LENPASTE_TITLE_MAX_LENGTH" ]; then
59 | RUN_CMD="$RUN_CMD -title-max-length '$LENPASTE_TITLE_MAX_LENGTH'"
60 | fi
61 |
62 |
63 | # LENPASTE_BODY_MAX_LENGTH
64 | if [ -n "$LENPASTE_BODY_MAX_LENGTH" ]; then
65 | RUN_CMD="$RUN_CMD -body-max-length '$LENPASTE_BODY_MAX_LENGTH'"
66 | fi
67 |
68 |
69 | # LENPASTE_MAX_PASTE_LIFETIME
70 | if [ -n "$LENPASTE_MAX_PASTE_LIFETIME" ]; then
71 | RUN_CMD="$RUN_CMD -max-paste-lifetime '$LENPASTE_MAX_PASTE_LIFETIME'"
72 | fi
73 |
74 | # Rate limits to get
75 | if [ -n "$LENPASTE_GET_PASTES_PER_5MIN" ]; then
76 | RUN_CMD="$RUN_CMD -get-pastes-per-5min '$LENPASTE_GET_PASTES_PER_5MIN'"
77 | fi
78 |
79 | if [ -n "$LENPASTE_GET_PASTES_PER_15MIN" ]; then
80 | RUN_CMD="$RUN_CMD -get-pastes-per-15min '$LENPASTE_GET_PASTES_PER_15MIN'"
81 | fi
82 |
83 | if [ -n "$LENPASTE_GET_PASTES_PER_1HOUR" ]; then
84 | RUN_CMD="$RUN_CMD -get-pastes-per-1hour '$LENPASTE_GET_PASTES_PER_1HOUR'"
85 | fi
86 |
87 |
88 | # Rate limits to create
89 | if [ -n "$LENPASTE_NEW_PASTES_PER_5MIN" ]; then
90 | RUN_CMD="$RUN_CMD -new-pastes-per-5min '$LENPASTE_NEW_PASTES_PER_5MIN'"
91 | fi
92 |
93 | if [ -n "$LENPASTE_NEW_PASTES_PER_15MIN" ]; then
94 | RUN_CMD="$RUN_CMD -new-pastes-per-15min '$LENPASTE_NEW_PASTES_PER_15MIN'"
95 | fi
96 |
97 | if [ -n "$LENPASTE_NEW_PASTES_PER_1HOUR" ]; then
98 | RUN_CMD="$RUN_CMD -new-pastes-per-1hour '$LENPASTE_NEW_PASTES_PER_1HOUR'"
99 | fi
100 |
101 |
102 |
103 | # Server about
104 | if [ -f "/data/about" ]; then
105 | RUN_CMD="$RUN_CMD -server-about /data/about"
106 | fi
107 |
108 |
109 | # Server rules
110 | if [ -f "/data/rules" ]; then
111 | RUN_CMD="$RUN_CMD -server-rules /data/rules"
112 | fi
113 |
114 |
115 | # Server terms of use
116 | if [ -f "/data/terms" ]; then
117 | RUN_CMD="$RUN_CMD -server-terms /data/terms"
118 | fi
119 |
120 |
121 | # LENPASTE_ADMIN_NAME
122 | if [ -n "$LENPASTE_ADMIN_NAME" ]; then
123 | RUN_CMD="$RUN_CMD -admin-name '$LENPASTE_ADMIN_NAME'"
124 | fi
125 |
126 |
127 | # LENPASTE_ADMIN_MAIL
128 | if [ -n "$LENPASTE_ADMIN_MAIL" ]; then
129 | RUN_CMD="$RUN_CMD -admin-mail '$LENPASTE_ADMIN_MAIL'"
130 | fi
131 |
132 |
133 | # LENPASTE_UI_DEFAULT_LIFETIME
134 | if [ -n "$LENPASTE_UI_DEFAULT_LIFETIME" ]; then
135 | RUN_CMD="$RUN_CMD -ui-default-lifetime '$LENPASTE_UI_DEFAULT_LIFETIME'"
136 | fi
137 |
138 |
139 | # LENPASTE_UI_DEFAULT_THEME
140 | if [ -n "$LENPASTE_UI_DEFAULT_THEME" ]; then
141 | RUN_CMD="$RUN_CMD -ui-default-theme $LENPASTE_UI_DEFAULT_THEME"
142 | fi
143 |
144 |
145 | # External UI themes
146 | if [ -d "/data/themes" ]; then
147 | RUN_CMD="$RUN_CMD -ui-themes-dir /data/themes"
148 | fi
149 |
150 |
151 | # Lenpsswd file
152 | if [ -f "/data/lenpasswd" ]; then
153 | RUN_CMD="$RUN_CMD -lenpasswd-file /data/lenpasswd"
154 | fi
155 |
156 |
157 | # Run Lenpaste
158 | echo "[ENTRYPOINT] $RUN_CMD"
159 | sh -c "$RUN_CMD"
160 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/lcomrade/lenpaste
2 |
3 | go 1.16
4 |
5 | replace github.com/lcomrade/lenpaste/internal => ./internal
6 |
7 | require (
8 | github.com/alecthomas/chroma/v2 v2.4.0
9 | github.com/lib/pq v1.10.7
10 | github.com/mattn/go-sqlite3 v1.14.16
11 | )
12 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/lcomrade/lenpaste/internal/lineend v1.0.0 h1:qcxrR4DS18Erx+pG/EspoDhEDai+mjgSYefwSK2dq5g=
2 | github.com/lcomrade/lenpaste/internal/lineend v1.0.0/go.mod h1:D0q3jMx0I1PFZjAFwkmUNW8D5tIw8rZJoeUAxhYD7Ec=
3 | github.com/alecthomas/assert/v2 v2.2.0 h1:f6L/b7KE2bfA+9O4FL3CM/xJccDEwPVYd5fALBiuwvw=
4 | github.com/alecthomas/assert/v2 v2.2.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
5 | github.com/alecthomas/chroma/v2 v2.4.0 h1:Loe2ZjT5x3q1bcWwemqyqEi8p11/IV/ncFCeLYDpWC4=
6 | github.com/alecthomas/chroma/v2 v2.4.0/go.mod h1:6kHzqF5O6FUSJzBXW7fXELjb+e+7OXW4UpoPqMO7IBQ=
7 | github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
8 | github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
9 | github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
10 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
11 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
12 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
13 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
14 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
15 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
16 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
17 |
--------------------------------------------------------------------------------
/internal/apiv1/api.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package apiv1
20 |
21 | import (
22 | chromaLexers "github.com/alecthomas/chroma/v2/lexers"
23 | "github.com/lcomrade/lenpaste/internal/config"
24 | "github.com/lcomrade/lenpaste/internal/logger"
25 | "github.com/lcomrade/lenpaste/internal/netshare"
26 | "github.com/lcomrade/lenpaste/internal/storage"
27 | "net/http"
28 | )
29 |
30 | type Data struct {
31 | Log logger.Logger
32 | DB storage.DB
33 |
34 | RateLimitNew *netshare.RateLimitSystem
35 | RateLimitGet *netshare.RateLimitSystem
36 |
37 | Lexers []string
38 |
39 | Version string
40 |
41 | TitleMaxLen int
42 | BodyMaxLen int
43 | MaxLifeTime int64
44 |
45 | ServerAbout string
46 | ServerRules string
47 | ServerTermsOfUse string
48 |
49 | AdminName string
50 | AdminMail string
51 |
52 | LenPasswdFile string
53 |
54 | UiDefaultLifeTime string
55 | }
56 |
57 | func Load(db storage.DB, cfg config.Config) *Data {
58 | lexers := chromaLexers.Names(false)
59 |
60 | return &Data{
61 | DB: db,
62 | Log: cfg.Log,
63 | RateLimitNew: cfg.RateLimitNew,
64 | RateLimitGet: cfg.RateLimitGet,
65 | Lexers: lexers,
66 | Version: cfg.Version,
67 | TitleMaxLen: cfg.TitleMaxLen,
68 | BodyMaxLen: cfg.BodyMaxLen,
69 | MaxLifeTime: cfg.MaxLifeTime,
70 | ServerAbout: cfg.ServerAbout,
71 | ServerRules: cfg.ServerRules,
72 | ServerTermsOfUse: cfg.ServerTermsOfUse,
73 | AdminName: cfg.AdminName,
74 | AdminMail: cfg.AdminMail,
75 | LenPasswdFile: cfg.LenPasswdFile,
76 | UiDefaultLifeTime: cfg.UiDefaultLifetime,
77 | }
78 | }
79 |
80 | func (data *Data) Hand(rw http.ResponseWriter, req *http.Request) {
81 | // Process request
82 | var err error
83 |
84 | rw.Header().Set("Server", config.Software+"/"+data.Version)
85 |
86 | switch req.URL.Path {
87 | // Search engines
88 | case "/api/v1/new":
89 | err = data.newHand(rw, req)
90 | case "/api/v1/get":
91 | err = data.getHand(rw, req)
92 | case "/api/v1/getServerInfo":
93 | err = data.getServerInfoHand(rw, req)
94 | default:
95 | err = netshare.ErrNotFound
96 | }
97 |
98 | // Log
99 | if err == nil {
100 | data.Log.HttpRequest(req, 200)
101 |
102 | } else {
103 | code, err := data.writeError(rw, req, err)
104 | if err != nil {
105 | data.Log.HttpError(req, err)
106 | } else {
107 | data.Log.HttpRequest(req, code)
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/internal/apiv1/api_error.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package apiv1
20 |
21 | import (
22 | "encoding/json"
23 | "errors"
24 | "github.com/lcomrade/lenpaste/internal/netshare"
25 | "github.com/lcomrade/lenpaste/internal/storage"
26 | "net/http"
27 | "strconv"
28 | )
29 |
30 | type errorType struct {
31 | Code int `json:"code"`
32 | Error string `json:"error"`
33 | }
34 |
35 | func (data *Data) writeError(rw http.ResponseWriter, req *http.Request, e error) (int, error) {
36 | var resp errorType
37 |
38 | var eTmp429 *netshare.ErrTooManyRequests
39 |
40 | if e == netshare.ErrBadRequest {
41 | resp.Code = 400
42 | resp.Error = "Bad Request"
43 |
44 | } else if e == netshare.ErrUnauthorized {
45 | rw.Header().Add("WWW-Authenticate", "Basic")
46 | resp.Code = 401
47 | resp.Error = "Unauthorized"
48 |
49 | } else if e == storage.ErrNotFoundID {
50 | resp.Code = 404
51 | resp.Error = "Could not find ID"
52 |
53 | } else if e == netshare.ErrNotFound {
54 | resp.Code = 404
55 | resp.Error = "Not Found"
56 |
57 | } else if e == netshare.ErrMethodNotAllowed {
58 | resp.Code = 405
59 | resp.Error = "Method Not Allowed"
60 |
61 | } else if e == netshare.ErrPayloadTooLarge {
62 | resp.Code = 413
63 | resp.Error = "Payload Too Large"
64 |
65 | } else if errors.As(e, &eTmp429) {
66 | resp.Code = 429
67 | resp.Error = "Too Many Requests"
68 | rw.Header().Set("Retry-After", strconv.FormatInt(eTmp429.RetryAfter, 10))
69 |
70 | } else {
71 | resp.Code = 500
72 | resp.Error = "Internal Server Error"
73 | }
74 |
75 | rw.Header().Set("Content-Type", "application/json")
76 | rw.WriteHeader(resp.Code)
77 |
78 | err := json.NewEncoder(rw).Encode(resp)
79 | if err != nil {
80 | return 500, err
81 | }
82 |
83 | return resp.Code, nil
84 | }
85 |
--------------------------------------------------------------------------------
/internal/apiv1/api_get.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package apiv1
20 |
21 | import (
22 | "encoding/json"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | "github.com/lcomrade/lenpaste/internal/storage"
25 | "net/http"
26 | )
27 |
28 | // GET /api/v1/get
29 | func (data *Data) getHand(rw http.ResponseWriter, req *http.Request) error {
30 | // Check rate limit
31 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req))
32 | if err != nil {
33 | return err
34 | }
35 |
36 | // Check method
37 | if req.Method != "GET" {
38 | return netshare.ErrMethodNotAllowed
39 | }
40 |
41 | // Get paste ID
42 | req.ParseForm()
43 |
44 | pasteID := req.Form.Get("id")
45 |
46 | // Check paste id
47 | if pasteID == "" {
48 | return netshare.ErrBadRequest
49 | }
50 |
51 | // Get paste
52 | paste, err := data.DB.PasteGet(pasteID)
53 | if err != nil {
54 | return err
55 | }
56 |
57 | // If "one use" paste
58 | if paste.OneUse == true {
59 | if req.Form.Get("openOneUse") == "true" {
60 | // Delete paste
61 | err = data.DB.PasteDelete(pasteID)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | } else {
67 | // Remove secret data
68 | paste = storage.Paste{
69 | ID: paste.ID,
70 | OneUse: true,
71 | }
72 | }
73 | }
74 |
75 | // Return response
76 | rw.Header().Set("Content-Type", "application/json")
77 | return json.NewEncoder(rw).Encode(paste)
78 | }
79 |
--------------------------------------------------------------------------------
/internal/apiv1/api_main.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package apiv1
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/netshare"
23 | "net/http"
24 | )
25 |
26 | // GET /api/v1/
27 | func (data *Data) MainHand(rw http.ResponseWriter, req *http.Request) {
28 | data.writeError(rw, req, netshare.ErrNotFound)
29 | }
30 |
--------------------------------------------------------------------------------
/internal/apiv1/api_new.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package apiv1
20 |
21 | import (
22 | "encoding/json"
23 | "github.com/lcomrade/lenpaste/internal/lenpasswd"
24 | "github.com/lcomrade/lenpaste/internal/netshare"
25 | "net/http"
26 | )
27 |
28 | type newPasteAnswer struct {
29 | ID string `json:"id"`
30 | CreateTime int64 `json:"createTime"`
31 | DeleteTime int64 `json:"deleteTime"`
32 | }
33 |
34 | // POST /api/v1/new
35 | func (data *Data) newHand(rw http.ResponseWriter, req *http.Request) error {
36 | var err error
37 |
38 | // Check auth
39 | if data.LenPasswdFile != "" {
40 | authOk := false
41 |
42 | user, pass, authExist := req.BasicAuth()
43 | if authExist == true {
44 | authOk, err = lenpasswd.LoadAndCheck(data.LenPasswdFile, user, pass)
45 | if err != nil {
46 | return err
47 | }
48 | }
49 |
50 | if authOk == false {
51 | return netshare.ErrUnauthorized
52 | }
53 | }
54 |
55 | // Check method
56 | if req.Method != "POST" {
57 | return netshare.ErrMethodNotAllowed
58 | }
59 |
60 | // Get form data and create paste
61 | pasteID, createTime, deleteTime, err := netshare.PasteAddFromForm(req, data.DB, data.RateLimitNew, data.TitleMaxLen, data.BodyMaxLen, data.MaxLifeTime, data.Lexers)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | // Return response
67 | rw.Header().Set("Content-Type", "application/json")
68 | return json.NewEncoder(rw).Encode(newPasteAnswer{ID: pasteID, CreateTime: createTime, DeleteTime: deleteTime})
69 | }
70 |
--------------------------------------------------------------------------------
/internal/apiv1/api_server.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package apiv1
20 |
21 | import (
22 | "encoding/json"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | "net/http"
25 | )
26 |
27 | type serverInfoType struct {
28 | Software string `json:"software"`
29 | Version string `json:"version"`
30 | TitleMaxLen int `json:"titleMaxlength"`
31 | BodyMaxLen int `json:"bodyMaxlength"`
32 | MaxLifeTime int64 `json:"maxLifeTime"`
33 | ServerAbout string `json:"serverAbout"`
34 | ServerRules string `json:"serverRules"`
35 | ServerTermsOfUse string `json:"serverTermsOfUse"`
36 | AdminName string `json:"adminName"`
37 | AdminMail string `json:"adminMail"`
38 | Syntaxes []string `json:"syntaxes"`
39 | UiDefaultLifeTime string `json:"uiDefaultLifeTime"`
40 | AuthRequired bool `json:"authRequired"`
41 | }
42 |
43 | // GET /api/v1/getServerInfo
44 | func (data *Data) getServerInfoHand(rw http.ResponseWriter, req *http.Request) error {
45 | // Check method
46 | if req.Method != "GET" {
47 | return netshare.ErrMethodNotAllowed
48 | }
49 |
50 | // Prepare data
51 | serverInfo := serverInfoType{
52 | Software: "Lenpaste",
53 | Version: data.Version,
54 | TitleMaxLen: data.TitleMaxLen,
55 | BodyMaxLen: data.BodyMaxLen,
56 | MaxLifeTime: data.MaxLifeTime,
57 | ServerAbout: data.ServerAbout,
58 | ServerRules: data.ServerRules,
59 | ServerTermsOfUse: data.ServerTermsOfUse,
60 | AdminName: data.AdminName,
61 | AdminMail: data.AdminMail,
62 | Syntaxes: data.Lexers,
63 | UiDefaultLifeTime: data.UiDefaultLifeTime,
64 | AuthRequired: data.LenPasswdFile != "",
65 | }
66 |
67 | // Return response
68 | rw.Header().Set("Content-Type", "application/json")
69 | return json.NewEncoder(rw).Encode(serverInfo)
70 | }
71 |
--------------------------------------------------------------------------------
/internal/cli/cli.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package cli
20 |
21 | import (
22 | "fmt"
23 | "os"
24 | "strconv"
25 | "time"
26 | )
27 |
28 | func exitOnError(msg string) {
29 | fmt.Fprintln(os.Stderr, "error:", msg)
30 | os.Exit(1)
31 | }
32 |
33 | type variable struct {
34 | name string
35 | cliFlagName string
36 |
37 | preHook func(string) (string, error)
38 |
39 | value interface{}
40 | valueDefault string
41 | required bool
42 | usage string
43 | }
44 |
45 | type CLI struct {
46 | version string
47 |
48 | vars []variable
49 | }
50 |
51 | type FlagOptions struct {
52 | Required bool
53 | PreHook func(string) (string, error)
54 | }
55 |
56 | func New(version string) *CLI {
57 | return &CLI{
58 | version: version,
59 |
60 | vars: []variable{},
61 | }
62 | }
63 |
64 | func (c *CLI) addVar(name string, value interface{}, defValue string, usage string, opts *FlagOptions) {
65 | if name == "" {
66 | panic("cli: add variable: variable name could not be empty")
67 | }
68 |
69 | if usage == "" {
70 | panic("cli: flag \"" + name + "\" has empty \"usage\" field")
71 | }
72 |
73 | if opts == nil {
74 | opts = &FlagOptions{}
75 | }
76 |
77 | c.vars = append(c.vars, variable{
78 | name: name,
79 | cliFlagName: "-" + name,
80 |
81 | preHook: opts.PreHook,
82 |
83 | value: value,
84 | valueDefault: defValue,
85 | required: opts.Required,
86 | usage: usage,
87 | })
88 | }
89 |
90 | func (c *CLI) AddStringVar(name, defValue string, usage string, opts *FlagOptions) *string {
91 | if opts != nil {
92 | if opts.PreHook != nil {
93 | var err error
94 | defValue, err = opts.PreHook(defValue)
95 | if err != nil {
96 | panic("cli: add duration variable \"" + name + "\": " + err.Error())
97 | }
98 | }
99 | }
100 |
101 | val := &defValue
102 | c.addVar(name, val, defValue, usage, opts)
103 | return val
104 | }
105 |
106 | func (c *CLI) AddBoolVar(name string, usage string) *bool {
107 | valVar := false
108 | val := &valVar
109 | c.addVar(name, val, "", usage, nil)
110 | return val
111 | }
112 |
113 | func (c *CLI) AddIntVar(name string, defValue int, usage string, opts *FlagOptions) *int {
114 | val := &defValue
115 | c.addVar(name, val, strconv.Itoa(defValue), usage, opts)
116 | return val
117 | }
118 |
119 | func (c *CLI) AddUintVar(name string, defValue uint, usage string, opts *FlagOptions) *uint {
120 | val := &defValue
121 | c.addVar(name, val, strconv.FormatUint(uint64(defValue), 10), usage, opts)
122 | return val
123 | }
124 |
125 | func (c *CLI) AddDurationVar(name, defValue string, usage string, opts *FlagOptions) *time.Duration {
126 | if opts != nil {
127 | if opts.PreHook != nil {
128 | var err error
129 | defValue, err = opts.PreHook(defValue)
130 | if err != nil {
131 | panic("cli: add duration variable \"" + name + "\": " + err.Error())
132 | }
133 | }
134 | }
135 |
136 | valDuration, err := parseDuration(defValue)
137 | if err != nil {
138 | panic("cli: add duration variable \"" + name + "\": " + err.Error())
139 | }
140 |
141 | val := &valDuration
142 | c.addVar(name, val, defValue, usage, opts)
143 | return val
144 | }
145 |
146 | func writeVar(val string, to interface{}, preHook func(string) (string, error)) error {
147 | if preHook != nil {
148 | var err error
149 | val, err = preHook(val)
150 | if err != nil {
151 | return err
152 | }
153 | }
154 |
155 | switch to := to.(type) {
156 | case *string:
157 | *to = val
158 |
159 | case *int:
160 | val, err := strconv.Atoi(val)
161 | if err != nil {
162 | return err
163 | }
164 | *to = val
165 |
166 | case *bool:
167 | val := true
168 | *to = val
169 |
170 | case *uint:
171 | val, err := strconv.ParseUint(val, 10, 64)
172 | if err != nil {
173 | return err
174 | }
175 | *to = uint(val)
176 |
177 | case *time.Duration:
178 | val, err := parseDuration(val)
179 | if err != nil {
180 | return err
181 | }
182 | *to = val
183 |
184 | default:
185 | panic("cli: write variable: unknown \"to\" argument type")
186 | }
187 |
188 | return nil
189 | }
190 |
191 | func (c *CLI) printVersion() {
192 | fmt.Println(c.version)
193 | os.Exit(0)
194 | }
195 |
196 | func (c *CLI) printHelp() {
197 | // Search for the longest flag and required flags list.
198 | var maxFlagSize int
199 | var reqFlags string
200 |
201 | for _, v := range c.vars {
202 | flagSize := len(v.cliFlagName)
203 | if flagSize > maxFlagSize {
204 | maxFlagSize = flagSize
205 | }
206 |
207 | if v.required {
208 | reqFlags += "[" + v.cliFlagName + "] "
209 | }
210 | }
211 |
212 | // Print help
213 | fmt.Println("Usage:", os.Args[0], reqFlags+"[OPTION]...")
214 | fmt.Println("")
215 |
216 | for _, v := range c.vars {
217 | var spaces string
218 | for i := 0; i < maxFlagSize-len(v.cliFlagName)+2; i++ {
219 | spaces += " "
220 | }
221 |
222 | var defaultStr string
223 | if v.valueDefault != "" {
224 | defaultStr = " (default: " + v.valueDefault + ")"
225 | }
226 |
227 | fmt.Println(" ", v.cliFlagName, spaces, v.usage+defaultStr)
228 | }
229 |
230 | fmt.Println()
231 | fmt.Println(" -version Display version and exit.")
232 | fmt.Println(" -help Display this help and exit.")
233 |
234 | os.Exit(0)
235 | }
236 |
237 | func (c *CLI) Parse() {
238 | // The name of variables that were read from environment variables or CLI flags.
239 | // Used to check if "required" flags are present.
240 | readVars := make(map[string]struct{})
241 |
242 | // Read variables from CLI flags
243 | {
244 | alreadyRead := make(map[string]struct{})
245 |
246 | var varInProgress *variable
247 | for _, arg := range os.Args[1:] {
248 | if varInProgress == nil {
249 | switch arg {
250 | case "-version":
251 | c.printVersion()
252 |
253 | case "-help":
254 | c.printHelp()
255 | }
256 |
257 | _, exist := alreadyRead[arg]
258 | if exist {
259 | exitOnError("flag \"" + varInProgress.cliFlagName + "\" occurs twice")
260 | }
261 |
262 | ok := false
263 | for _, v := range c.vars {
264 | if v.cliFlagName == arg {
265 | switch v.value.(type) {
266 | case *bool:
267 | // pass
268 | default:
269 | varInProgress = &v
270 | }
271 |
272 | alreadyRead[v.cliFlagName] = struct{}{}
273 | readVars[v.name] = struct{}{}
274 |
275 | ok = true
276 | break
277 | }
278 | }
279 |
280 | if !ok {
281 | exitOnError("unknown flag \"" + arg + "\"")
282 | }
283 |
284 | } else {
285 | err := writeVar(arg, varInProgress.value, varInProgress.preHook)
286 | if err != nil {
287 | exitOnError("read \"" + varInProgress.cliFlagName + "\" flag: " + err.Error())
288 | }
289 |
290 | varInProgress = nil
291 | }
292 | }
293 |
294 | if varInProgress != nil {
295 | exitOnError("no value for \"" + varInProgress.cliFlagName + "\" flag")
296 | }
297 | }
298 |
299 | // Check required variables
300 | for _, v := range c.vars {
301 | if v.required {
302 | _, ok := readVars[v.name]
303 | if !ok {
304 | exitOnError("\"" + v.cliFlagName + "\" flag is missing")
305 | }
306 | }
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/internal/cli/duration.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package cli
20 |
21 | import (
22 | "errors"
23 | "strconv"
24 | "time"
25 | )
26 |
27 | func parseDuration(s string) (time.Duration, error) {
28 | var out int64
29 |
30 | var tmp string
31 | for _, c := range s {
32 | if c == ' ' {
33 | continue
34 | }
35 |
36 | if '0' <= c && c <= '9' {
37 | tmp += string(c)
38 | continue
39 | }
40 |
41 | val, err := strconv.ParseInt(tmp, 10, 64)
42 | if err != nil {
43 | return 0, errors.New("invalid format \"" + s + "\"")
44 | }
45 |
46 | switch c {
47 | case 'm':
48 | out += val * 60
49 | case 'h':
50 | out += val * 60 * 60
51 | case 'd':
52 | out += val * 60 * 60 * 24
53 | case 'w':
54 | out += val * 60 * 60 * 24 * 7
55 | default:
56 | return 0, errors.New("invalid format \"" + s + "\"")
57 | }
58 |
59 | tmp = ""
60 | }
61 |
62 | return time.Duration(out) * time.Second, nil
63 | }
64 |
--------------------------------------------------------------------------------
/internal/cli/duration_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package cli
20 |
21 | import (
22 | "testing"
23 | "time"
24 | )
25 |
26 | func TestParseDuration(t *testing.T) {
27 | testData := map[string]time.Duration{
28 | "10m": 60 * 10 * time.Second,
29 | "1h 1d": 60 * 60 * 25 * time.Second,
30 | "1h1d": 60 * 60 * 25 * time.Second,
31 | "1w": 60 * 60 * 24 * 7 * time.Second,
32 | "365d": 60 * 60 * 24 * 365 * time.Second,
33 | }
34 |
35 | for s, exp := range testData {
36 | res, err := parseDuration(s)
37 | if err != nil {
38 | t.Fatal(err)
39 | }
40 |
41 | if exp != res {
42 | t.Error("expected", exp, "but got", res, "(input:", s, ")")
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package config
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/logger"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | )
25 |
26 | const Software = "Lenpaste"
27 |
28 | type Config struct {
29 | Log logger.Logger
30 |
31 | RateLimitNew *netshare.RateLimitSystem
32 | RateLimitGet *netshare.RateLimitSystem
33 |
34 | Version string
35 |
36 | TitleMaxLen int
37 | BodyMaxLen int
38 | MaxLifeTime int64
39 |
40 | ServerAbout string
41 | ServerRules string
42 | ServerTermsOfUse string
43 |
44 | AdminName string
45 | AdminMail string
46 |
47 | RobotsDisallow bool
48 |
49 | LenPasswdFile string
50 |
51 | UiDefaultLifetime string
52 | UiDefaultTheme string
53 | UiThemesDir string
54 | }
55 |
--------------------------------------------------------------------------------
/internal/lenpasswd/lenpasswd.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package lenpasswd
20 |
21 | import (
22 | "bytes"
23 | "errors"
24 | "io/ioutil"
25 | "os"
26 | "strconv"
27 | "strings"
28 | )
29 |
30 | type Data map[string]string
31 |
32 | func LoadFile(path string) (Data, error) {
33 | // Open file
34 | file, err := os.Open(path)
35 | if err != nil {
36 | return nil, errors.New("lenpasswd: " + err.Error())
37 | }
38 | defer file.Close()
39 |
40 | // Read file
41 | fileByte, err := ioutil.ReadAll(file)
42 | if err != nil {
43 | return nil, errors.New("lenpasswd: " + err.Error())
44 | }
45 |
46 | // Convert []byte to string
47 | fileTxt := bytes.NewBuffer(fileByte).String()
48 |
49 | // Parse file
50 | data := make(Data)
51 | for i, line := range strings.Split(fileTxt, "\n") {
52 | if line == "" {
53 | continue
54 | }
55 |
56 | lineSplit := strings.Split(line, ":")
57 | if len(lineSplit) != 2 {
58 | return nil, errors.New("lenpasswd: error in line " + strconv.Itoa(i))
59 | }
60 |
61 | user := lineSplit[0]
62 | pass := lineSplit[1]
63 |
64 | _, exist := data[user]
65 | if exist == true {
66 | return nil, errors.New("lenpasswd: overriding user " + user + " in line " + strconv.Itoa(i))
67 | }
68 |
69 | data[user] = pass
70 | }
71 |
72 | return data, nil
73 | }
74 |
75 | func (data Data) Check(user string, pass string) bool {
76 | truePass, exist := data[user]
77 | if exist == false {
78 | return false
79 | }
80 |
81 | if pass != truePass {
82 | return false
83 | }
84 |
85 | return true
86 | }
87 |
88 | func LoadAndCheck(path string, user string, pass string) (bool, error) {
89 | data, err := LoadFile(path)
90 | if err != nil {
91 | return false, err
92 | }
93 |
94 | return data.Check(user, pass), nil
95 | }
96 |
--------------------------------------------------------------------------------
/internal/lineend/lineend.go:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Leonid Maslakov. All rights reserved.
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package lineend
6 |
7 | import (
8 | "strings"
9 | )
10 |
11 | // GetLineEnd allows you to get the end of a line used in the text.
12 | // It can return \r\n, \r, \n or an empty string.
13 | func GetLineEnd(text string) string {
14 | dos := strings.Count(text, "\r\n")
15 | oldMac := strings.Count(text, "\r")
16 | unix := strings.Count(text, "\n")
17 |
18 | if dos == 0 && oldMac == 0 && unix == 0 {
19 | return ""
20 | }
21 |
22 | if dos >= oldMac && dos >= unix {
23 | return "\r\n"
24 | }
25 |
26 | if oldMac >= dos && oldMac >= unix {
27 | return "\r"
28 | }
29 |
30 | if unix >= dos && unix >= oldMac {
31 | return "\n"
32 | }
33 |
34 | return ""
35 | }
36 |
37 | // DosToOldMac DosToOldMac сonverts end of line from CRLF to CR.
38 | func DosToOldMac(text string) string {
39 | return strings.Replace(text, "\n", "", -1)
40 | }
41 |
42 | // DosToUnix сonverts end of line from CRLF to LF.
43 | func DosToUnix(text string) string {
44 | return strings.Replace(text, "\r", "", -1)
45 | }
46 |
47 | // OldMacToDos сonverts end of line from CR to CRLF.
48 | func OldMacToDos(text string) string {
49 | return strings.Replace(text, "\r", "\r\n", -1)
50 | }
51 |
52 | // OldMacToUnix сonverts end of line from CR to LF.
53 | func OldMacToUnix(text string) string {
54 | return strings.Replace(text, "\r", "\n", -1)
55 | }
56 |
57 | // UnixToDos сonverts end of line from LF to CRLF.
58 | func UnixToDos(text string) string {
59 | return strings.Replace(text, "\n", "\r\n", -1)
60 | }
61 |
62 | // UnixToOldMac сonverts end of line from LF to CR.
63 | func UnixToOldMac(text string) string {
64 | return strings.Replace(text, "\n", "\r", -1)
65 | }
66 |
67 | // UnknownToDos converts unknown line end to CRLF.
68 | func UnknownToDos(text string) string {
69 | switch GetLineEnd(text) {
70 | case "\r":
71 | return OldMacToDos(text)
72 |
73 | case "\n":
74 | return UnixToDos(text)
75 | }
76 |
77 | return text
78 | }
79 |
80 | // UnknownToOldMac converts unknown line end to CR.
81 | func UnknownToOldMac(text string) string {
82 | switch GetLineEnd(text) {
83 | case "\r\n":
84 | return DosToOldMac(text)
85 |
86 | case "\n":
87 | return UnixToOldMac(text)
88 | }
89 |
90 | return text
91 | }
92 |
93 | // UnknownToUnix converts unknown line end to LF.
94 | func UnknownToUnix(text string) string {
95 | switch GetLineEnd(text) {
96 | case "\r\n":
97 | return DosToUnix(text)
98 |
99 | case "\r":
100 | return OldMacToUnix(text)
101 | }
102 |
103 | return text
104 | }
105 |
--------------------------------------------------------------------------------
/internal/lineend/lineend_test.go:
--------------------------------------------------------------------------------
1 | package lineend
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | type testDataType struct {
8 | Input string
9 | Expect string
10 | }
11 |
12 | func TestGetLineEnd(t *testing.T) {
13 | testData := []testDataType{
14 | {
15 | Input: "",
16 | Expect: "",
17 | },
18 | {
19 | Input: "my line",
20 | Expect: "",
21 | },
22 | {
23 | Input: "my\r\nline\r\n",
24 | Expect: "\r\n",
25 | },
26 | {
27 | Input: "my\rline\r",
28 | Expect: "\r",
29 | },
30 | {
31 | Input: "my\nline\n",
32 | Expect: "\n",
33 | },
34 | }
35 |
36 | for i, test := range testData {
37 | if GetLineEnd(test.Input) != test.Expect {
38 | t.Fatal("Number of failed test:", i)
39 | }
40 | }
41 | }
42 |
43 | func TestUnknownToDos(t *testing.T) {
44 | testData := []testDataType{
45 | {
46 | Input: "",
47 | Expect: "",
48 | },
49 | {
50 | Input: "my line",
51 | Expect: "my line",
52 | },
53 | {
54 | Input: "my\r\nline\r\n",
55 | Expect: "my\r\nline\r\n",
56 | },
57 | {
58 | Input: "my\rline\r",
59 | Expect: "my\r\nline\r\n",
60 | },
61 | {
62 | Input: "my\nline\n",
63 | Expect: "my\r\nline\r\n",
64 | },
65 | }
66 |
67 | for i, test := range testData {
68 | if UnknownToDos(test.Input) != test.Expect {
69 | t.Fatal("Number of failed test:", i)
70 | }
71 | }
72 | }
73 |
74 | func TestUnknownToOldMac(t *testing.T) {
75 | testData := []testDataType{
76 | {
77 | Input: "",
78 | Expect: "",
79 | },
80 | {
81 | Input: "my line",
82 | Expect: "my line",
83 | },
84 | {
85 | Input: "my\r\nline\r\n",
86 | Expect: "my\rline\r",
87 | },
88 | {
89 | Input: "my\rline\r",
90 | Expect: "my\rline\r",
91 | },
92 | {
93 | Input: "my\nline\n",
94 | Expect: "my\rline\r",
95 | },
96 | }
97 |
98 | for i, test := range testData {
99 | if UnknownToOldMac(test.Input) != test.Expect {
100 | t.Fatal("Number of failed test:", i)
101 | }
102 | }
103 | }
104 |
105 | func TestUnknownToUnix(t *testing.T) {
106 | testData := []testDataType{
107 | {
108 | Input: "",
109 | Expect: "",
110 | },
111 | {
112 | Input: "my line",
113 | Expect: "my line",
114 | },
115 | {
116 | Input: "my\r\nline\r\n",
117 | Expect: "my\nline\n",
118 | },
119 | {
120 | Input: "my\rline\r",
121 | Expect: "my\nline\n",
122 | },
123 | {
124 | Input: "my\nline\n",
125 | Expect: "my\nline\n",
126 | },
127 | }
128 |
129 | for i, test := range testData {
130 | if UnknownToUnix(test.Input) != test.Expect {
131 | t.Fatal("Number of failed test:", i)
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/internal/logger/logger.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package logger
20 |
21 | import (
22 | "fmt"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | "net/http"
25 | "os"
26 | "runtime"
27 | "strconv"
28 | "time"
29 | )
30 |
31 | type Logger struct {
32 | TimeFormat string
33 | }
34 |
35 | func New(timeFormat string) Logger {
36 | return Logger{
37 | TimeFormat: timeFormat,
38 | }
39 | }
40 |
41 | func getTrace() string {
42 | trace := ""
43 |
44 | for i := 2; ; i++ {
45 | _, file, line, ok := runtime.Caller(i)
46 | if ok {
47 | trace = trace + file + "#" + strconv.Itoa(line) + ": "
48 |
49 | } else {
50 | return trace
51 | }
52 | }
53 | }
54 |
55 | func (cfg Logger) Info(msg string) {
56 | fmt.Fprintln(os.Stdout, time.Now().Format(cfg.TimeFormat), "[INFO] ", msg)
57 | }
58 |
59 | func (cfg Logger) Error(e error) {
60 | fmt.Fprintln(os.Stderr, time.Now().Format(cfg.TimeFormat), "[ERROR] ", getTrace(), e.Error())
61 | }
62 |
63 | func (cfg Logger) HttpRequest(req *http.Request, code int) {
64 | fmt.Fprintln(os.Stdout, time.Now().Format(cfg.TimeFormat), "[REQUEST]", netshare.GetClientAddr(req).String(), req.Method, code, req.URL.Path, "(User-Agent: "+req.UserAgent()+")")
65 | }
66 |
67 | func (cfg Logger) HttpError(req *http.Request, e error) {
68 | fmt.Fprintln(os.Stderr, time.Now().Format(cfg.TimeFormat), "[ERROR] ", netshare.GetClientAddr(req).String(), req.Method, 500, req.URL.Path, "(User-Agent: "+req.UserAgent()+")", "Error:", getTrace(), e.Error())
69 | }
70 |
--------------------------------------------------------------------------------
/internal/netshare/netshare.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package netshare
20 |
21 | import (
22 | "errors"
23 | )
24 |
25 | const (
26 | MaxLengthAuthorAll = 100 // Max length or paste author name, email and URL.
27 | )
28 |
29 | var (
30 | ErrBadRequest = errors.New("Bad Request") // 400
31 | ErrUnauthorized = errors.New("Unauthorized") // 401
32 | ErrNotFound = errors.New("Not Found") // 404
33 | ErrMethodNotAllowed = errors.New("Method Not Allowed") // 405
34 | ErrPayloadTooLarge = errors.New("Payload Too Large") // 413
35 | // ErrTooManyRequests = errors.New("Too Many Requests") // 429
36 | ErrInternal = errors.New("Internal Server Error") // 500
37 | )
38 |
39 | type ErrTooManyRequests struct {
40 | s string
41 | RetryAfter int64
42 | }
43 |
44 | func (e *ErrTooManyRequests) Error() string {
45 | return e.s
46 | }
47 |
48 | func ErrTooManyRequestsNew(retryAfter int64) *ErrTooManyRequests {
49 | return &ErrTooManyRequests{
50 | s: "Too Many Requests",
51 | RetryAfter: retryAfter,
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/netshare/netshare_host.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package netshare
20 |
21 | import (
22 | "net"
23 | "net/http"
24 | "strings"
25 | )
26 |
27 | func GetHost(req *http.Request) string {
28 | // Read header
29 | xHost := req.Header.Get("X-Forwarded-Host")
30 |
31 | // Check
32 | if xHost != "" {
33 | return xHost
34 | }
35 |
36 | return req.Host
37 | }
38 |
39 | func GetProtocol(req *http.Request) string {
40 | // X-Forwarded-Proto
41 | xProto := req.Header.Get("X-Forwarded-Proto")
42 |
43 | if xProto != "" {
44 | return xProto
45 | }
46 |
47 | // Else real protocol
48 | return req.URL.Scheme
49 | }
50 |
51 | func GetClientAddr(req *http.Request) net.IP {
52 | // X-Real-IP
53 | xReal := req.Header.Get("X-Real-IP")
54 | if xReal != "" {
55 | return net.ParseIP(xReal)
56 | }
57 |
58 | // X-Forwarded-For
59 | xFor := req.Header.Get("X-Forwarded-For")
60 | xFor = strings.Split(xFor, ",")[0]
61 |
62 | if xFor != "" {
63 | return net.ParseIP(xFor)
64 | }
65 |
66 | // Else use real client address
67 | host, _, err := net.SplitHostPort(req.RemoteAddr)
68 | if err != nil {
69 | return nil
70 | }
71 |
72 | return net.ParseIP(host)
73 | }
74 |
--------------------------------------------------------------------------------
/internal/netshare/netshare_paste.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package netshare
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/lineend"
23 | "github.com/lcomrade/lenpaste/internal/storage"
24 | "net/http"
25 | "strconv"
26 | "strings"
27 | "time"
28 | "unicode/utf8"
29 | )
30 |
31 | func PasteAddFromForm(req *http.Request, db storage.DB, rateSys *RateLimitSystem, titleMaxLen int, bodyMaxLen int, maxLifeTime int64, lexerNames []string) (string, int64, int64, error) {
32 | // Check HTTP method
33 | if req.Method != "POST" {
34 | return "", 0, 0, ErrMethodNotAllowed
35 | }
36 |
37 | // Check rate limit
38 | err := rateSys.CheckAndUse(GetClientAddr(req))
39 | if err != nil {
40 | return "", 0, 0, err
41 | }
42 |
43 | // Read form
44 | req.ParseForm()
45 |
46 | paste := storage.Paste{
47 | Title: req.PostForm.Get("title"),
48 | Body: req.PostForm.Get("body"),
49 | Syntax: req.PostForm.Get("syntax"),
50 | DeleteTime: 0,
51 | OneUse: false,
52 | Author: req.PostForm.Get("author"),
53 | AuthorEmail: req.PostForm.Get("authorEmail"),
54 | AuthorURL: req.PostForm.Get("authorURL"),
55 | }
56 |
57 | // Remove new line from title
58 | paste.Title = strings.Replace(paste.Title, "\n", "", -1)
59 | paste.Title = strings.Replace(paste.Title, "\r", "", -1)
60 | paste.Title = strings.Replace(paste.Title, "\t", " ", -1)
61 |
62 | // Check title
63 | if utf8.RuneCountInString(paste.Title) > titleMaxLen && titleMaxLen >= 0 {
64 | return "", 0, 0, ErrPayloadTooLarge
65 | }
66 |
67 | // Check paste body
68 | if paste.Body == "" {
69 | return "", 0, 0, ErrBadRequest
70 | }
71 |
72 | if utf8.RuneCountInString(paste.Body) > bodyMaxLen && bodyMaxLen > 0 {
73 | return "", 0, 0, ErrPayloadTooLarge
74 | }
75 |
76 | // Change paste body lines end
77 | switch req.PostForm.Get("lineEnd") {
78 | case "", "LF", "lf":
79 | paste.Body = lineend.UnknownToUnix(paste.Body)
80 |
81 | case "CRLF", "crlf":
82 | paste.Body = lineend.UnknownToDos(paste.Body)
83 |
84 | case "CR", "cr":
85 | paste.Body = lineend.UnknownToOldMac(paste.Body)
86 |
87 | default:
88 | return "", 0, 0, ErrBadRequest
89 | }
90 |
91 | // Check syntax
92 | if paste.Syntax == "" {
93 | paste.Syntax = "plaintext"
94 | }
95 |
96 | syntaxOk := false
97 | for _, name := range lexerNames {
98 | if name == paste.Syntax {
99 | syntaxOk = true
100 | break
101 | }
102 | }
103 |
104 | if syntaxOk == false {
105 | return "", 0, 0, ErrBadRequest
106 | }
107 |
108 | // Get delete time
109 | expirStr := req.PostForm.Get("expiration")
110 | if expirStr != "" {
111 | // Convert string to int
112 | expir, err := strconv.ParseInt(expirStr, 10, 64)
113 | if err != nil {
114 | return "", 0, 0, ErrBadRequest
115 | }
116 |
117 | // Check limits
118 | if maxLifeTime > 0 {
119 | if expir > maxLifeTime || expir <= 0 {
120 | return "", 0, 0, ErrBadRequest
121 | }
122 | }
123 |
124 | // Save if ok
125 | if expir > 0 {
126 | paste.DeleteTime = time.Now().Unix() + expir
127 | }
128 | }
129 |
130 | // Get "one use" parameter
131 | if req.PostForm.Get("oneUse") == "true" {
132 | paste.OneUse = true
133 | }
134 |
135 | // Check author name, email and URL length.
136 | if utf8.RuneCountInString(paste.Author) > MaxLengthAuthorAll {
137 | return "", 0, 0, ErrPayloadTooLarge
138 | }
139 |
140 | if utf8.RuneCountInString(paste.AuthorEmail) > MaxLengthAuthorAll {
141 | return "", 0, 0, ErrPayloadTooLarge
142 | }
143 |
144 | if utf8.RuneCountInString(paste.AuthorURL) > MaxLengthAuthorAll {
145 | return "", 0, 0, ErrPayloadTooLarge
146 | }
147 |
148 | // Create paste
149 | pasteID, createTime, deleteTime, err := db.PasteAdd(paste)
150 | if err != nil {
151 | return pasteID, createTime, deleteTime, err
152 | }
153 |
154 | return pasteID, createTime, deleteTime, nil
155 | }
156 |
--------------------------------------------------------------------------------
/internal/netshare/netshare_ratelimit.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package netshare
20 |
21 | import (
22 | "net"
23 | "sync"
24 | "time"
25 | )
26 |
27 | type RateLimitSystem struct {
28 | per5Min *RateLimit
29 | per15Min *RateLimit
30 | per1Hour *RateLimit
31 | }
32 |
33 | func NewRateLimitSystem(per5Min, per15Min, per1Hour uint) *RateLimitSystem {
34 | return &RateLimitSystem{
35 | per5Min: NewRateLimit(5*60, per5Min),
36 | per15Min: NewRateLimit(15*60, per15Min),
37 | per1Hour: NewRateLimit(60*60, per1Hour),
38 | }
39 | }
40 |
41 | func (rateSys *RateLimitSystem) CheckAndUse(ip net.IP) error {
42 | var tmp int64
43 |
44 | tmp = rateSys.per5Min.CheckAndUse(ip)
45 | if tmp != 0 {
46 | return ErrTooManyRequestsNew(tmp)
47 | }
48 |
49 | tmp = rateSys.per15Min.CheckAndUse(ip)
50 | if tmp != 0 {
51 | return ErrTooManyRequestsNew(tmp)
52 | }
53 |
54 | tmp = rateSys.per1Hour.CheckAndUse(ip)
55 | if tmp != 0 {
56 | return ErrTooManyRequestsNew(tmp)
57 | }
58 |
59 | return nil
60 | }
61 |
62 | type RateLimit struct {
63 | sync.RWMutex
64 |
65 | limitPeriod int // N - Rate limit period (in seconds)
66 | limitCount uint // X - Max request count per N seconds period
67 |
68 | list map[string]rateLimitIP // Rate limit bucket
69 | }
70 |
71 | type rateLimitIP struct {
72 | UseTime int64 // Fist IP use time
73 | UseCount uint // Requests count by IP
74 | }
75 |
76 | func NewRateLimit(rateLimitPeriod int, limitCount uint) *RateLimit {
77 | rateLimit := &RateLimit{
78 | limitPeriod: rateLimitPeriod,
79 | limitCount: limitCount,
80 | list: make(map[string]rateLimitIP),
81 | }
82 |
83 | go rateLimit.runWorker()
84 |
85 | return rateLimit
86 | }
87 |
88 | func (rateLimit *RateLimit) runWorker() {
89 | for {
90 | time.Sleep(time.Duration(rateLimit.limitPeriod) * time.Second)
91 |
92 | timeNow := time.Now().Unix()
93 | rateLimit.Lock()
94 |
95 | for ipStr, data := range rateLimit.list {
96 | if data.UseTime+int64(rateLimit.limitPeriod) <= timeNow {
97 | delete(rateLimit.list, ipStr)
98 | }
99 | }
100 |
101 | rateLimit.Unlock()
102 | }
103 | }
104 |
105 | func (rateLimit *RateLimit) CheckAndUse(ip net.IP) int64 {
106 | // If rate limit not need
107 | if rateLimit.limitCount == 0 {
108 | return 0
109 | }
110 |
111 | // Lock
112 | rateLimit.Lock()
113 | defer rateLimit.Unlock()
114 |
115 | ipStr := ip.String()
116 | timeNow := time.Now().Unix()
117 |
118 | // If last use time out
119 | if rateLimit.list[ipStr].UseTime+int64(rateLimit.limitPeriod) <= timeNow {
120 | rateLimit.list[ipStr] = rateLimitIP{
121 | UseTime: timeNow,
122 | UseCount: 1,
123 | }
124 |
125 | return 0
126 |
127 | // Else
128 | } else {
129 | if rateLimit.list[ipStr].UseCount < rateLimit.limitCount {
130 | tmp := rateLimit.list[ipStr]
131 | tmp.UseCount = tmp.UseCount + 1
132 | rateLimit.list[ipStr] = tmp
133 | return 0
134 | }
135 | }
136 |
137 | return rateLimit.list[ipStr].UseTime + int64(rateLimit.limitPeriod) - timeNow
138 | }
139 |
--------------------------------------------------------------------------------
/internal/raw/raw.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package raw
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/config"
23 | "github.com/lcomrade/lenpaste/internal/logger"
24 | "github.com/lcomrade/lenpaste/internal/netshare"
25 | "github.com/lcomrade/lenpaste/internal/storage"
26 | "net/http"
27 | )
28 |
29 | type Data struct {
30 | DB storage.DB
31 | Log logger.Logger
32 |
33 | RateLimitGet *netshare.RateLimitSystem
34 |
35 | Version string
36 | }
37 |
38 | func Load(db storage.DB, cfg config.Config) *Data {
39 | return &Data{
40 | DB: db,
41 | Log: cfg.Log,
42 | RateLimitGet: cfg.RateLimitGet,
43 | Version: cfg.Version,
44 | }
45 | }
46 |
47 | func (data *Data) Hand(rw http.ResponseWriter, req *http.Request) {
48 | rw.Header().Set("Server", config.Software+"/"+data.Version)
49 |
50 | err := data.rawHand(rw, req)
51 |
52 | if err == nil {
53 | data.Log.HttpRequest(req, 200)
54 |
55 | } else {
56 | code, err := data.writeError(rw, req, err)
57 | if err != nil {
58 | data.Log.HttpError(req, err)
59 | } else {
60 | data.Log.HttpRequest(req, code)
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/internal/raw/raw_error.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package raw
20 |
21 | import (
22 | "errors"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | "github.com/lcomrade/lenpaste/internal/storage"
25 | "io"
26 | "net/http"
27 | "strconv"
28 | )
29 |
30 | func (data *Data) writeError(rw http.ResponseWriter, req *http.Request, e error) (int, error) {
31 | var errText string
32 | var errCode int
33 |
34 | // Dectect error
35 | var eTmp429 *netshare.ErrTooManyRequests
36 |
37 | if e == storage.ErrNotFoundID && e == netshare.ErrNotFound {
38 | errCode = 404
39 | errText = "404 Not Found"
40 |
41 | } else if errors.As(e, &eTmp429) {
42 | errCode = 429
43 | errText = "429 Too Many Requests"
44 | rw.Header().Set("Retry-After", strconv.FormatInt(eTmp429.RetryAfter, 10))
45 |
46 | } else {
47 | errCode = 500
48 | errText = "500 Internal Server Error"
49 | }
50 |
51 | // Write response
52 | rw.Header().Set("Content-type", "text/plain; charset=utf-8")
53 | rw.WriteHeader(errCode)
54 |
55 | _, err := io.WriteString(rw, errText)
56 | if err != nil {
57 | return 500, err
58 | }
59 |
60 | return errCode, nil
61 | }
62 |
--------------------------------------------------------------------------------
/internal/raw/raw_raw.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package raw
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/netshare"
23 | "io"
24 | "net/http"
25 | )
26 |
27 | // Pattern: /raw/
28 | func (data *Data) rawHand(rw http.ResponseWriter, req *http.Request) error {
29 | // Check rate limit
30 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req))
31 | if err != nil {
32 | return err
33 | }
34 |
35 | // Read DB
36 | pasteID := string([]rune(req.URL.Path)[5:])
37 |
38 | paste, err := data.DB.PasteGet(pasteID)
39 | if err != nil {
40 | return err
41 | }
42 |
43 | // If "one use" paste
44 | if paste.OneUse == true {
45 | // Delete paste
46 | err = data.DB.PasteDelete(pasteID)
47 | if err != nil {
48 | return err
49 | }
50 | }
51 |
52 | // Write result
53 | rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
54 |
55 | _, err = io.WriteString(rw, paste.Body)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/internal/storage/share.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package storage
20 |
21 | import (
22 | "crypto/rand"
23 | "math/big"
24 | )
25 |
26 | func genTokenCrypto(tokenLen int) (string, error) {
27 | // Generate token
28 | var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
29 | charsLen := int64(len(chars))
30 | charsLenBig := big.NewInt(charsLen)
31 |
32 | token := ""
33 |
34 | for i := 0; i < tokenLen; i++ {
35 | randInt, err := rand.Int(rand.Reader, charsLenBig)
36 | if err != nil {
37 | return "", err
38 | }
39 |
40 | token = token + string(chars[randInt.Int64()])
41 | }
42 |
43 | return token, nil
44 | }
45 |
--------------------------------------------------------------------------------
/internal/storage/storage.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package storage
20 |
21 | import (
22 | "database/sql"
23 | "errors"
24 | _ "github.com/lib/pq"
25 | _ "github.com/mattn/go-sqlite3"
26 | )
27 |
28 | var (
29 | ErrNotFoundID = errors.New("db: could not find ID")
30 | )
31 |
32 | type DB struct {
33 | pool *sql.DB
34 | }
35 |
36 | func NewPool(driverName string, dataSourceName string, maxOpenConns int, maxIdleConns int) (DB, error) {
37 | var db DB
38 | var err error
39 |
40 | db.pool, err = sql.Open(driverName, dataSourceName)
41 | if err != nil {
42 | return db, err
43 | }
44 |
45 | db.pool.SetMaxOpenConns(maxOpenConns)
46 | db.pool.SetMaxIdleConns(maxIdleConns)
47 |
48 | return db, nil
49 | }
50 |
51 | func (db DB) Close() error {
52 | return db.pool.Close()
53 | }
54 |
55 | func InitDB(driverName string, dataSourceName string) error {
56 | // Open DB
57 | db, err := NewPool(driverName, dataSourceName, 1, 0)
58 | if err != nil {
59 | return err
60 | }
61 | defer db.Close()
62 |
63 | // Create tables
64 | _, err = db.pool.Exec(`
65 | CREATE TABLE IF NOT EXISTS pastes (
66 | id TEXT PRIMARY KEY,
67 | title TEXT NOT NULL,
68 | body TEXT NOT NULL,
69 | syntax TEXT NOT NULL,
70 | create_time INTEGER NOT NULL,
71 | delete_time INTEGER NOT NULL,
72 | one_use BOOL NOT NULL
73 | );
74 | `)
75 | if err != nil {
76 | return err
77 | }
78 |
79 | // Crutch for SQLite3
80 | if driverName == "sqlite3" {
81 | _, err = db.pool.Exec(`ALTER TABLE pastes ADD COLUMN author TEXT NOT NULL DEFAULT ''`)
82 | if err != nil {
83 | if err.Error() != "duplicate column name: author" {
84 | return err
85 | }
86 | }
87 |
88 | _, err = db.pool.Exec(`ALTER TABLE pastes ADD COLUMN author_email TEXT NOT NULL DEFAULT ''`)
89 | if err != nil {
90 | if err.Error() != "duplicate column name: author_email" {
91 | return err
92 | }
93 | }
94 |
95 | _, err = db.pool.Exec(`ALTER TABLE pastes ADD COLUMN author_url TEXT NOT NULL DEFAULT ''`)
96 | if err != nil {
97 | if err.Error() != "duplicate column name: author_url" {
98 | return err
99 | }
100 | }
101 |
102 | // Normal SQL for all other DBs
103 | } else {
104 | _, err = db.pool.Exec(`
105 | ALTER TABLE pastes ADD COLUMN IF NOT EXISTS author TEXT NOT NULL DEFAULT '';
106 | ALTER TABLE pastes ADD COLUMN IF NOT EXISTS author_email TEXT NOT NULL DEFAULT '';
107 | ALTER TABLE pastes ADD COLUMN IF NOT EXISTS author_url TEXT NOT NULL DEFAULT '';
108 | `)
109 | if err != nil {
110 | return err
111 | }
112 | }
113 |
114 | return nil
115 | }
116 |
--------------------------------------------------------------------------------
/internal/storage/storage_paste.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package storage
20 |
21 | import (
22 | "database/sql"
23 | "time"
24 | )
25 |
26 | type Paste struct {
27 | ID string `json:"id"` // Ignored when creating
28 | Title string `json:"title"`
29 | Body string `json:"body"`
30 | CreateTime int64 `json:"createTime"` // Ignored when creating
31 | DeleteTime int64 `json:"deleteTime"`
32 | OneUse bool `json:"oneUse"`
33 | Syntax string `json:"syntax"`
34 |
35 | Author string `json:"author"`
36 | AuthorEmail string `json:"authorEmail"`
37 | AuthorURL string `json:"authorURL"`
38 | }
39 |
40 | func (db DB) PasteAdd(paste Paste) (string, int64, int64, error) {
41 | var err error
42 |
43 | // Generate ID
44 | paste.ID, err = genTokenCrypto(8)
45 | if err != nil {
46 | return paste.ID, paste.CreateTime, paste.DeleteTime, err
47 | }
48 |
49 | // Set paste create time
50 | paste.CreateTime = time.Now().Unix()
51 |
52 | // Check delete time
53 | if paste.DeleteTime < 0 {
54 | paste.DeleteTime = 0
55 | }
56 |
57 | // Add
58 | _, err = db.pool.Exec(
59 | `INSERT INTO pastes (id, title, body, syntax, create_time, delete_time, one_use, author, author_email, author_url) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
60 | paste.ID, paste.Title, paste.Body, paste.Syntax, paste.CreateTime, paste.DeleteTime, paste.OneUse, paste.Author, paste.AuthorEmail, paste.AuthorURL,
61 | )
62 | if err != nil {
63 | return paste.ID, paste.CreateTime, paste.DeleteTime, err
64 | }
65 |
66 | return paste.ID, paste.CreateTime, paste.DeleteTime, nil
67 | }
68 |
69 | func (db DB) PasteDelete(id string) error {
70 | // Delete
71 | result, err := db.pool.Exec(
72 | `DELETE FROM pastes WHERE id = $1`,
73 | id,
74 | )
75 | if err != nil {
76 | return err
77 | }
78 |
79 | // Check result
80 | rowsAffected, err := result.RowsAffected()
81 | if err != nil {
82 | return err
83 | }
84 |
85 | if rowsAffected == 0 {
86 | return ErrNotFoundID
87 | }
88 |
89 | return nil
90 | }
91 |
92 | func (db DB) PasteGet(id string) (Paste, error) {
93 | var paste Paste
94 |
95 | // Make query
96 | row := db.pool.QueryRow(
97 | `SELECT id, title, body, syntax, create_time, delete_time, one_use, author, author_email, author_url FROM pastes WHERE id = $1`,
98 | id,
99 | )
100 |
101 | // Read query
102 | err := row.Scan(&paste.ID, &paste.Title, &paste.Body, &paste.Syntax, &paste.CreateTime, &paste.DeleteTime, &paste.OneUse, &paste.Author, &paste.AuthorEmail, &paste.AuthorURL)
103 | if err != nil {
104 | if err == sql.ErrNoRows {
105 | return paste, ErrNotFoundID
106 | }
107 |
108 | return paste, err
109 | }
110 |
111 | // Check paste expiration
112 | if paste.DeleteTime < time.Now().Unix() && paste.DeleteTime > 0 {
113 | // Delete expired paste
114 | _, err = db.pool.Exec(
115 | `DELETE FROM pastes WHERE id = $1`,
116 | paste.ID,
117 | )
118 | if err != nil {
119 | return Paste{}, err
120 | }
121 |
122 | // Return ErrNotFound
123 | return Paste{}, ErrNotFoundID
124 | }
125 |
126 | return paste, nil
127 | }
128 |
129 | func (db DB) PasteDeleteExpired() (int64, error) {
130 | // Delete
131 | result, err := db.pool.Exec(
132 | `DELETE FROM pastes WHERE (delete_time < $1) AND (delete_time > 0)`,
133 | time.Now().Unix(),
134 | )
135 | if err != nil {
136 | return 0, err
137 | }
138 |
139 | // Check result
140 | rowsAffected, err := result.RowsAffected()
141 | if err != nil {
142 | return rowsAffected, err
143 | }
144 |
145 | return rowsAffected, nil
146 | }
147 |
--------------------------------------------------------------------------------
/internal/web/data/about.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{ call .Translate `about.Title` }} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 | {{if ne .ServerAbout ``}}
25 |
{{ call .Translate `about.AboutServerTitle` }}
26 | {{ call .Highlight .ServerAbout `plaintext` }}
27 | {{end}}
28 |
29 | {{if ne .ServerRules ``}}
30 | {{ call .Translate `about.RulesTitle` }}
31 | {{ call .Highlight .ServerRules `plaintext` }}
32 | {{if .ServerTermsExist}}{{ call .Translate `about.SeeTerms` `/terms` }}
{{end}}
33 | {{end}}
34 |
35 | {{ call .Translate `about.Limit` }}
36 | {{if ne .TitleMaxLen 0}}
37 | {{if gt .TitleMaxLen 0}}
38 | {{call .Translate `about.LimitTitle` .TitleMaxLen}}
39 | {{else}}
40 |
{{ call .Translate `about.LimitTitleNo` }}
41 | {{end}}
42 | {{else}}
43 |
{{ call .Translate `about.LimitTitleDisable` }}
44 | {{end}}
45 | {{if gt .BodyMaxLen 0}}
46 | {{call .Translate `about.LimitBody` .BodyMaxLen }}
47 | {{else}}
48 | {{ call .Translate `about.LimitBodyNo` }}
49 | {{end}}
50 | {{if gt .MaxLifeTime 0}}
51 | {{call .Translate `about.LimitLifeTime` .MaxLifeTime }}
52 | {{else}}
53 | {{ call .Translate `about.LimitLifeTimeNo` }}
54 | {{end}}
55 |
56 | {{if or (ne .AdminName ``) (ne .AdminMail ``)}}
57 | {{ call .Translate `about.AdminTitle` }}
58 | {{if ne .AdminName ``}}{{ call .Translate `about.AdminName` }} {{.AdminName}}
{{end}}
59 | {{if ne .AdminMail ``}}{{ call .Translate `about.AdminEmail` }} {{.AdminMail}}
{{end}}
60 | {{end}}
61 |
62 | {{ call .Translate `about.LenpasteTitle` }}
63 | {{call .Translate `about.LenpasteMessage` .Version}}
64 |
65 | {{ call .Translate `about.Lenpaste1` `/about/source_code` `/about/license` `AGPL 3` }}
66 | {{ call .Translate `about.Lenpaste2` }}
67 | {{ call .Translate `about.Lenpaste3` }}
68 | {{ call .Translate `about.Lenpaste4` }}
69 | {{ call .Translate `about.Lenpaste5` `/docs/apiv1` }}
70 |
71 | {{call .Translate `about.LenpasteAuthors` `/about/authors`}}
72 | {{end}}
73 |
--------------------------------------------------------------------------------
/internal/web/data/authors.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{ call .Translate `authors.Title` }} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 |
25 |
30 | {{end}}
31 |
--------------------------------------------------------------------------------
/internal/web/data/base.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 |
22 |
23 |
24 |
25 | {{template "titlePrefix" .}}{{ call .Translate `base.Lenpaste` }}
26 |
27 |
28 |
29 | {{template "headAppend" .}}
30 |
31 |
32 |
33 |
37 | {{template "article" .}}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/internal/web/data/code.js:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | function copyToClipboard(text) {
20 | let tmp = document.createElement("textarea");
21 | let focus = document.activeElement;
22 |
23 | tmp.value = text;
24 |
25 | document.body.appendChild(tmp);
26 | tmp.select();
27 | document.execCommand("copy");
28 | document.body.removeChild(tmp);
29 | focus.focus();
30 | }
31 |
32 | function copyButton(element) {
33 | let result = "";
34 |
35 | let strings = element.parentNode.getElementsByTagName("code")[0].textContent.split("\n");
36 | let stringsLen = strings.length;
37 | let cutLen = stringsLen.toString().length;
38 | for (let i = 0; stringsLen > i; i++) {
39 | if (i != 0) {
40 | result = result + "\n"
41 | }
42 |
43 | result = result + strings[i].slice(cutLen);
44 | }
45 |
46 | result = result.trim() + "\n";
47 | copyToClipboard(result);
48 | }
49 |
50 |
51 | document.addEventListener("DOMContentLoaded", () => {
52 | // Edit CSS
53 | let newStyleSheet = `
54 | pre {
55 | position: relative;
56 | overflow: auto;
57 | }
58 |
59 | pre button {
60 | visibility: hidden;
61 | }
62 |
63 | pre:hover > button {
64 | visibility: visible;
65 | }
66 | `;
67 | let styleSheet = document.createElement("style")
68 | styleSheet.innerText = newStyleSheet
69 | document.head.appendChild(styleSheet)
70 |
71 | // Edit pre tags
72 | let preElements = document.getElementsByTagName("pre");
73 |
74 | for (var i = 0; preElements.length > i; i++) {
75 | preElements[i].insertAdjacentHTML("beforeend", "{{call .Translate `codeJS.Paste`}} ");
76 | }
77 | });
78 |
--------------------------------------------------------------------------------
/internal/web/data/docs.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{ call .Translate `docs.Title` }} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 | {{ call .Translate `docs.Title` }}
25 |
29 | {{end}}
30 |
--------------------------------------------------------------------------------
/internal/web/data/docs_api_libs.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{ call .Translate `docsAPIv1Libs.Title` }} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 |
25 | {{ call .Translate `docsAPIv1Libs.Recommended` }}
26 |
27 | {{ call .Translate `docsAPIv1Libs.Name` }}
28 | {{ call .Translate `docsAPIv1Libs.Language` }}
29 | {{ call .Translate `docsAPIv1Libs.ApiVersion` }}
30 | {{ call .Translate `docsAPIv1Libs.Status` }}
31 | {{ call .Translate `docsAPIv1Libs.License` }}
32 |
33 | PasteAPI.go
34 | Go
35 | v1.2
36 | {{ call .Translate `docsAPIv1Libs.StatusOfficial`}}
37 | MIT
38 |
39 |
40 | {{ call .Translate `docsAPIv1Libs.OutOfDateTitle` }}
41 | {{ call .Translate `docsAPIv1Libs.OutOfDataMessage` }}
42 |
43 | {{ call .Translate `docsAPIv1Libs.Name` }}
44 | {{ call .Translate `docsAPIv1Libs.Language` }}
45 | {{ call .Translate `docsAPIv1Libs.ApiVersion` }}
46 | {{ call .Translate `docsAPIv1Libs.Status` }}
47 | {{ call .Translate `docsAPIv1Libs.License` }}
48 |
49 | Lenin
50 | Go
51 | v0.2
52 | {{ call .Translate `docsAPIv1Libs.StatusOfficial` }}
53 | {{ call .Translate `docsAPIv1Libs.GPL3Later` }}
54 |
55 |
56 | PyLenin
57 | Python
58 | v0.1
59 | {{ call .Translate `docsAPIv1Libs.StatusUnofficial` }}
60 | {{ call .Translate `docsAPIv1Libs.GPL3Later` }}
61 |
62 |
63 | {{end}}
64 |
--------------------------------------------------------------------------------
/internal/web/data/docs_apiv1.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{call .Translate `docsAPIv1.Title`}} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 |
25 |
26 | {{call .Translate `docsAPIv1.Introduction1`}}
27 | {{call .Translate `docsAPIv1.Introduction2`}}
28 |
29 | {{call .Translate `docsAPIv1.TableOfContent`}}
30 |
36 |
37 |
38 | POST /api/v1/new
39 | {{call .Translate `docsAPIv1.NewPasteAuth`}}
40 | {{call .Translate `docsAPIv1.RequestParameters`}}
41 |
42 | {{call .Translate `docsAPIv1.Field`}}
43 | {{call .Translate `docsAPIv1.Required`}}
44 | {{call .Translate `docsAPIv1.Default`}}
45 | {{call .Translate `docsAPIv1.Description`}}
46 |
47 | title
48 |
49 |
50 | {{call .Translate `docsAPIv1.ReqNewTitle`}}
51 |
52 |
53 | body
54 | {{call .Translate `docsAPIv1.RequiredYes`}}
55 |
56 | {{call .Translate `docsAPIv1.ReqNewBody`}}
57 |
58 |
59 | lineEnd
60 |
61 | LF
62 | {{call .Translate `docsAPIv1.ReqNewLineEnd`}}
63 |
64 |
65 | syntax
66 |
67 | plaintext
68 | {{call .Translate `docsAPIv1.ReqNewSyntax` `#getServerInfo`}}
69 |
70 |
71 | oneUse
72 |
73 | false
74 | {{call .Translate `docsAPIv1.ReqNewOneUse`}}
75 |
76 |
77 | expiration
78 |
79 | 0
80 | {{call .Translate `docsAPIv1.ReqNewExpiration`}}
81 |
82 |
83 | author
84 |
85 |
86 | {{call .Translate `docsAPIv1.ReqNewAuthor` .MaxLenAuthorAll}}
87 |
88 |
89 | authorEmail
90 |
91 |
92 | {{call .Translate `docsAPIv1.ReqNewAuthorEmail` .MaxLenAuthorAll}}
93 |
94 |
95 | authorURL
96 |
97 |
98 | {{call .Translate `docsAPIv1.ReqNewAuthorURL` .MaxLenAuthorAll}}
99 |
100 |
101 | {{call .Translate `docsAPIv1.ResponseExample`}}
102 | {{ call .Highlight `{
103 | "id": "XcmX9ON1",
104 | "createTime": 1653387358,
105 | "deleteTime": 0
106 | }` `json`}}
107 |
108 |
109 | GET /api/v1/get
110 | {{call .Translate `docsAPIv1.RequestParameters`}}
111 |
112 | {{call .Translate `docsAPIv1.Field`}}
113 | {{call .Translate `docsAPIv1.Required`}}
114 | {{call .Translate `docsAPIv1.Default`}}
115 | {{call .Translate `docsAPIv1.Description`}}
116 |
117 | id
118 | {{call .Translate `docsAPIv1.RequiredYes`}}
119 |
120 | {{call .Translate `docsAPIv1.ReqGetID`}}
121 |
122 |
123 | openOneUse
124 |
125 | false
126 | {{call .Translate `docsAPIv1.ReqGetOpenOneUse`}}
127 |
128 |
129 | {{call .Translate `docsAPIv1.ResponseExample`}}
130 | {{ call .Highlight `{
131 | "id": "XcmX9ON1",
132 | "title": "Paste title.",
133 | "body": "Line 1.\nLine 2.\nLine 3.\n\nLine 5.",
134 | "createTime": 1653387358,
135 | "deleteTime": 0,
136 | "oneUse": false,
137 | "syntax": "plaintext",
138 | "author": "Anon",
139 | "authorEmail": "me@example.org",
140 | "authorURL": "https://example.org"
141 | }` `json`}}
142 | {{ call .Highlight `{
143 | "id": "5mqqHZRg",
144 | "title": "",
145 | "body": "",
146 | "createTime": 0,
147 | "deleteTime": 0,
148 | "oneUse": true,
149 | "syntax": "",
150 | "author": "",
151 | "authorEmail": "",
152 | "authorURL": ""
153 | }` `json`}}
154 |
155 |
156 | GET /api/v1/getServerInfo
157 | {{call .Translate `docsAPIv1.ResponseExample`}}
158 | {{ call .Highlight `{
159 | "software": "Lenpaste",
160 | "version": "1.2",
161 | "titleMaxlength": 100,
162 | "bodyMaxlength": 20000,
163 | "maxLifeTime": -1,
164 | "serverAbout": "",
165 | "serverRules": "",
166 | "serverTermsOfUse": "",
167 | "adminName": "Vasya Pupkin",
168 | "adminMail": "me@example.org",
169 | "syntaxes": [
170 | "ABAP",
171 | "ABNF",
172 | "AL",
173 | "ANTLR",
174 | "APL",
175 | "ActionScript",
176 | "ActionScript 3",
177 | "Ada",
178 | "Angular2",
179 | "ApacheConf",
180 | "AppleScript",
181 | "Arduino",
182 | "ArmAsm",
183 | "Awk"
184 | ],
185 | "uiDefaultLifeTime": "1y",
186 | "authRequired": false
187 | }` `json`}}
188 |
189 |
190 | {{call .Translate `docsAPIv1.PossibleAPIErrors`}}
191 | {{call .Translate `docsAPIv1.Error400`}}
192 | {{ call .Highlight `{
193 | "code": 400,
194 | "error": "Bad Request"
195 | }` `json`}}
196 |
197 | {{call .Translate `docsAPIv1.Error401`}}
198 | {{ call .Highlight `{
199 | "code": 401,
200 | "error": "Unauthorized"
201 | }` `json`}}
202 |
203 | {{call .Translate `docsAPIv1.Error404n1`}}
204 | {{ call .Highlight `{
205 | "code": 404,
206 | "error": "Could not find ID"
207 | }` `json`}}
208 |
209 | {{call .Translate `docsAPIv1.Error404n2`}}
210 | {{ call .Highlight `{
211 | "code": 404,
212 | "error": "Not Found"
213 | }` `json`}}
214 |
215 | {{call .Translate `docsAPIv1.Error405`}}
216 | {{ call .Highlight `{
217 | "code": 405,
218 | "error": "Method Not Allowed"
219 | }` `json`}}
220 |
221 | {{call .Translate `docsAPIv1.Error413`}}
222 | {{ call .Highlight `{
223 | "code": 413,
224 | "error": "Payload Too Large"
225 | }` `json`}}
226 |
227 | {{call .Translate `docsAPIv1.Error429`}}
228 | {{ call .Highlight `{
229 | "code": 429,
230 | "error": "Too Many Requests"
231 | }` `json`}}
232 |
233 | {{call .Translate `docsAPIv1.Error500`}}
234 | {{ call .Highlight `{
235 | "code": 500,
236 | "error": "Internal Server Error"
237 | }` `json`}}
238 | {{end}}
239 |
--------------------------------------------------------------------------------
/internal/web/data/emb.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 |
22 |
23 |
24 |
50 |
51 |
52 |
56 | {{if or (.ErrorNotFound) (.OneUse) (ne .DeleteTime 0)}}
57 |
58 | {{if .ErrorNotFound}}
59 | {{ call .Translate `pasteEmd.Error` }}: {{ call .Translate `pasteEmd.ErrorNotFound` }}
60 | {{else}}
61 | {{ call .Translate `pasteEmd.Error` }}: {{ call .Translate `pasteEmb.ErrorCouldNotEmb` }}
62 | {{end}}
63 |
64 | {{else}}
65 | {{.Body}}
66 | {{end}}
67 |
68 |
69 |
--------------------------------------------------------------------------------
/internal/web/data/emb_help.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{ call .Translate `pasteEmbHelp.Title` }} {{.ID}} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 | {{.ID}} / {{ call .Translate `pasteEmbHelp.Title` }}
25 | {{if or (.OneUse) (ne .DeleteTime 0)}}
26 | {{ call .Translate `pasteEmbHelp.OneUseError` }}`
27 | {{else}}
28 | {{ call .Translate `pasteEmbHelp.Message` }}
29 | {{call .Highlight (printf `` .Protocol .Host .ID) `html`}}
30 | {{end}}
31 | {{end}}
32 |
--------------------------------------------------------------------------------
/internal/web/data/error.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{.Code}} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 | {{.Code}}
25 | {{if eq .Code 400 }}{{ call .Translate `error.400` }}
{{end}}
26 | {{if eq .Code 401 }}{{ call .Translate `error.401` }}
{{end}}
27 | {{if eq .Code 404 }}{{ call .Translate `error.404` }}
{{end}}
28 | {{if eq .Code 405 }}{{ call .Translate `error.405` }}
{{end}}
29 | {{if eq .Code 413 }}{{ call .Translate `error.413` }}
{{end}}
30 | {{if eq .Code 429 }}{{ call .Translate `error.429` }}
{{end}}
31 | {{if eq .Code 500 }}{{ call .Translate `error.500` }}
{{end}}
32 |
33 |
34 | {{if and (ne .AdminName ``) (ne .AdminMail ``)}}
35 | {{ call .Translate `error.AdminContacts` }} {{.AdminName}} <{{.AdminMail}} >
36 | {{else}}
37 | {{if ne .AdminName ``}}{{ call .Translate `error.AdminContacts` }} {{.AdminName}}
{{end}}
38 | {{if ne .AdminMail ``}}{{ call .Translate `error.AdminContacts` }} {{.AdminMail}}
{{end}}
39 | {{end}}
40 |
41 | << {{ call .Translate `error.BackToHome` }}
42 | {{end}}
43 |
--------------------------------------------------------------------------------
/internal/web/data/locale/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "about.AboutServerTitle": "About this server",
3 | "about.AdminEmail": "Administrator's mail:",
4 | "about.AdminName": "Administrator's name:",
5 | "about.AdminTitle": "Contact the administrator",
6 | "about.Lenpaste1": "Lenpaste is free software. All of its source code is available under the %s license.",
7 | "about.Lenpaste2": "You do not need to register here.",
8 | "about.Lenpaste3": "This site does not use cookies to keep track of you.",
9 | "about.Lenpaste4": "This site can work without JavaScript.",
10 | "about.Lenpaste5": "Lenpaste has its own API .",
11 | "about.LenpasteAuthors": "Learn more about the authors of Lenpaste software. ",
12 | "about.LenpasteMessage": "This server uses a Lenpaste version of %s
. A little bit about it:",
13 | "about.LenpasteTitle": "What is Lenpaste?",
14 | "about.Limit": "Limits",
15 | "about.LimitBody": "Maximum length of the paste: %d
symbols.",
16 | "about.LimitBodyNo": "The length of the paste is unlimited.",
17 | "about.LimitLifeTime": "Maximum lifetime of the paste: %d
seconds.",
18 | "about.LimitLifeTimeNo": "The lifetime of the paste is unlimited.",
19 | "about.LimitTitle": "Maximum title length: %d
characters.",
20 | "about.LimitTitleDisable": "On this server, you cannot set a header for the paste.",
21 | "about.LimitTitleNo": "There is no limit to the length of the title.",
22 | "about.RulesTitle": "Rules of this server",
23 | "about.SeeTerms": "See the terms of use for more information.",
24 | "about.Title": "About",
25 | "authors.Title": "Authors",
26 | "base.About": "About",
27 | "base.Docs": "Docs",
28 | "base.Lenpaste": "Lenpaste",
29 | "base.Settings": "Settings",
30 | "codeJS.Paste": "Copy",
31 | "docs.Title": "Documentation",
32 | "docsAPIv1.Default": "Default",
33 | "docsAPIv1.Description": "Description",
34 | "docsAPIv1.Error400": "This API method exists on the server, but you passed the wrong arguments for it.",
35 | "docsAPIv1.Error401": "This server requires \"HTTP Basic Authentication\" authorization.",
36 | "docsAPIv1.Error404n1": "There is no paste with this ID.",
37 | "docsAPIv1.Error404n2": "There is no such API method.",
38 | "docsAPIv1.Error405": "You made a mistake with HTTP request (example: you made POST instead of GET).",
39 | "docsAPIv1.Error413": "You have exceeded the maximum size of one or more fields (title
, body
, author
, authorEmail
, authorURL
).",
40 | "docsAPIv1.Error429": "You have made too many requests, try again after some time. The Retry-After
HTTP header will also be returned along with this error.",
41 | "docsAPIv1.Error500": "There was a failure on the server. Contact your server administrator to find out what the problem is.",
42 | "docsAPIv1.Field": "Field",
43 | "docsAPIv1.NewPasteAuth": "If you are using a private server, authenticate using \"HTTP Basic Authentication\". Otherwise you will get a 401 error.",
44 | "docsAPIv1.PossibleAPIErrors": "Possible API errors",
45 | "docsAPIv1.ReqGetID": "Paste ID.",
46 | "docsAPIv1.ReqGetOpenOneUse": "If true
, the entire contents of the paste will be returned, after which it will be deleted. If false
, the API will return only id
and oneUse
, and the paste will not be deleted.",
47 | "docsAPIv1.ReqNewAuthor": "Author name. Must not be more than %d characters.",
48 | "docsAPIv1.ReqNewAuthorEmail": "Author email. Must not be more than %d characters.",
49 | "docsAPIv1.ReqNewAuthorURL": "Author URL. Must not be more than %d characters.",
50 | "docsAPIv1.ReqNewBody": "Paste text.",
51 | "docsAPIv1.ReqNewExpiration": "Indicates expiration of paste in seconds. If this parameter is 0
, the storage time will be unlimited.",
52 | "docsAPIv1.ReqNewLineEnd": "Line end in the text of the excerpt will automatically be replaced by the one specified by this parameter. Can be LF
, CRLF
or CR
.",
53 | "docsAPIv1.ReqNewOneUse": "If it is true
, the paste can be opened only once and then it will be deleted.",
54 | "docsAPIv1.ReqNewSyntax": "Syntax highlighting in paste. A list of available syntaxes can be obtained using the getServerInfo
method.",
55 | "docsAPIv1.ReqNewTitle": "Paste title.",
56 | "docsAPIv1.RequestParameters": "Request parameters:",
57 | "docsAPIv1.Required": "Required?",
58 | "docsAPIv1.RequiredYes": "Yes",
59 | "docsAPIv1.ResponseExample": "Response example:",
60 | "docsAPIv1.TableOfContent": "Table of content",
61 | "docsAPIv1.Title": "API v1",
62 | "docsAPIv1.Introduction1": "The Lenpaste API does not require registration to use it.",
63 | "docsAPIv1.Introduction2": "Any POST request to the API is sent in application/x-www-form-urlencoded
format. Similarly, the API always returns a response to any request in the format application/json
and UTF8 encoding.",
64 | "docsAPIv1Libs.ApiVersion": "API version",
65 | "docsAPIv1Libs.GPL3Later": "GPL 3.0 or later",
66 | "docsAPIv1Libs.Language": "Language",
67 | "docsAPIv1Libs.License": "License",
68 | "docsAPIv1Libs.Name": "Name",
69 | "docsAPIv1Libs.OutOfDataMessage": "These libraries are no longer being updated. This means that they cannot use the Lenpaste API v1.0 and higher.",
70 | "docsAPIv1Libs.OutOfDateTitle": "Out of date",
71 | "docsAPIv1Libs.Recommended": "Recommended",
72 | "docsAPIv1Libs.Status": "Status",
73 | "docsAPIv1Libs.StatusOfficial": "Official",
74 | "docsAPIv1Libs.StatusUnofficial": "Unofficial",
75 | "docsAPIv1Libs.Title": "Libraries for working with API",
76 | "error.400": "Bad Request",
77 | "error.401": "Unauthorized",
78 | "error.404": "Not Found",
79 | "error.405": "Method Not Allowed",
80 | "error.413": "Payload Too Large",
81 | "error.429": "Too Many Requests",
82 | "error.500": "Internal Server Error",
83 | "error.AdminContacts": "Contact administrator:",
84 | "error.BackToHome": "Back to Home",
85 | "error.Error": "Error",
86 | "historyJS.ClearHistory": "Clear history...",
87 | "historyJS.ClearHistoryConfirm": "Are you sure you want to clear the history?",
88 | "historyJS.EnableHistory": "Remember history",
89 | "historyJS.Error": "HTTP error: %d %s",
90 | "historyJS.ErrorUnknown": "Unknown error: %s",
91 | "historyJS.History": "History",
92 | "historyJS.HistoryDisabledAlert": "History saving has been DISABLED.",
93 | "historyJS.HistoryEnabledAlert": "History saving has been ENABLED.",
94 | "historyJS.LocalStorageNotSupported1": "Your browser does not support JavaScript localStorage, so the history saving function is disabled.",
95 | "historyJS.LocalStorageNotSupported2": "If you are using Safari, try exiting private browsing mode, this should fix the error.",
96 | "historyJS.Untitled": "Untitled",
97 | "license.LicenseTitle": "License",
98 | "locale.Name": "English",
99 | "main.10Minutes": "10 minutes",
100 | "main.12Hour": "12 hours",
101 | "main.1Day": "1 day",
102 | "main.1Hour": "1 hour",
103 | "main.1Month": "1 month",
104 | "main.1Week": "1 week",
105 | "main.1Year": "1 year",
106 | "main.2Hour": "2 hours",
107 | "main.2Months": "2 months",
108 | "main.2Weeks": "2 weeks",
109 | "main.30Minutes": "30 minutes",
110 | "main.4Hour": "4 hours",
111 | "main.6Months": "6 months",
112 | "main.AcceptTerms": "See Terms of Use ",
113 | "main.AdvancedParameters": "Advanced parameters",
114 | "main.AdvancedParametersHelp": "*You can set the default values for these parameters in the settings .",
115 | "main.AuthRequired": "This is a private server and authorization is required to use it. Please contact the server administrator for details.",
116 | "main.Author": "Author name:",
117 | "main.AuthorEmail": "Author email:",
118 | "main.AuthorEmailPlaceholder": "me@example.org",
119 | "main.AuthorPlaceholder": "Name",
120 | "main.AuthorURL": "Author URL:",
121 | "main.AuthorURLPlaceholder": "https://example.org",
122 | "main.BurnAfterReading": "Burn after reading",
123 | "main.Create": "Create New Paste",
124 | "main.CreatePaste": "Create paste",
125 | "main.EnterText": "Enter text...",
126 | "main.EnterTitle": "Title (optional)...",
127 | "main.Expiration": "Expiration:",
128 | "main.MaximumSymbols": "*Maximum %d symbols",
129 | "main.Never": "Never",
130 | "main.Syntax": "Syntax:",
131 | "paste.Author": "Author:",
132 | "paste.Created": "Created:",
133 | "paste.Download": "Download",
134 | "paste.Embedded": "Embedded",
135 | "paste.Expires": "Expires:",
136 | "paste.Never": "Never",
137 | "paste.Now": "Now",
138 | "paste.Raw": "Raw",
139 | "pasteContinue.Cancel": "Cancel",
140 | "pasteContinue.Continue": "Continue",
141 | "pasteContinue.Message": "This paste can only be viewed once, after which it will be deleted. Continue?",
142 | "pasteContinue.Title": "Continue?",
143 | "pasteEmb.ErrorCouldNotEmb": "This paste cannot be embedded in other pages",
144 | "pasteEmbHelp.Message": "Add the following code to your page:",
145 | "pasteEmbHelp.OneUseError": "You cannot embed the paste in another page if it is intended to be read once or has a limited expiration.",
146 | "pasteEmbHelp.Title": "Embedded",
147 | "pasteEmd.Error": "Error:",
148 | "pasteEmd.ErrorNotFound": "404 Not Found",
149 | "pasteJS.ShortMonth": "\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"",
150 | "pasteJS.ShortWeekDay": "\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"",
151 | "settings.Language": "Language:",
152 | "settings.LanguageDefault": "Use browser language",
153 | "settings.Save": "Save Settings",
154 | "settings.Theme": "Theme:",
155 | "settings.Title": "Settings",
156 | "sourceCode.Message": "Unfortunately, it is not yet possible to download the source code directly from this server. But you can download it from the link:",
157 | "sourceCode.Title": "Source Code",
158 | "terms.NoTerms": "This server has no terms of use.",
159 | "terms.Notice": "The Terms of Use apply to this server only, not to the Lenpaste software.",
160 | "terms.Title": "Terms of Use"
161 | }
162 |
--------------------------------------------------------------------------------
/internal/web/data/main.js:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | document.addEventListener("DOMContentLoaded", () => {
20 | var editor = document.getElementById("editor");
21 |
22 | editor.addEventListener("keydown", (e) => {
23 | // If TAB pressed
24 | if (e.keyCode === 9) {
25 | e.preventDefault();
26 |
27 | let startOrig = editor.selectionStart;
28 | let endOrig = editor.selectionEnd;
29 |
30 | editor.value = editor.value.substring(0, startOrig) + "\t" + editor.value.substring(endOrig);
31 |
32 | editor.selectionStart = editor.selectionEnd = startOrig + 1;
33 | }
34 | });
35 |
36 | // Add HTML and CSS code for line numbers support
37 | editor.insertAdjacentHTML("beforebegin", "");
38 | var editorLines = document.getElementById("editorLines");
39 | editorLines.rows = editor.rows;
40 |
41 | var styleSheet = document.createElement("style");
42 | styleSheet.innerText = `
43 | #editor {
44 | margin-left: 60px;
45 | resize: none;
46 |
47 | width: calc(100% - 60px);
48 | min-width: calc(100% - 60px);
49 | max-width: calc(100% - 60px);
50 | }
51 |
52 | #editorLines {
53 | display: flex;
54 | user-select: none;
55 |
56 | text-align: right;
57 | position: absolute;
58 | resize: none;
59 | overflow-y: hidden;
60 | width: 60px;
61 | max-width: 60px;
62 | min-width: 60px;
63 | }
64 |
65 | #editor:focus-visible, #editorLines:focus-visible {
66 | outline: 0;
67 | }
68 | `;
69 | document.head.appendChild(styleSheet);
70 |
71 | editorLines.addEventListener("focus", () => {
72 | editor.focus();
73 | });
74 |
75 | // Add JS code for line numbers
76 | editor.addEventListener("scroll", () => {
77 | editorLines.scrollTop = editor.scrollTop;
78 | editorLines.scrollLeft = editor.scrollLeft;
79 | });
80 |
81 | var lineCountCache = 0;
82 | editor.addEventListener("input", () => {
83 | let lineCount = editor.value.split("\n").length;
84 |
85 | if (lineCountCache != lineCount) {
86 | editorLines.value = "";
87 |
88 | for (var i = 0; i < lineCount; i++) {
89 | editorLines.value = editorLines.value + (i + 1) + "\n";
90 | }
91 |
92 | lineCountCache = lineCount;
93 | }
94 | });
95 |
96 | // Add symbol counter
97 | document.getElementById("symbolCounterContainer").innerHTML = " ";
98 | var symbolCounter = document.getElementById("symbolCounter");
99 |
100 | function updateSymbolCounter() {
101 | symbolCounter.textContent = editor.value.length;
102 |
103 | if (editor.maxLength !== -1) {
104 | symbolCounter.textContent = symbolCounter.textContent + "/" + editor.maxLength;
105 | } else {
106 | symbolCounter.textContent = symbolCounter.textContent + "/∞";
107 | }
108 | }
109 |
110 | editor.addEventListener("input", updateSymbolCounter);
111 | updateSymbolCounter();
112 | });
113 |
--------------------------------------------------------------------------------
/internal/web/data/main.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 | {{if eq .AuthOk false}}
25 | {{call .Translate `main.CreatePaste`}}
26 | {{call .Translate `main.AuthRequired`}}
27 | {{else}}
28 | {{if ne .TitleMaxLen 0}}{{call .Translate `main.CreatePaste`}} {{end}}
29 |
128 | {{end}}
129 | {{end}}
130 |
--------------------------------------------------------------------------------
/internal/web/data/paste.js:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | document.addEventListener("DOMContentLoaded", () => {
20 | const shortWeekDay = [{{call .Translate `pasteJS.ShortWeekDay`}}];
21 | const shortMonth = [{{call .Translate `pasteJS.ShortMonth`}}];
22 |
23 | function dateToString(date) {
24 | let dateStr = shortWeekDay[date.getDay()] + ", " + date.getDate() + " " + shortMonth[date.getMonth()];
25 | dateStr = dateStr + " " + date.getFullYear();
26 | dateStr = dateStr + " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds();
27 |
28 | let tz = date.getTimezoneOffset() / 60 * -1;
29 | if (tz >= 0) {
30 | dateStr = dateStr + " +" + tz;
31 |
32 | } else {
33 | dateStr = dateStr + " " + tz;
34 | }
35 |
36 | return dateStr;
37 | }
38 |
39 | let createTime = document.getElementById("createTime");
40 | createTime.textContent = dateToString(new Date(createTime.textContent));
41 |
42 |
43 | let deleteTime = document.getElementById("deleteTime");
44 | if (deleteTime != null) {
45 | deleteTime.textContent = dateToString(new Date(deleteTime.textContent));
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/internal/web/data/paste.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{if .Title}}{{.Title}}{{else}}{{.ID}}{{end}} | {{end}}
22 | {{define "headAppend"}}
23 |
24 |
25 | {{end}}
26 | {{define "article"}}
27 | {{if .Title}}
28 | {{end}}
29 |
30 |
31 |
{{.Syntax}}, {{.LineEnd}}
32 |
33 | {{if not .OneUse}}
34 |
37 | {{end}}
38 |
39 |
40 | {{.Body}}
41 |
42 | {{if and (ne .Author ``) (ne .AuthorEmail ``) (ne .AuthorURL ``) }}{{ call .Translate `paste.Author` }} {{.Author}} <{{.AuthorEmail}} > - {{.AuthorURL}}
{{end}}
43 | {{if and (ne .Author ``) (ne .AuthorEmail ``) (eq .AuthorURL ``) }}{{ call .Translate `paste.Author` }} {{.Author}} <{{.AuthorEmail}} >
{{end}}
44 | {{if and (ne .Author ``) (eq .AuthorEmail ``) (ne .AuthorURL ``) }}{{ call .Translate `paste.Author` }} {{.Author}} - {{.AuthorURL}}
{{end}}
45 | {{if and (eq .Author ``) (ne .AuthorEmail ``) (ne .AuthorURL ``) }}{{ call .Translate `paste.Author` }} {{.AuthorEmail}} - {{.AuthorURL}}
{{end}}
46 | {{if and (ne .Author ``) (eq .AuthorEmail ``) (eq .AuthorURL ``) }}{{ call .Translate `paste.Author` }} {{.Author}}
{{end}}
47 | {{if and (eq .Author ``) (ne .AuthorEmail ``) (eq .AuthorURL ``) }}{{ call .Translate `paste.Author` }} {{.AuthorEmail}}
{{end}}
48 | {{if and (eq .Author ``) (eq .AuthorEmail ``) (ne .AuthorURL ``) }}{{ call .Translate `paste.Author` }} {{.AuthorURL}}
{{end}}
49 |
50 | {{ call .Translate `paste.Created` }} {{.CreateTimeStr}}
51 |
52 | {{if .OneUse}}
53 | {{ call .Translate `paste.Expires` }} {{ call .Translate `paste.Now` }}
54 | {{else if eq .DeleteTime 0}}
55 | {{ call .Translate `paste.Expires` }} {{ call .Translate `paste.Never` }}
56 | {{else}}
57 | {{ call .Translate `paste.Expires` }} {{.DeleteTimeStr}}
58 | {{end}}
59 |
60 | {{end}}
61 |
--------------------------------------------------------------------------------
/internal/web/data/paste_continue.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{.ID}} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 | {{ call .Translate `pasteContinue.Title` }}
25 | {{ call .Translate `pasteContinue.Message` }}
26 |
27 |
28 | {{ call .Translate `pasteContinue.Cancel` }}
29 |
30 |
31 |
32 | {{ call .Translate `pasteContinue.Continue` }}
33 |
34 |
35 | {{end}}
36 |
--------------------------------------------------------------------------------
/internal/web/data/settings.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{call .Translate `settings.Title`}} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 | {{call .Translate `settings.Title`}}
25 |
26 |
27 |
28 | {{ call .Translate `settings.Language` }}
29 |
30 | {{call .Translate `settings.LanguageDefault`}}
31 | {{ $language := .Language }}
32 | {{range $key, $value := .LanguageSelector}}
33 | {{$value}}
34 | {{end}}
35 |
36 |
37 |
38 | {{ call .Translate `settings.Theme` }}
39 |
40 | {{ $theme := .Theme }}
41 | {{range $key, $value := .ThemeSelector}}
42 | {{$value}}
43 | {{end}}
44 |
45 |
46 |
47 | {{if eq .AuthOk true}}
48 | {{ call .Translate `main.AdvancedParameters` }}
49 |
78 | {{end}}
79 | {{ call .Translate `settings.Save` }}
80 |
81 | {{end}}
82 |
--------------------------------------------------------------------------------
/internal/web/data/source_code.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{ call .Translate `sourceCode.Title` }} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 |
25 | {{ call .Translate `sourceCode.Message` }}
26 |
27 | https://github.com/lcomrade/lenpaste
28 | {{end}}
29 |
--------------------------------------------------------------------------------
/internal/web/data/style.css:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */
20 |
21 | /* MAIN */
22 | body {
23 | width: 70%;
24 | margin: 0;
25 | margin-left: auto;
26 | margin-right: auto;
27 | font-style: normal;
28 | background-color: {{call .Theme `color.BackgroundBody`}};
29 | font-family: {{call .Theme `font.Default`}};
30 | font-size: 16px;
31 | color: {{call .Theme `color.Font`}};
32 | }
33 |
34 | @media all and (max-device-width: 720px), all and (orientation: portrait) {
35 | body {
36 | width: 100%;
37 | }
38 | }
39 |
40 | @media all and (max-device-width: 640px) {
41 | .header-right {
42 | display: block;
43 | width: 100%;
44 | }
45 | }
46 |
47 |
48 | /* HEADER */
49 | header {
50 | padding-top: 20px;
51 | padding-bottom: 20px;
52 | padding-left: 20px;
53 | padding-right: 20px;
54 | color: {{call .Theme `color.HeaderFont`}};
55 | background: {{call .Theme `color.Header`}};
56 | }
57 |
58 | header h2,
59 | header h4 {
60 | display: inline;
61 | margin-right: 15px;
62 | padding: 0;
63 | }
64 |
65 | header div {
66 | width: 50%;
67 | display: inline-block;
68 | }
69 |
70 | .header-right {
71 | text-align: right;
72 | }
73 |
74 | header h2 a, header h2 a:hover,
75 | header h4 a, header h4 a:hover {
76 | color: {{call .Theme `color.HeaderFont`}};
77 | text-decoration: none;
78 | white-space: nowrap;
79 | }
80 |
81 |
82 | /* ARTICLE */
83 | article {
84 | width: 100%;
85 | box-sizing: border-box;
86 | margin-top: 20px;
87 | margin-bottom: 20px;
88 | padding: 20px;
89 | line-height: 1.5;
90 | background: {{call .Theme `color.Article`}};
91 | }
92 |
93 |
94 | /* CONTAINERS */
95 | .stretch-width {
96 | width: 100%;
97 | box-sizing: border-box;
98 | }
99 |
100 | .button-block-right {
101 | display: flex;
102 | justify-content: flex-end;
103 | }
104 |
105 | .button-block-right button {
106 | margin-left: 20px;
107 | }
108 |
109 | .text-bar {
110 | display: flex;
111 | justify-content: start;
112 | font-family: {{call .Theme `font.Monospace`}};
113 | }
114 |
115 | .text-bar div {
116 | width: 50%;
117 | }
118 |
119 | .text-bar h1,
120 | .text-bar h2,
121 | .text-bar h3,
122 | .text-bar h4,
123 | .text-bar h5,
124 | .text-bar h6 {
125 | font-family: {{call .Theme `font.Default`}};
126 | }
127 |
128 | .text-bar-right {
129 | align-self: flex-end;
130 | text-align: right;
131 | }
132 |
133 | .text-bar-right a {
134 | margin-left: 10px;
135 | }
136 |
137 |
138 | /* TEXT */
139 | .text-red, .text-red:hover {
140 | color: {{call .Theme `color.Red`}};
141 | }
142 |
143 | .text-grey, .text-grey:hover {
144 | color: {{call .Theme `color.Grey`}};
145 | }
146 |
147 |
148 | /* OTHER */
149 | :focus-visible {
150 | outline: 2px solid {{call .Theme `color.FocusOutline`}};
151 | }
152 |
153 | :active {
154 | outline: 0;
155 | }
156 |
157 | h1, h2, h3, h4, h5, h6,
158 | p, pre,
159 | select, label,
160 | button, input,
161 | textarea {
162 | margin-top: 10px;
163 | margin-bottom: 10px;
164 | }
165 |
166 | h4 {
167 | margin-top: 40px;
168 | }
169 |
170 | pre, textarea {
171 | width: 100%;
172 | max-width: 100%;
173 | min-width: 100%;
174 | box-sizing: border-box;
175 | tab-size: 4;
176 | background-color: {{call .Theme `color.Element`}};
177 | padding: 10px;
178 | border: 2px;
179 | font-family: {{call .Theme `font.Monospace`}};
180 | font-size: inherit;
181 | color: inherit;
182 | }
183 |
184 | pre {
185 | overflow: auto;
186 | }
187 |
188 | textarea {
189 | min-height: 100px;
190 | resize: vertical;
191 | }
192 |
193 | label {
194 | font-family: {{call .Theme `font.Default`}};
195 | }
196 |
197 | input::placeholder, textarea::placeholder {
198 | color: {{call .Theme `color.InputPlaceholder`}};
199 | }
200 |
201 | input {
202 | background: {{call .Theme `color.Element`}};
203 | font-size: inherit;
204 | font-family: inherit;
205 | color: inherit;
206 | padding: 10px;
207 | border: 2px;
208 | overflow-x: scroll;
209 | }
210 |
211 | select {
212 | appearance: none;
213 | background: {{call .Theme `color.Element`}};
214 | color: {{call .Theme `color.Font`}};
215 | font-size: 16px;
216 | padding: 4px 22px 4px 4px;
217 | border: 2px;
218 | }
219 |
220 | select:hover, input[type="checkbox"]:hover {
221 | background-color: {{call .Theme `color.InputHover`}};
222 | cursor: pointer;
223 | }
224 |
225 | select, select:hover {
226 | background-image: url("data:image/svg+xml, ");
227 | background-repeat: no-repeat;
228 | background-position: right 4px center;
229 | background-size: 10px;
230 | }
231 |
232 | label {
233 | margin-right: 10px;
234 | }
235 |
236 | button, select, input, textarea {
237 | border-radius: 0;
238 | -webkit-border-radius: 0;
239 | -moz-border-radius: 0;
240 |
241 | appearance: none;
242 | -webkit-appearance: none;
243 | -moz-appearance: none;
244 | }
245 |
246 | button {
247 | background: {{call .Theme `color.Grey`}};
248 | font-size: 16px;
249 | padding: 8px 8px 8px 8px;
250 | border: 2px;
251 | color: {{call .Theme `color.ButtonFont`}};
252 | }
253 |
254 | button:hover {
255 | background: {{call .Theme `color.ButtonHover`}};
256 | cursor: pointer;
257 | }
258 |
259 | .button-green {
260 | background: {{call .Theme `color.ButtonGreen`}};
261 | color: {{call .Theme `color.ButtonGreenFont`}};
262 | }
263 |
264 | .button-green:hover {
265 | background: {{call .Theme `color.ButtonGreenHover`}};
266 | }
267 |
268 | .checkbox {
269 | padding-left: 25px;
270 | }
271 |
272 | .checkbox:hover,
273 | .checkbox:hover input {
274 | cursor: pointer;
275 | }
276 |
277 | input[type="checkbox"] {
278 | position: absolute;
279 | margin-left: -25px;
280 | margin-top: 0.1em;
281 | overflow: hidden;
282 | }
283 |
284 | input[type="checkbox"]:checked {
285 | background-image: url("data:image/svg+xml, ");
286 | background-repeat: no-repeat;
287 | background-position: center;
288 | background-size: 14px;
289 | }
290 |
291 | ul {
292 | list-style: square;
293 | }
294 |
295 | code {
296 | background-color: {{call .Theme `color.Element`}};
297 | padding: 2px 5px 2px 5px;
298 | color: inherit;
299 | }
300 |
301 | pre code {
302 | background-color: inherit;
303 | }
304 |
305 | details {
306 | margin-top: 10px;
307 | margin-bottom: 10px;
308 | }
309 |
310 | details summary {
311 | cursor: pointer;
312 | }
313 |
314 | details summary::-webkit-details-marker {
315 | display:none;
316 | }
317 |
318 | details summary {
319 | list-style-type: none;
320 | background-image: url("data:image/svg+xml, ");
321 | background-repeat: no-repeat;
322 | background-position: left;
323 | background-size: 10px;
324 | padding-left: 14px;
325 | }
326 |
327 | details[open] summary {
328 | background-image: url("data:image/svg+xml, ");
329 | }
330 |
331 | table {
332 | background: inherit;
333 | border-collapse: collapse;
334 | }
335 |
336 | th, td {
337 | text-align: left;
338 | padding: 10px;
339 | border: 1px;
340 | border-style: solid;
341 | font-size: inherit;
342 | font-family: inherit;
343 | color: inherit;
344 | }
345 |
346 | .table-hidden td {
347 | text-align: right;
348 | padding: 0;
349 | border: 0;
350 | }
351 |
352 | a, a:hover {
353 | color: {{call .Theme `color.FocusOutline`}};
354 | }
355 |
356 | h1 a, h2 a, h3 a,
357 | h4 a, h5 a, h6 a {
358 | text-decoration: none;
359 | }
360 |
--------------------------------------------------------------------------------
/internal/web/data/terms.tmpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Copyright (C) 2021-2023 Leonid Maslakov.
3 |
4 | This file is part of Lenpaste.
5 |
6 | Lenpaste is free software: you can redistribute it
7 | and/or modify it under the terms of the
8 | GNU Affero Public License as published by the
9 | Free Software Foundation, either version 3 of the License,
10 | or (at your option) any later version.
11 |
12 | Lenpaste is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 | or FITNESS FOR A PARTICULAR PURPOSE.
15 | See the GNU Affero Public License for more details.
16 |
17 | You should have received a copy of the GNU Affero Public License along with Lenpaste.
18 | If not, see .
19 | */}}
20 |
21 | {{define "titlePrefix"}}{{ call .Translate `terms.Title`}} | {{end}}
22 | {{define "headAppend"}}{{end}}
23 | {{define "article"}}
24 | {{ call .Translate `terms.Title` }}
25 | {{if ne .TermsOfUse ``}}
26 | {{ call .Translate `terms.Notice` }}
27 | {{ call .Highlight .TermsOfUse `plaintext` }}
28 | {{else}}
29 | {{ call .Translate `terms.NoTerms` }}
30 | {{end}}
31 | {{end}}
32 |
--------------------------------------------------------------------------------
/internal/web/data/theme/dark.theme:
--------------------------------------------------------------------------------
1 | theme.Name.bn_IN = ডার্ক থিম
2 | theme.Name.de = Dunkel
3 | theme.Name.en = Dark
4 | theme.Name.ru = Тёмная
5 |
6 | font.Default = sans-serif
7 | font.Monospace = monospace
8 |
9 | // Full theme list here:
10 | // https://pkg.go.dev/github.com/alecthomas/chroma/v2/styles#pkg-variables
11 | highlight.Theme = monokai
12 |
13 | color.Font = #FFFFFF
14 | color.SVG = white
15 | color.BackgroundBody = #333333
16 | color.Header = #E88B2E
17 | color.HeaderFont = #FFFFFF
18 | color.Article = #444444
19 | color.BackgroundBlack = #000000
20 | color.FocusOutline = #3DAEE9
21 | color.Element = #666666
22 |
23 | color.Red = #FF1F1F
24 | color.Grey = #888888
25 |
26 | color.Button = #888888
27 | color.ButtonHover = #999999
28 | color.ButtonFont = #FFFFFF
29 | color.ButtonGreen = #66CC00
30 | color.ButtonGreenHover = #74D117
31 | color.ButtonGreenFont = #FFFFFF
32 |
33 | color.InputHover = #777777
34 | color.InputPlaceholder = #B9B9B9
35 |
--------------------------------------------------------------------------------
/internal/web/data/theme/light.theme:
--------------------------------------------------------------------------------
1 | theme.Name.bn_IN = লাইট থিম
2 | theme.Name.de = Hell
3 | theme.Name.en = Light
4 | theme.Name.ru = Светлая
5 |
6 | font.Default = sans-serif
7 | font.Monospace = monospace
8 |
9 | // Full theme list here:
10 | // https://pkg.go.dev/github.com/alecthomas/chroma/v2/styles#pkg-variables
11 | highlight.Theme = monokailight
12 |
13 | color.Font = #000000
14 | color.SVG = black
15 | color.BackgroundBody = #CCCCCC
16 | color.Header = #E88B2E
17 | color.HeaderFont = #FFFFFF
18 | color.Article = #E0E0E0
19 | color.BackgroundBlack = #000000
20 | color.FocusOutline = #342DB5
21 | color.Element = #B9B9B9
22 |
23 | color.Red = #FF1F1F
24 | color.Grey = #888888
25 |
26 | color.Button = #888888
27 | color.ButtonFont = #FFFFFF
28 | color.ButtonHover = #999999
29 | color.ButtonGreen = #66CC00
30 | color.ButtonGreenFont = #FFFFFF
31 | color.ButtonGreenHover = #74D117
32 |
33 | color.InputHover = #C3C3C3
34 | color.InputPlaceholder = #474747
35 |
--------------------------------------------------------------------------------
/internal/web/kvcfg.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "errors"
23 | "strconv"
24 | "strings"
25 | )
26 |
27 | func readKVCfg(data string) (map[string]string, error) {
28 | out := make(map[string]string)
29 |
30 | dataSplit := strings.Split(data, "\n")
31 | dataSplitLen := len(dataSplit)
32 |
33 | for num := 0; num < dataSplitLen; num++ {
34 | str := strings.TrimSpace(dataSplit[num])
35 |
36 | if str == "" || strings.HasPrefix(str, "//") {
37 | continue
38 | }
39 |
40 | strSplit := strings.SplitN(str, "=", 2)
41 | if len(strSplit) != 2 {
42 | return out, errors.New("error in line " + strconv.Itoa(num+1) + ": expected '=' delimiter")
43 | }
44 |
45 | key := strings.TrimSpace(strSplit[0])
46 | val := strings.TrimSpace(strSplit[1])
47 | val, isMultiline := multilineCheck(val)
48 |
49 | if isMultiline {
50 | num = num + 1
51 | for ; num < dataSplitLen; num++ {
52 | strPlus := strings.TrimSpace(dataSplit[num])
53 | strPlus, isMultilinePlus := multilineCheck(strPlus)
54 | val = val + strPlus
55 |
56 | if isMultilinePlus == false {
57 | break
58 | }
59 | }
60 | }
61 |
62 | _, exist := out[key]
63 | if exist {
64 | return out, errors.New("duplicate key: " + key)
65 | }
66 |
67 | out[key] = val
68 | }
69 |
70 | return out, nil
71 | }
72 |
73 | func multilineCheck(s string) (string, bool) {
74 | sLen := len(s)
75 |
76 | if sLen > 0 && s[sLen-1] == '\\' {
77 | if sLen > 1 && s[sLen-2] == '\\' {
78 | return s[:sLen-1], false
79 | }
80 |
81 | return s[:sLen-1], true
82 | }
83 |
84 | return s, false
85 | }
86 |
--------------------------------------------------------------------------------
/internal/web/share.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "net/http"
23 | )
24 |
25 | func getCookie(req *http.Request, name string) string {
26 | cookie, err := req.Cookie(name)
27 | if err != nil {
28 | return ""
29 | }
30 |
31 | return cookie.Value
32 | }
33 |
--------------------------------------------------------------------------------
/internal/web/web.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "embed"
23 | chromaLexers "github.com/alecthomas/chroma/v2/lexers"
24 | "github.com/lcomrade/lenpaste/internal/config"
25 | "github.com/lcomrade/lenpaste/internal/logger"
26 | "github.com/lcomrade/lenpaste/internal/netshare"
27 | "github.com/lcomrade/lenpaste/internal/storage"
28 | "html/template"
29 | "net/http"
30 | "strings"
31 | textTemplate "text/template"
32 | )
33 |
34 | //go:embed data/*
35 | var embFS embed.FS
36 |
37 | type Data struct {
38 | DB storage.DB
39 | Log logger.Logger
40 |
41 | RateLimitNew *netshare.RateLimitSystem
42 | RateLimitGet *netshare.RateLimitSystem
43 |
44 | Lexers []string
45 | Locales Locales
46 | LocalesList LocalesList
47 | Themes Themes
48 | ThemesList ThemesList
49 |
50 | StyleCSS *textTemplate.Template
51 | ErrorPage *template.Template
52 | Main *template.Template
53 | MainJS *[]byte
54 | HistoryJS *textTemplate.Template
55 | CodeJS *textTemplate.Template
56 | PastePage *template.Template
57 | PasteJS *textTemplate.Template
58 | PasteContinue *template.Template
59 | Settings *template.Template
60 | About *template.Template
61 | TermsOfUse *template.Template
62 | Authors *template.Template
63 | License *template.Template
64 | SourceCodePage *template.Template
65 |
66 | Docs *template.Template
67 | DocsApiV1 *template.Template
68 | DocsApiLibs *template.Template
69 |
70 | EmbeddedPage *template.Template
71 | EmbeddedHelpPage *template.Template
72 |
73 | Version string
74 |
75 | TitleMaxLen int
76 | BodyMaxLen int
77 | MaxLifeTime int64
78 |
79 | ServerAbout string
80 | ServerRules string
81 | ServerTermsExist bool
82 | ServerTermsOfUse string
83 |
84 | AdminName string
85 | AdminMail string
86 |
87 | RobotsDisallow bool
88 |
89 | LenPasswdFile string
90 |
91 | UiDefaultLifeTime string
92 | UiDefaultTheme string
93 | }
94 |
95 | func Load(db storage.DB, cfg config.Config) (*Data, error) {
96 | var data Data
97 | var err error
98 |
99 | // Setup base info
100 | data.DB = db
101 | data.Log = cfg.Log
102 |
103 | data.RateLimitNew = cfg.RateLimitNew
104 | data.RateLimitGet = cfg.RateLimitGet
105 |
106 | data.Version = cfg.Version
107 |
108 | data.TitleMaxLen = cfg.TitleMaxLen
109 | data.BodyMaxLen = cfg.BodyMaxLen
110 | data.MaxLifeTime = cfg.MaxLifeTime
111 | data.UiDefaultLifeTime = cfg.UiDefaultLifetime
112 | data.UiDefaultTheme = cfg.UiDefaultTheme
113 | data.LenPasswdFile = cfg.LenPasswdFile
114 |
115 | data.ServerAbout = cfg.ServerAbout
116 | data.ServerRules = cfg.ServerRules
117 | data.ServerTermsOfUse = cfg.ServerTermsOfUse
118 |
119 | serverTermsExist := false
120 | if cfg.ServerTermsOfUse != "" {
121 | serverTermsExist = true
122 | }
123 | data.ServerTermsExist = serverTermsExist
124 |
125 | data.AdminName = cfg.AdminName
126 | data.AdminMail = cfg.AdminMail
127 |
128 | data.RobotsDisallow = cfg.RobotsDisallow
129 |
130 | // Get Chroma lexers
131 | data.Lexers = chromaLexers.Names(false)
132 |
133 | // Load locales
134 | data.Locales, data.LocalesList, err = loadLocales(embFS, "data/locale")
135 | if err != nil {
136 | return nil, err
137 | }
138 |
139 | // Load themes
140 | data.Themes, data.ThemesList, err = loadThemes(cfg.UiThemesDir, data.LocalesList, data.UiDefaultTheme)
141 | if err != nil {
142 | return nil, err
143 | }
144 |
145 | // style.css file
146 | data.StyleCSS, err = textTemplate.ParseFS(embFS, "data/style.css")
147 | if err != nil {
148 | return nil, err
149 | }
150 |
151 | // main.tmpl
152 | data.Main, err = template.ParseFS(embFS, "data/base.tmpl", "data/main.tmpl")
153 | if err != nil {
154 | return nil, err
155 | }
156 |
157 | // main.js
158 | mainJS, err := embFS.ReadFile("data/main.js")
159 | if err != nil {
160 | return nil, err
161 | }
162 | data.MainJS = &mainJS
163 |
164 | // history.js
165 | data.HistoryJS, err = textTemplate.ParseFS(embFS, "data/history.js")
166 | if err != nil {
167 | return nil, err
168 | }
169 |
170 | // code.js
171 | data.CodeJS, err = textTemplate.ParseFS(embFS, "data/code.js")
172 | if err != nil {
173 | return nil, err
174 | }
175 |
176 | // paste.tmpl
177 | data.PastePage, err = template.ParseFS(embFS, "data/base.tmpl", "data/paste.tmpl")
178 | if err != nil {
179 | return nil, err
180 | }
181 |
182 | // paste.js
183 | data.PasteJS, err = textTemplate.ParseFS(embFS, "data/paste.js")
184 | if err != nil {
185 | return nil, err
186 | }
187 |
188 | // paste_continue.tmpl
189 | data.PasteContinue, err = template.ParseFS(embFS, "data/base.tmpl", "data/paste_continue.tmpl")
190 | if err != nil {
191 | return nil, err
192 | }
193 |
194 | // settings.tmpl
195 | data.Settings, err = template.ParseFS(embFS, "data/base.tmpl", "data/settings.tmpl")
196 | if err != nil {
197 | return nil, err
198 | }
199 |
200 | // about.tmpl
201 | data.About, err = template.ParseFS(embFS, "data/base.tmpl", "data/about.tmpl")
202 | if err != nil {
203 | return nil, err
204 | }
205 |
206 | // terms.tmpl
207 | data.TermsOfUse, err = template.ParseFS(embFS, "data/base.tmpl", "data/terms.tmpl")
208 | if err != nil {
209 | return nil, err
210 | }
211 |
212 | // authors.tmpl
213 | data.Authors, err = template.ParseFS(embFS, "data/base.tmpl", "data/authors.tmpl")
214 | if err != nil {
215 | return nil, err
216 | }
217 |
218 | // license.tmpl
219 | data.License, err = template.ParseFS(embFS, "data/base.tmpl", "data/license.tmpl")
220 | if err != nil {
221 | return nil, err
222 | }
223 |
224 | // source_code.tmpl
225 | data.SourceCodePage, err = template.ParseFS(embFS, "data/base.tmpl", "data/source_code.tmpl")
226 | if err != nil {
227 | return nil, err
228 | }
229 |
230 | // docs.tmpl
231 | data.Docs, err = template.ParseFS(embFS, "data/base.tmpl", "data/docs.tmpl")
232 | if err != nil {
233 | return nil, err
234 | }
235 |
236 | // docs_apiv1.tmpl
237 | data.DocsApiV1, err = template.ParseFS(embFS, "data/base.tmpl", "data/docs_apiv1.tmpl")
238 | if err != nil {
239 | return nil, err
240 | }
241 |
242 | // docs_api_libs.tmpl
243 | data.DocsApiLibs, err = template.ParseFS(embFS, "data/base.tmpl", "data/docs_api_libs.tmpl")
244 | if err != nil {
245 | return nil, err
246 | }
247 |
248 | // error.tmpl
249 | data.ErrorPage, err = template.ParseFS(embFS, "data/base.tmpl", "data/error.tmpl")
250 | if err != nil {
251 | return nil, err
252 | }
253 |
254 | // emb.tmpl
255 | data.EmbeddedPage, err = template.ParseFS(embFS, "data/emb.tmpl")
256 | if err != nil {
257 | return nil, err
258 | }
259 |
260 | // emb_help.tmpl
261 | data.EmbeddedHelpPage, err = template.ParseFS(embFS, "data/base.tmpl", "data/emb_help.tmpl")
262 | if err != nil {
263 | return nil, err
264 | }
265 |
266 | return &data, nil
267 | }
268 |
269 | func (data *Data) Handler(rw http.ResponseWriter, req *http.Request) {
270 | // Process request
271 | var err error
272 |
273 | rw.Header().Set("Server", config.Software+"/"+data.Version)
274 |
275 | switch req.URL.Path {
276 | // Search engines
277 | case "/robots.txt":
278 | err = data.robotsTxtHand(rw, req)
279 | case "/sitemap.xml":
280 | err = data.sitemapHand(rw, req)
281 | // Resources
282 | case "/style.css":
283 | err = data.styleCSSHand(rw, req)
284 | case "/main.js":
285 | err = data.mainJSHand(rw, req)
286 | case "/history.js":
287 | err = data.historyJSHand(rw, req)
288 | case "/code.js":
289 | err = data.codeJSHand(rw, req)
290 | case "/paste.js":
291 | err = data.pasteJSHand(rw, req)
292 | case "/about":
293 | err = data.aboutHand(rw, req)
294 | case "/about/authors":
295 | err = data.authorsHand(rw, req)
296 | case "/about/license":
297 | err = data.licenseHand(rw, req)
298 | case "/about/source_code":
299 | err = data.sourceCodePageHand(rw, req)
300 | case "/docs":
301 | err = data.docsHand(rw, req)
302 | case "/docs/apiv1":
303 | err = data.docsApiV1Hand(rw, req)
304 | case "/docs/api_libs":
305 | err = data.docsApiLibsHand(rw, req)
306 | // Pages
307 | case "/":
308 | err = data.newPasteHand(rw, req)
309 | case "/settings":
310 | err = data.settingsHand(rw, req)
311 | case "/terms":
312 | err = data.termsOfUseHand(rw, req)
313 | // Else
314 | default:
315 | if strings.HasPrefix(req.URL.Path, "/dl/") {
316 | err = data.dlHand(rw, req)
317 |
318 | } else if strings.HasPrefix(req.URL.Path, "/emb/") {
319 | err = data.embeddedHand(rw, req)
320 |
321 | } else if strings.HasPrefix(req.URL.Path, "/emb_help/") {
322 | err = data.embeddedHelpHand(rw, req)
323 |
324 | } else {
325 | err = data.getPasteHand(rw, req)
326 | }
327 | }
328 |
329 | // Log
330 | if err == nil {
331 | data.Log.HttpRequest(req, 200)
332 |
333 | } else {
334 | code, err := data.writeError(rw, req, err)
335 | if err != nil {
336 | data.Log.HttpError(req, err)
337 | } else {
338 | data.Log.HttpRequest(req, code)
339 | }
340 | }
341 | }
342 |
--------------------------------------------------------------------------------
/internal/web/web_about.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "html/template"
23 | "net/http"
24 | )
25 |
26 | type aboutTmpl struct {
27 | Version string
28 | TitleMaxLen int
29 | BodyMaxLen int
30 | MaxLifeTime int64
31 |
32 | ServerAbout string
33 | ServerRules string
34 | ServerTermsExist bool
35 |
36 | AdminName string
37 | AdminMail string
38 |
39 | Highlight func(string, string) template.HTML
40 | Translate func(string, ...interface{}) template.HTML
41 | }
42 |
43 | type aboutMinTmp struct {
44 | Translate func(string, ...interface{}) template.HTML
45 | }
46 |
47 | // Pattern: /about
48 | func (data *Data) aboutHand(rw http.ResponseWriter, req *http.Request) error {
49 | dataTmpl := aboutTmpl{
50 | Version: data.Version,
51 | TitleMaxLen: data.TitleMaxLen,
52 | BodyMaxLen: data.BodyMaxLen,
53 | MaxLifeTime: data.MaxLifeTime,
54 | ServerAbout: data.ServerAbout,
55 | ServerRules: data.ServerRules,
56 | ServerTermsExist: data.ServerTermsExist,
57 | AdminName: data.AdminName,
58 | AdminMail: data.AdminMail,
59 | Highlight: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight,
60 | Translate: data.Locales.findLocale(req).translate,
61 | }
62 |
63 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
64 | return data.About.Execute(rw, dataTmpl)
65 | }
66 |
67 | // Pattern: /about/authors
68 | func (data *Data) authorsHand(rw http.ResponseWriter, req *http.Request) error {
69 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
70 | return data.Authors.Execute(rw, aboutMinTmp{Translate: data.Locales.findLocale(req).translate})
71 | }
72 |
73 | // Pattern: /about/license
74 | func (data *Data) licenseHand(rw http.ResponseWriter, req *http.Request) error {
75 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
76 | return data.License.Execute(rw, aboutMinTmp{Translate: data.Locales.findLocale(req).translate})
77 | }
78 |
79 | // Pattern: /about/source_code
80 | func (data *Data) sourceCodePageHand(rw http.ResponseWriter, req *http.Request) error {
81 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
82 | return data.SourceCodePage.Execute(rw, aboutMinTmp{Translate: data.Locales.findLocale(req).translate})
83 | }
84 |
--------------------------------------------------------------------------------
/internal/web/web_dl.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | chromaLexers "github.com/alecthomas/chroma/v2/lexers"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | "net/http"
25 | "strings"
26 | "time"
27 | )
28 |
29 | // Pattern: /dl/
30 | func (data *Data) dlHand(rw http.ResponseWriter, req *http.Request) error {
31 | // Check rate limit
32 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req))
33 | if err != nil {
34 | return err
35 | }
36 |
37 | // Read DB
38 | pasteID := string([]rune(req.URL.Path)[4:])
39 |
40 | paste, err := data.DB.PasteGet(pasteID)
41 | if err != nil {
42 | return err
43 | }
44 |
45 | // If "one use" paste
46 | if paste.OneUse == true {
47 | // Delete paste
48 | err = data.DB.PasteDelete(pasteID)
49 | if err != nil {
50 | return err
51 | }
52 | }
53 |
54 | // Get create time
55 | createTime := time.Unix(paste.CreateTime, 0).UTC()
56 |
57 | // Get file name
58 | fileName := paste.ID
59 | if paste.Title != "" {
60 | fileName = paste.Title
61 | }
62 |
63 | // Get file extension
64 | fileExt := chromaLexers.Get(paste.Syntax).Config().Filenames[0][1:]
65 | if strings.HasSuffix(fileName, fileExt) == false {
66 | fileName = fileName + fileExt
67 | }
68 |
69 | // Write result
70 | rw.Header().Set("Content-Type", "application/octet-stream")
71 | rw.Header().Set("Content-Disposition", "attachment; filename="+fileName)
72 | rw.Header().Set("Content-Transfer-Encoding", "binary")
73 | rw.Header().Set("Expires", "0")
74 |
75 | http.ServeContent(rw, req, fileName, createTime, strings.NewReader(paste.Body))
76 |
77 | return nil
78 | }
79 |
--------------------------------------------------------------------------------
/internal/web/web_docs.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/netshare"
23 | "html/template"
24 | "net/http"
25 | )
26 |
27 | type docsTmpl struct {
28 | Highlight func(string, string) template.HTML
29 | Translate func(string, ...interface{}) template.HTML
30 | }
31 |
32 | type docsApiV1Tmpl struct {
33 | MaxLenAuthorAll int
34 |
35 | Highlight func(string, string) template.HTML
36 | Translate func(string, ...interface{}) template.HTML
37 | }
38 |
39 | // Pattern: /docs
40 | func (data *Data) docsHand(rw http.ResponseWriter, req *http.Request) error {
41 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
42 | return data.Docs.Execute(rw, docsTmpl{Translate: data.Locales.findLocale(req).translate})
43 | }
44 |
45 | // Pattern: /docs/apiv1
46 | func (data *Data) docsApiV1Hand(rw http.ResponseWriter, req *http.Request) error {
47 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
48 | return data.DocsApiV1.Execute(rw, docsApiV1Tmpl{
49 | MaxLenAuthorAll: netshare.MaxLengthAuthorAll,
50 | Translate: data.Locales.findLocale(req).translate,
51 | Highlight: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight,
52 | })
53 | }
54 |
55 | // Pattern: /docs/api_libs
56 | func (data *Data) docsApiLibsHand(rw http.ResponseWriter, req *http.Request) error {
57 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
58 | return data.DocsApiLibs.Execute(rw, docsTmpl{Translate: data.Locales.findLocale(req).translate})
59 | }
60 |
--------------------------------------------------------------------------------
/internal/web/web_embedded.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/netshare"
23 | "github.com/lcomrade/lenpaste/internal/storage"
24 | "html/template"
25 | "net/http"
26 | "time"
27 | )
28 |
29 | type embTmpl struct {
30 | ID string
31 | CreateTimeStr string
32 | DeleteTime int64
33 | OneUse bool
34 | Title string
35 | Body template.HTML
36 |
37 | ErrorNotFound bool
38 | Translate func(string, ...interface{}) template.HTML
39 | }
40 |
41 | // Pattern: /emb/
42 | func (data *Data) embeddedHand(rw http.ResponseWriter, req *http.Request) error {
43 | errorNotFound := false
44 |
45 | // Check rate limit
46 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req))
47 | if err != nil {
48 | return err
49 | }
50 |
51 | // Get paste ID
52 | pasteID := string([]rune(req.URL.Path)[5:])
53 |
54 | // Read DB
55 | paste, err := data.DB.PasteGet(pasteID)
56 | if err != nil {
57 | if err == storage.ErrNotFoundID {
58 | errorNotFound = true
59 |
60 | } else {
61 | return err
62 | }
63 | }
64 |
65 | // Prepare template data
66 | createTime := time.Unix(paste.CreateTime, 0).UTC()
67 |
68 | tmplData := embTmpl{
69 | ID: paste.ID,
70 | CreateTimeStr: createTime.Format("1 Jan, 2006"),
71 | DeleteTime: paste.DeleteTime,
72 | OneUse: paste.OneUse,
73 | Title: paste.Title,
74 | Body: tryHighlight(paste.Body, paste.Syntax, "monokai"),
75 |
76 | ErrorNotFound: errorNotFound,
77 | Translate: data.Locales.findLocale(req).translate,
78 | }
79 |
80 | // Show paste
81 | return data.EmbeddedPage.Execute(rw, tmplData)
82 | }
83 |
--------------------------------------------------------------------------------
/internal/web/web_embedded_help.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/netshare"
23 | "html/template"
24 | "net/http"
25 | )
26 |
27 | type embHelpTmpl struct {
28 | ID string
29 | DeleteTime int64
30 | OneUse bool
31 |
32 | Protocol string
33 | Host string
34 |
35 | Translate func(string, ...interface{}) template.HTML
36 | Highlight func(string, string) template.HTML
37 | }
38 |
39 | // Pattern: /emb_help/
40 | func (data *Data) embeddedHelpHand(rw http.ResponseWriter, req *http.Request) error {
41 | // Check rate limit
42 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req))
43 | if err != nil {
44 | return err
45 | }
46 |
47 | // Get paste ID
48 | pasteID := string([]rune(req.URL.Path)[10:])
49 |
50 | // Read DB
51 | paste, err := data.DB.PasteGet(pasteID)
52 | if err != nil {
53 | return err
54 | }
55 |
56 | // Show paste
57 | tmplData := embHelpTmpl{
58 | ID: paste.ID,
59 | DeleteTime: paste.DeleteTime,
60 | OneUse: paste.OneUse,
61 | Protocol: netshare.GetProtocol(req),
62 | Host: netshare.GetHost(req),
63 | Translate: data.Locales.findLocale(req).translate,
64 | Highlight: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight,
65 | }
66 |
67 | return data.EmbeddedHelpPage.Execute(rw, tmplData)
68 | }
69 |
--------------------------------------------------------------------------------
/internal/web/web_error.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "errors"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | "github.com/lcomrade/lenpaste/internal/storage"
25 | "html/template"
26 | "net/http"
27 | "strconv"
28 | )
29 |
30 | type errorTmpl struct {
31 | Code int
32 | AdminName string
33 | AdminMail string
34 | Translate func(string, ...interface{}) template.HTML
35 | }
36 |
37 | func (data *Data) writeError(rw http.ResponseWriter, req *http.Request, e error) (int, error) {
38 | errData := errorTmpl{
39 | Code: 0,
40 | AdminName: data.AdminName,
41 | AdminMail: data.AdminMail,
42 | Translate: data.Locales.findLocale(req).translate,
43 | }
44 |
45 | // Dectect error
46 | var eTmp429 *netshare.ErrTooManyRequests
47 |
48 | if e == netshare.ErrBadRequest {
49 | errData.Code = 400
50 |
51 | } else if e == netshare.ErrUnauthorized {
52 | errData.Code = 401
53 |
54 | } else if e == storage.ErrNotFoundID {
55 | errData.Code = 404
56 |
57 | } else if e == netshare.ErrNotFound {
58 | errData.Code = 404
59 |
60 | } else if e == netshare.ErrMethodNotAllowed {
61 | errData.Code = 405
62 |
63 | } else if e == netshare.ErrPayloadTooLarge {
64 | errData.Code = 413
65 |
66 | } else if errors.As(e, &eTmp429) {
67 | errData.Code = 429
68 | rw.Header().Set("Retry-After", strconv.FormatInt(eTmp429.RetryAfter, 10))
69 |
70 | } else {
71 | errData.Code = 500
72 | }
73 |
74 | // Write response header
75 | rw.Header().Set("Content-type", "text/html; charset=utf-8")
76 | rw.WriteHeader(errData.Code)
77 |
78 | // Render template
79 | err := data.ErrorPage.Execute(rw, errData)
80 | if err != nil {
81 | return 500, err
82 | }
83 |
84 | return errData.Code, nil
85 | }
86 |
--------------------------------------------------------------------------------
/internal/web/web_get.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/lineend"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | "html/template"
25 | "net/http"
26 | "time"
27 | )
28 |
29 | type pasteTmpl struct {
30 | ID string
31 | Title string
32 | Body template.HTML
33 | Syntax string
34 | CreateTime int64
35 | DeleteTime int64
36 | OneUse bool
37 |
38 | LineEnd string
39 | CreateTimeStr string
40 | DeleteTimeStr string
41 |
42 | Author string
43 | AuthorEmail string
44 | AuthorURL string
45 |
46 | Translate func(string, ...interface{}) template.HTML
47 | }
48 |
49 | type pasteContinueTmpl struct {
50 | ID string
51 | Translate func(string, ...interface{}) template.HTML
52 | }
53 |
54 | func (data *Data) getPasteHand(rw http.ResponseWriter, req *http.Request) error {
55 | // Check rate limit
56 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req))
57 | if err != nil {
58 | return err
59 | }
60 |
61 | // Get paste ID
62 | pasteID := string([]rune(req.URL.Path)[1:])
63 |
64 | // Read DB
65 | paste, err := data.DB.PasteGet(pasteID)
66 | if err != nil {
67 | return err
68 | }
69 |
70 | // If "one use" paste
71 | if paste.OneUse == true {
72 | // If continue button not pressed
73 | req.ParseForm()
74 |
75 | if req.PostForm.Get("oneUseContinue") != "true" {
76 | tmplData := pasteContinueTmpl{
77 | ID: paste.ID,
78 | Translate: data.Locales.findLocale(req).translate,
79 | }
80 |
81 | return data.PasteContinue.Execute(rw, tmplData)
82 | }
83 |
84 | // If continue button pressed delete paste
85 | err = data.DB.PasteDelete(pasteID)
86 | if err != nil {
87 | return err
88 | }
89 | }
90 |
91 | // Prepare template data
92 | createTime := time.Unix(paste.CreateTime, 0).UTC()
93 | deleteTime := time.Unix(paste.DeleteTime, 0).UTC()
94 |
95 | tmplData := pasteTmpl{
96 | ID: paste.ID,
97 | Title: paste.Title,
98 | Body: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight(paste.Body, paste.Syntax),
99 | Syntax: paste.Syntax,
100 | CreateTime: paste.CreateTime,
101 | DeleteTime: paste.DeleteTime,
102 | OneUse: paste.OneUse,
103 |
104 | CreateTimeStr: createTime.Format("Mon, 02 Jan 2006 15:04:05 -0700"),
105 | DeleteTimeStr: deleteTime.Format("Mon, 02 Jan 2006 15:04:05 -0700"),
106 |
107 | Author: paste.Author,
108 | AuthorEmail: paste.AuthorEmail,
109 | AuthorURL: paste.AuthorURL,
110 |
111 | Translate: data.Locales.findLocale(req).translate,
112 | }
113 |
114 | // Get body line end
115 | switch lineend.GetLineEnd(paste.Body) {
116 | case "\r\n":
117 | tmplData.LineEnd = "CRLF"
118 | case "\r":
119 | tmplData.LineEnd = "CR"
120 | default:
121 | tmplData.LineEnd = "LF"
122 | }
123 |
124 | // Show paste
125 | return data.PastePage.Execute(rw, tmplData)
126 | }
127 |
--------------------------------------------------------------------------------
/internal/web/web_highlight.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "bytes"
23 | "github.com/alecthomas/chroma/v2"
24 | "github.com/alecthomas/chroma/v2/formatters/html"
25 | "github.com/alecthomas/chroma/v2/lexers"
26 | "github.com/alecthomas/chroma/v2/styles"
27 | "html/template"
28 | )
29 |
30 | func tryHighlight(source string, lexer string, theme string) template.HTML {
31 | // Determine lexer
32 | l := lexers.Get(lexer)
33 | if l == nil {
34 | return template.HTML(source)
35 | }
36 |
37 | l = chroma.Coalesce(l)
38 |
39 | // Determine formatter
40 | f := html.New(
41 | html.Standalone(false),
42 | html.WithClasses(false),
43 | html.TabWidth(4),
44 | html.WithLineNumbers(true),
45 | html.WrapLongLines(true),
46 | )
47 |
48 | s := styles.Get(theme)
49 |
50 | it, err := l.Tokenise(nil, source)
51 | if err != nil {
52 | return template.HTML(source)
53 | }
54 |
55 | // Format
56 | var buf bytes.Buffer
57 |
58 | err = f.Format(&buf, s, it)
59 | if err != nil {
60 | return template.HTML(source)
61 | }
62 |
63 | return template.HTML(buf.String())
64 | }
65 |
--------------------------------------------------------------------------------
/internal/web/web_new.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/lenpasswd"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | "html/template"
25 | "net/http"
26 | )
27 |
28 | type createTmpl struct {
29 | TitleMaxLen int
30 | BodyMaxLen int
31 | AuthorAllMaxLen int
32 | MaxLifeTime int64
33 | UiDefaultLifeTime string
34 | Lexers []string
35 | ServerTermsExist bool
36 |
37 | AuthorDefault string
38 | AuthorEmailDefault string
39 | AuthorURLDefault string
40 |
41 | AuthOk bool
42 |
43 | Translate func(string, ...interface{}) template.HTML
44 | }
45 |
46 | func (data *Data) newPasteHand(rw http.ResponseWriter, req *http.Request) error {
47 | var err error
48 |
49 | // Check auth
50 | authOk := true
51 |
52 | if data.LenPasswdFile != "" {
53 | authOk = false
54 |
55 | user, pass, authExist := req.BasicAuth()
56 | if authExist == true {
57 | authOk, err = lenpasswd.LoadAndCheck(data.LenPasswdFile, user, pass)
58 | if err != nil {
59 | return err
60 | }
61 | }
62 |
63 | if authOk == false {
64 | rw.Header().Add("WWW-Authenticate", "Basic")
65 | rw.WriteHeader(401)
66 | }
67 | }
68 |
69 | // Create paste if need
70 | if req.Method == "POST" {
71 | pasteID, _, _, err := netshare.PasteAddFromForm(req, data.DB, data.RateLimitNew, data.TitleMaxLen, data.BodyMaxLen, data.MaxLifeTime, data.Lexers)
72 | if err != nil {
73 | return err
74 | }
75 |
76 | // Redirect to paste
77 | writeRedirect(rw, req, "/"+pasteID, 302)
78 | return nil
79 | }
80 |
81 | // Else show create page
82 | tmplData := createTmpl{
83 | TitleMaxLen: data.TitleMaxLen,
84 | BodyMaxLen: data.BodyMaxLen,
85 | AuthorAllMaxLen: netshare.MaxLengthAuthorAll,
86 | MaxLifeTime: data.MaxLifeTime,
87 | UiDefaultLifeTime: data.UiDefaultLifeTime,
88 | Lexers: data.Lexers,
89 | ServerTermsExist: data.ServerTermsExist,
90 | AuthorDefault: getCookie(req, "author"),
91 | AuthorEmailDefault: getCookie(req, "authorEmail"),
92 | AuthorURLDefault: getCookie(req, "authorURL"),
93 | AuthOk: authOk,
94 | Translate: data.Locales.findLocale(req).translate,
95 | }
96 |
97 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
98 |
99 | return data.Main.Execute(rw, tmplData)
100 | }
101 |
--------------------------------------------------------------------------------
/internal/web/web_other.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "crypto/md5"
23 | "fmt"
24 | "html/template"
25 | "net/http"
26 | "os"
27 | "strings"
28 | )
29 |
30 | type jsTmpl struct {
31 | Translate func(string, ...interface{}) template.HTML
32 | Theme func(string) string
33 | }
34 |
35 | func (data *Data) styleCSSHand(rw http.ResponseWriter, req *http.Request) error {
36 | rw.Header().Set("Content-Type", "text/css; charset=utf-8")
37 | return data.StyleCSS.Execute(rw, jsTmpl{
38 | Translate: data.Locales.findLocale(req).translate,
39 | Theme: data.Themes.findTheme(req, data.UiDefaultTheme).theme,
40 | })
41 | }
42 |
43 | func (data *Data) mainJSHand(rw http.ResponseWriter, req *http.Request) error {
44 | rw.Header().Set("Content-Type", "application/javascript; charset=utf-8")
45 | rw.Write(*data.MainJS)
46 | return nil
47 | }
48 |
49 | func (data *Data) codeJSHand(rw http.ResponseWriter, req *http.Request) error {
50 | rw.Header().Set("Content-Type", "application/javascript; charset=utf-8")
51 | return data.CodeJS.Execute(rw, jsTmpl{Translate: data.Locales.findLocale(req).translate})
52 | }
53 |
54 | func (data *Data) historyJSHand(rw http.ResponseWriter, req *http.Request) error {
55 | rw.Header().Set("Content-Type", "application/javascript; charset=utf-8")
56 | return data.HistoryJS.Execute(rw, jsTmpl{
57 | Translate: data.Locales.findLocale(req).translate,
58 | Theme: data.Themes.findTheme(req, data.UiDefaultTheme).theme,
59 | })
60 | }
61 |
62 | func (data *Data) pasteJSHand(rw http.ResponseWriter, req *http.Request) error {
63 | rw.Header().Set("Content-Type", "application/javascript; charset=utf-8")
64 | return data.PasteJS.Execute(rw, jsTmpl{Translate: data.Locales.findLocale(req).translate})
65 | }
66 |
67 | func init() {
68 | resp := "\u0045\u0072\u0072\u006f\u0072\u002e\u0020\u0059\u006f\u0075\u0020\u006d\u0061"
69 | resp += "\u0079\u0020\u0062\u0065\u0020\u0076\u0069\u006f\u006c\u0061\u0074\u0069\u006e"
70 | resp += "\u0067\u0020\u0074\u0068\u0065\u0020\u0041\u0047\u0050\u004c\u0020\u0076\u0033"
71 | resp += "\u0020\u006c\u0069\u0063\u0065\u006e\u0073\u0065\u0021"
72 |
73 | tmp, err := embFS.ReadFile("data/base.tmpl")
74 | if err != nil {
75 | println("error:", err.Error())
76 | os.Exit(1)
77 | }
78 |
79 | if strings.Contains(string(tmp), "{{ call .Translate `base.About` }} ") == false {
80 | println(resp)
81 | os.Exit(1)
82 | }
83 |
84 | tmp, err = embFS.ReadFile("data/about.tmpl")
85 | if err != nil {
86 | println("\u0065\u0072\u0072\u006f\u0072\u003a", err.Error())
87 | os.Exit(1)
88 | }
89 |
90 | if strings.Contains(string(tmp), "{{call .Translate `about.LenpasteAuthors` `/about/authors`}}
") == false {
91 | println(resp)
92 | os.Exit(1)
93 | }
94 |
95 | if strings.Contains(string(tmp), "/about/source_code") == false {
96 | println(resp)
97 | os.Exit(1)
98 | }
99 |
100 | if strings.Contains(string(tmp), "/about/license") == false {
101 | println(resp)
102 | os.Exit(1)
103 | }
104 |
105 | tmp, err = embFS.ReadFile("data/authors.tmpl")
106 | if err != nil {
107 | println("\u0065\u0072\u0072\u006f\u0072\u003a", err.Error())
108 | os.Exit(1)
109 | }
110 |
111 | if strings.Contains(string(tmp), "Leonid Maslakov (aka lcomrade) <root@lcomrade.su > - Core Developer. ") == false {
112 | println(resp)
113 | os.Exit(1)
114 | }
115 |
116 | tmp, err = embFS.ReadFile("data/source_code.tmpl")
117 | if err != nil {
118 | println("\u0065\u0072\u0072\u006f\u0072\u003a", err.Error())
119 | os.Exit(1)
120 | }
121 |
122 | if strings.Contains(string(tmp), "https://github.com/lcomrade/lenpaste") == false {
123 | println(resp)
124 | os.Exit(1)
125 | }
126 |
127 | tmp, err = embFS.ReadFile("data/license.tmpl")
128 | if err != nil {
129 | println("\u0065\u0072\u0072\u006f\u0072\u003a", err.Error())
130 | os.Exit(1)
131 | }
132 |
133 | if fmt.Sprintf("%x", md5.Sum(tmp)) != "a1d6dd7f4b7470be5197381b85ee4fb5" {
134 | println(resp)
135 | os.Exit(1)
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/internal/web/web_redirect.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "net/http"
23 | )
24 |
25 | func writeRedirect(rw http.ResponseWriter, req *http.Request, newURL string, code int) {
26 | if newURL == "" {
27 | newURL = "/"
28 | }
29 |
30 | if req.URL.RawQuery != "" {
31 | newURL = newURL + "?" + req.URL.RawQuery
32 | }
33 |
34 | rw.Header().Set("Location", newURL)
35 | rw.WriteHeader(code)
36 | }
37 |
--------------------------------------------------------------------------------
/internal/web/web_settings.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/lenpasswd"
23 | "github.com/lcomrade/lenpaste/internal/netshare"
24 | "html/template"
25 | "net/http"
26 | )
27 |
28 | const cookieMaxAge = 60 * 60 * 24 * 360 * 50 // 50 year
29 |
30 | type settingsTmpl struct {
31 | Language string
32 | LanguageSelector map[string]string
33 |
34 | Theme string
35 | ThemeSelector map[string]string
36 |
37 | AuthorAllMaxLen int
38 | Author string
39 | AuthorEmail string
40 | AuthorURL string
41 |
42 | AuthOk bool
43 |
44 | Translate func(string, ...interface{}) template.HTML
45 | }
46 |
47 | // Pattern: /settings
48 | func (data *Data) settingsHand(rw http.ResponseWriter, req *http.Request) error {
49 | var err error
50 |
51 | // Check auth
52 | authOk := true
53 |
54 | if data.LenPasswdFile != "" {
55 | authOk = false
56 |
57 | user, pass, authExist := req.BasicAuth()
58 | if authExist == true {
59 | authOk, err = lenpasswd.LoadAndCheck(data.LenPasswdFile, user, pass)
60 | if err != nil {
61 | return err
62 | }
63 | }
64 | }
65 |
66 | // Show settings page
67 | if req.Method != "POST" {
68 | // Prepare data
69 | dataTmpl := settingsTmpl{
70 | Language: getCookie(req, "lang"),
71 | LanguageSelector: data.LocalesList,
72 | Theme: getCookie(req, "theme"),
73 | ThemeSelector: data.ThemesList.getForLocale(req),
74 | AuthorAllMaxLen: netshare.MaxLengthAuthorAll,
75 | Author: getCookie(req, "author"),
76 | AuthorEmail: getCookie(req, "authorEmail"),
77 | AuthorURL: getCookie(req, "authorURL"),
78 | AuthOk: authOk,
79 | Translate: data.Locales.findLocale(req).translate,
80 | }
81 |
82 | if dataTmpl.Theme == "" {
83 | dataTmpl.Theme = data.UiDefaultTheme
84 | }
85 |
86 | // Show page
87 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
88 |
89 | err := data.Settings.Execute(rw, dataTmpl)
90 | if err != nil {
91 | data.writeError(rw, req, err)
92 | }
93 |
94 | // Else update settings
95 | } else {
96 | req.ParseForm()
97 |
98 | lang := req.PostForm.Get("lang")
99 | if lang == "" {
100 | http.SetCookie(rw, &http.Cookie{
101 | Name: "lang",
102 | Value: "",
103 | MaxAge: -1,
104 | })
105 |
106 | } else {
107 | http.SetCookie(rw, &http.Cookie{
108 | Name: "lang",
109 | Value: lang,
110 | MaxAge: cookieMaxAge,
111 | })
112 | }
113 |
114 | theme := req.PostForm.Get("theme")
115 | if theme == "" {
116 | http.SetCookie(rw, &http.Cookie{
117 | Name: "theme",
118 | Value: "",
119 | MaxAge: -1,
120 | })
121 |
122 | } else {
123 | http.SetCookie(rw, &http.Cookie{
124 | Name: "theme",
125 | Value: theme,
126 | MaxAge: cookieMaxAge,
127 | })
128 | }
129 |
130 | author := req.PostForm.Get("author")
131 | if author == "" {
132 | http.SetCookie(rw, &http.Cookie{
133 | Name: "author",
134 | Value: "",
135 | MaxAge: -1,
136 | })
137 |
138 | } else {
139 | http.SetCookie(rw, &http.Cookie{
140 | Name: "author",
141 | Value: author,
142 | MaxAge: cookieMaxAge,
143 | })
144 | }
145 |
146 | authorEmail := req.PostForm.Get("authorEmail")
147 | if authorEmail == "" {
148 | http.SetCookie(rw, &http.Cookie{
149 | Name: "authorEmail",
150 | Value: "",
151 | MaxAge: -1,
152 | })
153 |
154 | } else {
155 | http.SetCookie(rw, &http.Cookie{
156 | Name: "authorEmail",
157 | Value: authorEmail,
158 | MaxAge: cookieMaxAge,
159 | })
160 | }
161 |
162 | authorURL := req.PostForm.Get("authorURL")
163 | if authorURL == "" {
164 | http.SetCookie(rw, &http.Cookie{
165 | Name: "authorURL",
166 | Value: "",
167 | MaxAge: -1,
168 | })
169 |
170 | } else {
171 | http.SetCookie(rw, &http.Cookie{
172 | Name: "authorURL",
173 | Value: authorURL,
174 | MaxAge: cookieMaxAge,
175 | })
176 | }
177 |
178 | writeRedirect(rw, req, "/settings", 302)
179 | }
180 |
181 | return nil
182 | }
183 |
--------------------------------------------------------------------------------
/internal/web/web_sitemap.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "github.com/lcomrade/lenpaste/internal/netshare"
23 | "io"
24 | "net/http"
25 | )
26 |
27 | func (data *Data) robotsTxtHand(rw http.ResponseWriter, req *http.Request) error {
28 | // Generate robots.txt
29 | robotsTxt := "User-agent: *\nDisallow: /\n"
30 |
31 | if data.RobotsDisallow == false {
32 | proto := netshare.GetProtocol(req)
33 | host := netshare.GetHost(req)
34 |
35 | robotsTxt = "User-agent: *\nAllow: /\nSitemap: " + proto + "://" + host + "/sitemap.xml\n"
36 | }
37 |
38 | // Write response
39 | rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
40 | _, err := io.WriteString(rw, robotsTxt)
41 | if err != nil {
42 | return err
43 | }
44 |
45 | return nil
46 | }
47 |
48 | func (data *Data) sitemapHand(rw http.ResponseWriter, req *http.Request) error {
49 | if data.RobotsDisallow {
50 | return netshare.ErrNotFound
51 | }
52 |
53 | // Get protocol and host
54 | proto := netshare.GetProtocol(req)
55 | host := netshare.GetHost(req)
56 |
57 | // Generate sitemap.xml
58 | sitemapXML := ``
59 | sitemapXML = sitemapXML + "\n" + `` + "\n"
60 | sitemapXML = sitemapXML + "" + proto + "://" + host + "/" + " \n"
61 | sitemapXML = sitemapXML + "" + proto + "://" + host + "/about" + " \n"
62 | sitemapXML = sitemapXML + "" + proto + "://" + host + "/docs/apiv1" + " \n"
63 | sitemapXML = sitemapXML + "" + proto + "://" + host + "/docs/api_libs" + " \n"
64 | sitemapXML = sitemapXML + " \n"
65 |
66 | // Write response
67 | rw.Header().Set("Content-Type", "text/xml; charset=utf-8")
68 | _, err := io.WriteString(rw, sitemapXML)
69 | if err != nil {
70 | return err
71 | }
72 |
73 | return nil
74 | }
75 |
--------------------------------------------------------------------------------
/internal/web/web_terms.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "html/template"
23 | "net/http"
24 | )
25 |
26 | type termsOfUseTmpl struct {
27 | TermsOfUse string
28 |
29 | Highlight func(string, string) template.HTML
30 | Translate func(string, ...interface{}) template.HTML
31 | }
32 |
33 | // Pattern: /terms
34 | func (data *Data) termsOfUseHand(rw http.ResponseWriter, req *http.Request) error {
35 | rw.Header().Set("Content-Type", "text/html; charset=utf-8")
36 | return data.TermsOfUse.Execute(rw, termsOfUseTmpl{
37 | TermsOfUse: data.ServerTermsOfUse,
38 | Highlight: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight,
39 | Translate: data.Locales.findLocale(req).translate},
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/internal/web/web_themes.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "bytes"
23 | "errors"
24 | "fmt"
25 | "html/template"
26 | "io/fs"
27 | "net/http"
28 | "os"
29 | "path/filepath"
30 | "strings"
31 | )
32 |
33 | const baseTheme = "dark"
34 | const embThemesDir = "data/theme"
35 |
36 | type Theme map[string]string
37 | type Themes map[string]Theme
38 |
39 | type ThemesListPart map[string]string
40 | type ThemesList map[string]ThemesListPart
41 |
42 | func loadThemes(hostThemeDir string, localesList LocalesList, defaultTheme string) (Themes, ThemesList, error) {
43 | themes := make(Themes)
44 | themesList := make(ThemesList)
45 |
46 | for localeCode, _ := range localesList {
47 | themesList[localeCode] = make(ThemesListPart)
48 | }
49 |
50 | // Prepare load FS function
51 | loadThemesFromFS := func(f fs.FS, themeDir string) error {
52 | // Get theme files list
53 | files, err := fs.ReadDir(f, themeDir)
54 | if err != nil {
55 | return errors.New("web: failed read dir '" + themeDir + "': " + err.Error())
56 | }
57 |
58 | for _, fileInfo := range files {
59 | // Check file
60 | if fileInfo.IsDir() {
61 | continue
62 | }
63 |
64 | fileName := fileInfo.Name()
65 | if strings.HasSuffix(fileName, ".theme") == false {
66 | continue
67 | }
68 | themeCode := fileName[:len(fileName)-6]
69 |
70 | // Read file
71 | filePath := filepath.Join(themeDir, fileName)
72 | fileByte, err := fs.ReadFile(f, filePath)
73 | if err != nil {
74 | return errors.New("web: failed open file '" + filePath + "': " + err.Error())
75 | }
76 |
77 | fileStr := bytes.NewBuffer(fileByte).String()
78 |
79 | // Load theme
80 | theme, err := readKVCfg(fileStr)
81 | if err != nil {
82 | return errors.New("web: failed read file '" + filePath + "': " + err.Error())
83 | }
84 |
85 | _, themeExist := themes[themeCode]
86 | if themeExist {
87 | return errors.New("web: theme alredy loaded: " + filePath)
88 | }
89 |
90 | themes[themeCode] = Theme(theme)
91 | }
92 |
93 | return nil
94 | }
95 |
96 | // Load embed themes
97 | err := loadThemesFromFS(embFS, embThemesDir)
98 | if err != nil {
99 | return nil, nil, err
100 | }
101 |
102 | // Load external themes
103 | if hostThemeDir != "" {
104 | err = loadThemesFromFS(os.DirFS(hostThemeDir), ".")
105 | if err != nil {
106 | return nil, nil, err
107 | }
108 | }
109 |
110 | // Prepare themes list
111 | for key, val := range themes {
112 | // Get theme name
113 | themeName := val["theme.Name."+baseLocale]
114 | if themeName == "" {
115 | return nil, nil, errors.New("web: empty theme.Name." + baseLocale + " parameter in '" + key + "' theme")
116 | }
117 |
118 | // Append to the translation, if it is not complete
119 | defTheme := themes[baseTheme]
120 | defTotal := len(defTheme)
121 | curTotal := 0
122 | for defKey, defVal := range defTheme {
123 | _, isExist := val[defKey]
124 | if isExist {
125 | curTotal = curTotal + 1
126 | } else {
127 | if strings.HasPrefix(defKey, "theme.Name.") {
128 | val[defKey] = val["theme.Name."+baseLocale]
129 | } else {
130 | val[defKey] = defVal
131 | }
132 | }
133 | }
134 |
135 | if curTotal == 0 {
136 | return nil, nil, errors.New("web: theme '" + key + "' is empty")
137 | }
138 |
139 | // Add theme to themes list
140 | themeNameSuffix := ""
141 | if curTotal != defTotal {
142 | themeNameSuffix = fmt.Sprintf(" (%.2f%%)", (float32(curTotal)/float32(defTotal))*100)
143 | }
144 | themesList[baseLocale][key] = themeName + themeNameSuffix
145 |
146 | for localeCode, _ := range localesList {
147 | result, ok := val["theme.Name."+localeCode]
148 | if ok {
149 | themesList[localeCode][key] = result + themeNameSuffix
150 | } else {
151 | themesList[localeCode][key] = themeName + themeNameSuffix
152 | }
153 | }
154 | }
155 |
156 | // Check default theme exist
157 | _, ok := themes[defaultTheme]
158 | if ok == false {
159 | return nil, nil, errors.New("web: default theme '" + defaultTheme + "' not found")
160 | }
161 |
162 | return themes, themesList, nil
163 | }
164 |
165 | func (themesList ThemesList) getForLocale(req *http.Request) ThemesListPart {
166 | // Get theme by cookie
167 | langCookie := getCookie(req, "lang")
168 | if langCookie != "" {
169 | theme, ok := themesList[langCookie]
170 | if ok == true {
171 | return theme
172 | }
173 | }
174 |
175 | // Load default part theme
176 | theme, _ := themesList[baseLocale]
177 | return theme
178 | }
179 |
180 | func (themes Themes) findTheme(req *http.Request, defaultTheme string) Theme {
181 | // Get theme by cookie
182 | themeCookie := getCookie(req, "theme")
183 | if themeCookie != "" {
184 | theme, ok := themes[themeCookie]
185 | if ok == true {
186 | return theme
187 | }
188 | }
189 |
190 | // Load default theme
191 | theme, _ := themes[defaultTheme]
192 | return theme
193 | }
194 |
195 | func (theme Theme) theme(s string) string {
196 | for key, val := range theme {
197 | if key == s {
198 | return val
199 | }
200 | }
201 |
202 | panic(errors.New("web: theme: unknown theme key: " + s))
203 | }
204 |
205 | func (theme Theme) tryHighlight(source string, lexer string) template.HTML {
206 | return tryHighlight(source, lexer, theme.theme("highlight.Theme"))
207 | }
208 |
--------------------------------------------------------------------------------
/internal/web/web_translate.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package web
20 |
21 | import (
22 | "embed"
23 | "encoding/json"
24 | "errors"
25 | "fmt"
26 | "html/template"
27 | "net/http"
28 | "path/filepath"
29 | "strings"
30 | )
31 |
32 | const baseLocale = "en"
33 |
34 | type Locale map[string]string
35 | type Locales map[string]Locale
36 | type LocalesList map[string]string
37 |
38 | func loadLocales(f embed.FS, localeDir string) (Locales, LocalesList, error) {
39 | locales := make(Locales)
40 | localesList := make(LocalesList)
41 |
42 | // Get locale files list
43 | files, err := f.ReadDir(localeDir)
44 | if err != nil {
45 | return nil, nil, errors.New("web: failed read dir '" + localeDir + "': " + err.Error())
46 | }
47 |
48 | // Load locales
49 | for _, fileInfo := range files {
50 | // Check file
51 | if fileInfo.IsDir() {
52 | continue
53 | }
54 |
55 | fileName := fileInfo.Name()
56 | if strings.HasSuffix(fileName, ".json") == false {
57 | continue
58 | }
59 | localeCode := fileName[:len(fileName)-5]
60 |
61 | // Open and read file
62 | filePath := filepath.Join(localeDir, fileName)
63 | file, err := f.Open(filePath)
64 | if err != nil {
65 | return nil, nil, errors.New("web: failed open file '" + filePath + "': " + err.Error())
66 | }
67 | defer file.Close()
68 |
69 | var locale Locale
70 | err = json.NewDecoder(file).Decode(&locale)
71 | if err != nil {
72 | return nil, nil, errors.New("web: failed read file '" + filePath + "': " + err.Error())
73 | }
74 |
75 | locales[localeCode] = Locale(locale)
76 | }
77 |
78 | // Prepare locales list
79 | for key, val := range locales {
80 | // Get locale name
81 | localeName := val["locale.Name"]
82 | if localeName == "" {
83 | return nil, nil, errors.New("web: empty locale.Name parameter in '" + key + "' locale")
84 | }
85 |
86 | // Append to the translation, if it is not complete
87 | defLocale := locales[baseLocale]
88 | defTotal := len(defLocale)
89 | curTotal := 0
90 | for defKey, defVal := range defLocale {
91 | _, isExist := val[defKey]
92 | if isExist {
93 | curTotal = curTotal + 1
94 | } else {
95 | val[defKey] = defVal
96 | }
97 | }
98 |
99 | if curTotal == 0 {
100 | return nil, nil, errors.New("web: locale '" + key + "' is empty")
101 | }
102 |
103 | if curTotal == defTotal {
104 | localesList[key] = localeName
105 | } else {
106 | localesList[key] = localeName + fmt.Sprintf(" (%.2f%%)", (float32(curTotal)/float32(defTotal))*100)
107 | }
108 | }
109 |
110 | return locales, localesList, nil
111 | }
112 |
113 | func (locales Locales) findLocale(req *http.Request) Locale {
114 | // Get accept language by cookie
115 | langCookie := getCookie(req, "lang")
116 | if langCookie != "" {
117 | locale, ok := locales[langCookie]
118 | if ok == true {
119 | return locale
120 | }
121 | }
122 |
123 | // Get user Accepr-Languages list
124 | acceptLanguage := req.Header.Get("Accept-Language")
125 | acceptLanguage = strings.Replace(acceptLanguage, " ", "", -1)
126 |
127 | var langs []string
128 | for _, part := range strings.Split(acceptLanguage, ";") {
129 | for _, lang := range strings.Split(part, ",") {
130 | if strings.HasPrefix(lang, "q=") == false {
131 | langs = append(langs, lang)
132 | }
133 | }
134 | }
135 |
136 | // Search locale
137 | for _, lang := range langs {
138 | for localeCode, locale := range locales {
139 | if localeCode == lang {
140 | return locale
141 | }
142 | }
143 | }
144 |
145 | // Load default locale
146 | locale, _ := locales[baseLocale]
147 | return locale
148 | }
149 |
150 | func (locale Locale) translate(s string, a ...interface{}) template.HTML {
151 | for key, val := range locale {
152 | if key == s {
153 | return template.HTML(fmt.Sprintf(val, a...))
154 | }
155 | }
156 |
157 | panic(errors.New("web: translate: unknown locale key: " + s))
158 | }
159 |
--------------------------------------------------------------------------------
/tools/kvcfg-to-json/kvcfg.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package main
20 |
21 | import (
22 | "errors"
23 | "strconv"
24 | "strings"
25 | )
26 |
27 | func readKVCfg(data string) (map[string]string, error) {
28 | out := make(map[string]string)
29 |
30 | dataSplit := strings.Split(data, "\n")
31 | dataSplitLen := len(dataSplit)
32 |
33 | for num := 0; num < dataSplitLen; num++ {
34 | str := strings.TrimSpace(dataSplit[num])
35 |
36 | if str == "" || strings.HasPrefix(str, "//") {
37 | continue
38 | }
39 |
40 | strSplit := strings.SplitN(str, "=", 2)
41 | if len(strSplit) != 2 {
42 | return out, errors.New("error in line " + strconv.Itoa(num+1) + ": expected '=' delimiter")
43 | }
44 |
45 | key := strings.TrimSpace(strSplit[0])
46 | val := strings.TrimSpace(strSplit[1])
47 | val, isMultiline := multilineCheck(val)
48 |
49 | if isMultiline {
50 | num = num + 1
51 | for ; num < dataSplitLen; num++ {
52 | strPlus := strings.TrimSpace(dataSplit[num])
53 | strPlus, isMultilinePlus := multilineCheck(strPlus)
54 | val = val + strPlus
55 |
56 | if isMultilinePlus == false {
57 | break
58 | }
59 | }
60 | }
61 |
62 | _, exist := out[key]
63 | if exist {
64 | return out, errors.New("duplicate key: " + key)
65 | }
66 |
67 | out[key] = val
68 | }
69 |
70 | return out, nil
71 | }
72 |
73 | func multilineCheck(s string) (string, bool) {
74 | sLen := len(s)
75 |
76 | if sLen > 0 && s[sLen-1] == '\\' {
77 | if sLen > 1 && s[sLen-2] == '\\' {
78 | return s[:sLen-1], false
79 | }
80 |
81 | return s[:sLen-1], true
82 | }
83 |
84 | return s, false
85 | }
86 |
--------------------------------------------------------------------------------
/tools/kvcfg-to-json/main.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2021-2023 Leonid Maslakov.
2 |
3 | // This file is part of Lenpaste.
4 |
5 | // Lenpaste is free software: you can redistribute it
6 | // and/or modify it under the terms of the
7 | // GNU Affero Public License as published by the
8 | // Free Software Foundation, either version 3 of the License,
9 | // or (at your option) any later version.
10 |
11 | // Lenpaste is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 | // or FITNESS FOR A PARTICULAR PURPOSE.
14 | // See the GNU Affero Public License for more details.
15 |
16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste.
17 | // If not, see .
18 |
19 | package main
20 |
21 | import (
22 | "encoding/json"
23 | "fmt"
24 | "os"
25 | )
26 |
27 | func main() {
28 | if len(os.Args) != 3 {
29 | fmt.Fprintln(os.Stdout, "Usage:", os.Args[0], "[SRC] [DST]")
30 | os.Exit(1)
31 | }
32 |
33 | src := os.Args[1]
34 | dst := os.Args[2]
35 |
36 | // Read key-value config
37 | fileByte, err := os.ReadFile(src)
38 | if err != nil {
39 | panic(err)
40 | }
41 |
42 | cfg, err := readKVCfg(string(fileByte))
43 | if err != nil {
44 | panic(err)
45 | }
46 |
47 | // Save config as JSON
48 | cfgRaw, err := json.MarshalIndent(cfg, "", "\t")
49 | if err != nil {
50 | panic(err)
51 | }
52 |
53 | cfgRaw = append(cfgRaw, byte('\n'))
54 |
55 | err = os.WriteFile(dst, cfgRaw, os.ModePerm)
56 | if err != nil {
57 | panic(err)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tools/localefmt.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | readonly VERSION="1.0, 14.12.2022"
5 |
6 | printHelp(){
7 | echo "Usage: $0 [OPTION]... [FILES]"
8 | echo "Check locale files."
9 | echo ""
10 | echo " -s, --slien print only errors"
11 | echo " --rm-empty remove empty locale files"
12 | echo ""
13 | echo " -h, --help show help and exit"
14 | echo " -v, --version show version and exit"
15 | }
16 |
17 | # CLI args
18 | ARG_SLIENT=false
19 | ARG_RM_EMPTY=false
20 |
21 | if [ -z "$1" ]; then
22 | echo "error: not files specified" 1>&2
23 | exit 1
24 | fi
25 |
26 | while [ -n "$1" ]; do
27 | case "$1" in
28 | -s|--slient)
29 | ARG_SLIENT=true
30 | ;;
31 |
32 | --rm-empty)
33 | ARG_RM_EMPTY=true
34 | ;;
35 |
36 | -h|--help)
37 | printHelp
38 | exit 0
39 | ;;
40 |
41 | -v|--version)
42 | echo "$VERSION"
43 | exit 0
44 | ;;
45 |
46 | *)
47 | break
48 | ;;
49 | esac
50 |
51 | shift
52 | done
53 |
54 | # RUN
55 | #find ./ -type f -name "*.locale" | while read -r file; do
56 | # if ! grep -q -Ev '^$' "$file"; then
57 | # echo "$file"
58 | # fi
59 | #done
60 |
61 | while [ -n "$1" ]; do
62 | # If empty
63 | if ! grep -q -Ev '^$' "$1"; then
64 | # If need remove empty files
65 | if [ $ARG_RM_EMPTY = true ]; then
66 | rm "$1"
67 |
68 | if [ $ARG_SLIENT = false ]; then
69 | echo "remove empty file: $1"
70 | fi
71 |
72 | # If only print errors
73 | else
74 | if [ $ARG_SLIENT = false ]; then
75 | echo "empty: $1"
76 | fi
77 | fi
78 |
79 | # If not ok
80 | else
81 | if [ $ARG_SLIENT = false ]; then
82 | echo "ok $1"
83 | fi
84 | fi
85 |
86 | shift
87 | done
88 |
--------------------------------------------------------------------------------