Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1048e95b3 | ||
|
|
5b6d8de807 | ||
|
|
dd2cff6a13 | ||
|
|
68fe6eb55f | ||
|
|
4d5f9f269b | ||
|
|
6140d82eb2 | ||
|
|
f9d4837567 | ||
|
|
9fb3014229 | ||
|
|
2a9b29dd53 | ||
|
|
872b818323 | ||
|
|
4a7bee1d98 | ||
|
|
cbe793fb42 | ||
|
|
cbf9a82a92 | ||
|
|
6cd5c45a6c | ||
|
|
0bc85a6ff6 | ||
|
|
88bf4b7d70 | ||
|
|
26c5425646 | ||
|
|
15d2ab9644 | ||
|
|
50c56cb771 | ||
|
|
e65f71b958 | ||
|
|
a9b439602b | ||
|
|
d2027f5241 | ||
|
|
95966f6407 | ||
|
|
8df986e70a | ||
|
|
71a6d0b385 | ||
|
|
974d4aaf55 | ||
|
|
dc8d7d46cb | ||
|
|
c4ace9fc0c | ||
|
|
16b07ded66 | ||
|
|
c04705947a | ||
|
|
eadd65e247 | ||
|
|
4428f79046 | ||
|
|
bad6af87f7 | ||
|
|
f7ba43e5f1 | ||
|
|
661f042cb4 | ||
|
|
877765cda2 | ||
|
|
4bb0017060 | ||
|
|
e0423b8683 | ||
|
|
5a781693aa | ||
|
|
e5743185b0 | ||
|
|
1510aec17c | ||
|
|
01837b69e5 | ||
|
|
d02099844e | ||
|
|
6fcfaa12b7 | ||
|
|
6269c19881 | ||
|
|
1313dee829 | ||
|
|
3210de8f7b | ||
|
|
532573fb34 | ||
|
|
e1173bbcad | ||
|
|
e748951182 | ||
|
|
df8670752a | ||
|
|
654250f033 | ||
|
|
0050c65596 | ||
|
|
02db7a917d | ||
|
|
81c30e01f8 | ||
|
|
82c617806d | ||
|
|
e043dc2ac3 | ||
|
|
cbd73865bd | ||
|
|
884b5685eb | ||
|
|
3f14651653 | ||
|
|
310b797679 | ||
|
|
0594565143 | ||
|
|
d4c153662b | ||
|
|
f64b0b8831 | ||
|
|
94893b77dc | ||
|
|
30f46efa5b | ||
|
|
e0904603c6 | ||
|
|
d143932924 | ||
|
|
dcc7613409 | ||
|
|
d04d2097f8 | ||
|
|
ce678bf967 | ||
|
|
cd481d401d | ||
|
|
618c267370 | ||
|
|
307866f7f5 | ||
|
|
19e1616129 | ||
|
|
c6506b8905 | ||
|
|
676ae77456 | ||
|
|
b93fc48b73 | ||
|
|
4be664e78d | ||
|
|
62aaa35287 | ||
|
|
69b18d3536 | ||
|
|
efceed6f68 | ||
|
|
f4358173c7 | ||
|
|
7a71d47556 | ||
|
|
41d27e2a7f | ||
|
|
10e4686f1a | ||
|
|
eba447319d | ||
|
|
40a0d7ca62 | ||
|
|
c9b6f9272f | ||
|
|
1f2500f16f |
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: debug if needed
|
||||
run: if [[ "$DEBUG" == "true" ]]; then env; fi
|
||||
@@ -30,9 +30,9 @@ jobs:
|
||||
DEBUG: ${{secrets.DEBUG}}
|
||||
|
||||
- name: install go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.20"
|
||||
go-version: "1.21"
|
||||
|
||||
- name: test and build backend
|
||||
run: |
|
||||
@@ -54,13 +54,13 @@ jobs:
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
version: v1.58
|
||||
working-directory: backend/app
|
||||
|
||||
- name: golangci-lint on example directory
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
version: v1.58
|
||||
args: --config ../../.golangci.yml
|
||||
working-directory: backend/_example/memory_store
|
||||
|
||||
|
||||
@@ -29,14 +29,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
@@ -20,19 +20,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
node: [16.20.2]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
uses: pnpm/action-setup@v2.4.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
@@ -64,19 +64,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
node: [16.20.2]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
uses: pnpm/action-setup@v2.4.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
@@ -108,19 +108,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
node: [16.20.2]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
uses: pnpm/action-setup@v2.4.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
|
||||
@@ -20,19 +20,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
node: [16.20.2]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
uses: pnpm/action-setup@v2.4.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
@@ -64,19 +64,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
node: [16.20.2]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
uses: pnpm/action-setup@v2.4.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
@@ -108,19 +108,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
node: [16.20.2]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
uses: pnpm/action-setup@v2.4.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
@@ -155,17 +155,17 @@ jobs:
|
||||
CI_JOB_NUMBER: 1
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
uses: pnpm/action-setup@v2.4.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Check bundle size
|
||||
uses: andresz1/size-limit-action@dd31dce7dcc72a041fd3e49abf0502b13fc4ce05
|
||||
uses: andresz1/size-limit-action@7313b26c76b3666c1dc41e2ca05370e201a9b7de
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
directory: ./frontend/apps/remark42
|
||||
@@ -176,19 +176,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
node: [16.20.2]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
uses: pnpm/action-setup@v2.4.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
@@ -6,12 +6,14 @@ on:
|
||||
paths:
|
||||
- "frontend/apps/remark42/**"
|
||||
- "frontend/e2e/**"
|
||||
- "frontend/Dockerfile.e2e"
|
||||
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "frontend/apps/remark42/**"
|
||||
- "frontend/e2e/**"
|
||||
- "frontend/Dockerfile.e2e"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -20,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build & run containers
|
||||
id: tests
|
||||
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM node:16.15.1-alpine AS frontend-deps
|
||||
FROM --platform=$BUILDPLATFORM node:16.20-alpine AS frontend-deps
|
||||
|
||||
ARG SKIP_FRONTEND_TEST
|
||||
ARG SKIP_FRONTEND_BUILD
|
||||
@@ -45,7 +45,7 @@ RUN \
|
||||
echo 'Skip frontend build'; \
|
||||
fi
|
||||
|
||||
FROM umputun/baseimage:buildgo-v1.11.0 as build-backend
|
||||
FROM umputun/baseimage:buildgo-v1.13.0 as build-backend
|
||||
|
||||
ARG CI
|
||||
ARG GITHUB_REF
|
||||
@@ -54,15 +54,15 @@ ARG GIT_BRANCH
|
||||
ARG SKIP_BACKEND_TEST
|
||||
ARG BACKEND_TEST_TIMEOUT
|
||||
|
||||
# install gcc in order to be able to go test package with -race
|
||||
RUN apk --no-cache add gcc libc-dev
|
||||
|
||||
ADD backend /build/backend
|
||||
# to embed the frontend files statically into Remark42 binary
|
||||
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /build/backend/app/cmd/web/
|
||||
RUN find /build/backend/app/cmd/web/ -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \;
|
||||
WORKDIR /build/backend
|
||||
|
||||
# install gcc in order to be able to go test package with -race
|
||||
RUN apk --no-cache add gcc libc-dev
|
||||
|
||||
RUN echo go version: `go version`
|
||||
|
||||
# run tests
|
||||
@@ -81,7 +81,7 @@ RUN \
|
||||
echo "version=$version" && \
|
||||
go build -o remark42 -ldflags "-X main.revision=${version} -s -w" ./app
|
||||
|
||||
FROM umputun/baseimage:app-v1.11.0
|
||||
FROM umputun/baseimage:app-v1.13.0
|
||||
|
||||
ARG GITHUB_SHA
|
||||
|
||||
@@ -89,7 +89,7 @@ LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
|
||||
org.opencontainers.image.description="Remark42 comment engine" \
|
||||
org.opencontainers.image.documentation="https://remark42.com/docs/getting-started/" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.source="https://github.com/umputun/remark42.git" \
|
||||
org.opencontainers.image.source="https://github.com/umputun/remark42" \
|
||||
org.opencontainers.image.title="Remark42" \
|
||||
org.opencontainers.image.url="https://remark42.com/" \
|
||||
org.opencontainers.image.revision="${GITHUB_SHA}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.15.1-alpine AS frontend-deps
|
||||
FROM node:16.20.2-alpine AS frontend-deps
|
||||
|
||||
ENV CI=true
|
||||
|
||||
@@ -19,7 +19,7 @@ WORKDIR /srv/frontend/apps/remark42/
|
||||
COPY ./frontend/apps/remark42/ /srv/frontend/apps/remark42/
|
||||
RUN pnpm build
|
||||
|
||||
FROM umputun/baseimage:buildgo-v1.9.2 as build-backend
|
||||
FROM umputun/baseimage:buildgo-v1.13.0 as build-backend
|
||||
|
||||
ARG GITHUB_TOKEN
|
||||
ARG GITHUB_REF
|
||||
|
||||
@@ -39,15 +39,15 @@ race_test:
|
||||
cd backend/app && go test -race -timeout=60s -count 1 ./...
|
||||
|
||||
backend:
|
||||
docker-compose -f compose-dev-backend.yml build
|
||||
docker compose -f compose-dev-backend.yml build
|
||||
|
||||
frontend:
|
||||
docker-compose -f compose-dev-frontend.yml build
|
||||
docker compose -f compose-dev-frontend.yml build
|
||||
|
||||
rundev:
|
||||
SKIP_BACKEND_TEST=true SKIP_FRONTEND_TEST=true GITHUB_REF=$(GITHUB_REF) GITHUB_SHA=$(GITHUB_SHA) CI=true \
|
||||
docker-compose -f compose-private.yml build
|
||||
docker-compose -f compose-private.yml up
|
||||
docker compose -f compose-private.yml build
|
||||
docker compose -f compose-private.yml up
|
||||
|
||||
e2e:
|
||||
docker compose -f compose-e2e-test.yml up --build --quiet-pull --exit-code-from tests
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Remark42 is a self-hosted, lightweight and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles, or any other place where readers add comments.
|
||||
|
||||
* Social login via Google, Twitter, Facebook, Microsoft, GitHub, Yandex, Patreon and Telegram
|
||||
* Social login via Google, Facebook, Microsoft, GitHub, Apple, Yandex, Patreon and Telegram
|
||||
* Login via email
|
||||
* Optional anonymous access
|
||||
* Multi-level nested comments with both tree and plain presentations
|
||||
|
||||
+19
-22
@@ -1,15 +1,10 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
output:
|
||||
format: tab
|
||||
skip-dirs:
|
||||
- vendor
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
maligned:
|
||||
suggest-new: true
|
||||
enable:
|
||||
- shadow
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
@@ -31,29 +26,31 @@ linters-settings:
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- megacheck
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
- exportloopref
|
||||
- gochecknoinits
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- revive
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- stylecheck
|
||||
- gochecknoinits
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- nakedret
|
||||
- gosimple
|
||||
- prealloc
|
||||
fast: false
|
||||
disable-all: true
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- vendor
|
||||
exclude-rules:
|
||||
- text: "at least one file in a package should have a package comment"
|
||||
linters:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
FROM umputun/baseimage:buildgo-v1.9.2 as build-backend
|
||||
FROM umputun/baseimage:buildgo-v1.13.0 as build-backend
|
||||
|
||||
ADD backend /build/backend
|
||||
WORKDIR /build/backend/_example/memory_store
|
||||
|
||||
RUN go build -o /build/bin/memory_store -ldflags "-X main.revision=0.0.0 -s -w"
|
||||
|
||||
FROM umputun/baseimage:app-v1.9.2
|
||||
FROM umputun/baseimage:app-v1.13.0
|
||||
|
||||
ARG GITHUB_SHA
|
||||
|
||||
@@ -13,7 +13,7 @@ LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
|
||||
org.opencontainers.image.description="Remark42 comment engine example JRPC memory store" \
|
||||
org.opencontainers.image.documentation="https://github.com/umputun/remark42/tree/master/backend/_example/memory_store" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.source="https://github.com/umputun/remark42.git" \
|
||||
org.opencontainers.image.source="https://github.com/umputun/remark42" \
|
||||
org.opencontainers.image.title="Remark42 JRPC example memory store" \
|
||||
org.opencontainers.image.url="https://remark42.com/" \
|
||||
org.opencontainers.image.revision="${GITHUB_SHA}"
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
In order to run remark42 with memory_store copy provided `compose-dev-memstore.yml` to the root directory and run:
|
||||
|
||||
1. `docker-compose -f compose-dev-memstore.yml build`
|
||||
1. `docker-compose -f compose-dev-memstore.yml up`
|
||||
1. `docker compose -f compose-dev-memstore.yml build`
|
||||
1. `docker compose -f compose-dev-memstore.yml up`
|
||||
|
||||
As usual, demo site will run on http://127.0.0.1:8080/web/
|
||||
|
||||
note: in order to work with the latest (current) version of master `go.mod` uses replacement directive for the backend package. In real-life usage `replace github.com/umputun/remark42/backend => ../../` should not be used.
|
||||
note: in order to work with the latest (current) version of master `go.mod` uses replacement directive for the backend package. In real-life usage `replace github.com/umputun/remark42/backend => ../../` should not be used.
|
||||
|
||||
@@ -293,17 +293,17 @@ func (m *MemData) UserDetail(req engine.UserDetailRequest) ([]engine.UserDetailE
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if req.Update == "" { // read detail value, no update requested
|
||||
return m.getUserDetail(req)
|
||||
return m.getUserDetail(req), nil
|
||||
}
|
||||
|
||||
return m.setUserDetail(req)
|
||||
return m.setUserDetail(req), nil
|
||||
case engine.AllUserDetails:
|
||||
// list of all details returned in case request is a read request
|
||||
// (Update is not set) and does not have UserID or Detail set
|
||||
if req.Update == "" && req.UserID == "" { // read list of all details
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.listDetails(req.Locator)
|
||||
return m.listDetails(req.Locator), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported request with userdetail all")
|
||||
default:
|
||||
@@ -319,7 +319,8 @@ func (m *MemData) Delete(req engine.DeleteRequest) error {
|
||||
|
||||
switch {
|
||||
case req.UserDetail != "": // delete user detail
|
||||
return m.deleteUserDetail(req.Locator, req.UserID, req.UserDetail)
|
||||
m.deleteUserDetail(req.Locator, req.UserID, req.UserDetail)
|
||||
return nil
|
||||
case req.Locator.URL != "" && req.CommentID != "" && req.UserDetail == "": // delete comment
|
||||
return m.deleteComment(req.Locator, req.CommentID, req.DeleteMode)
|
||||
|
||||
@@ -332,7 +333,8 @@ func (m *MemData) Delete(req engine.DeleteRequest) error {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return m.deleteUserDetail(req.Locator, req.UserID, engine.AllUserDetails)
|
||||
m.deleteUserDetail(req.Locator, req.UserID, engine.AllUserDetails)
|
||||
return nil
|
||||
|
||||
case req.Locator.SiteID != "" && req.Locator.URL == "" && req.CommentID == "" && req.UserID == "" && req.UserDetail == "": // delete site
|
||||
if _, ok := m.posts[req.Locator.SiteID]; !ok {
|
||||
@@ -437,29 +439,29 @@ func (m *MemData) setFlag(req engine.FlagRequest) (res bool, err error) {
|
||||
|
||||
// getUserDetail returns UserDetailEntry with requested userDetail (omitting other details)
|
||||
// as an only element of the slice.
|
||||
func (m *MemData) getUserDetail(req engine.UserDetailRequest) ([]engine.UserDetailEntry, error) {
|
||||
func (m *MemData) getUserDetail(req engine.UserDetailRequest) []engine.UserDetailEntry {
|
||||
if meta, ok := m.metaUsers[req.UserID]; ok {
|
||||
if meta.SiteID != req.Locator.SiteID {
|
||||
return []engine.UserDetailEntry{}, nil
|
||||
return []engine.UserDetailEntry{}
|
||||
}
|
||||
switch req.Detail {
|
||||
case engine.UserEmail:
|
||||
return []engine.UserDetailEntry{{UserID: req.UserID, Email: meta.Details.Email}}, nil
|
||||
return []engine.UserDetailEntry{{UserID: req.UserID, Email: meta.Details.Email}}
|
||||
case engine.UserTelegram:
|
||||
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: meta.Details.Telegram}}, nil
|
||||
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: meta.Details.Telegram}}
|
||||
}
|
||||
}
|
||||
|
||||
return []engine.UserDetailEntry{}, nil
|
||||
return []engine.UserDetailEntry{}
|
||||
}
|
||||
|
||||
// setUserDetail sets requested userDetail, returning complete updated UserDetailEntry as an onlyIps
|
||||
// element of the slice in case of success
|
||||
func (m *MemData) setUserDetail(req engine.UserDetailRequest) ([]engine.UserDetailEntry, error) {
|
||||
func (m *MemData) setUserDetail(req engine.UserDetailRequest) []engine.UserDetailEntry {
|
||||
var entry metaUser
|
||||
if meta, ok := m.metaUsers[req.UserID]; ok {
|
||||
if meta.SiteID != req.Locator.SiteID {
|
||||
return []engine.UserDetailEntry{}, nil
|
||||
return []engine.UserDetailEntry{}
|
||||
}
|
||||
entry = meta
|
||||
}
|
||||
@@ -476,42 +478,42 @@ func (m *MemData) setUserDetail(req engine.UserDetailRequest) ([]engine.UserDeta
|
||||
case engine.UserEmail:
|
||||
entry.Details.Email = req.Update
|
||||
m.metaUsers[req.UserID] = entry
|
||||
return []engine.UserDetailEntry{{UserID: req.UserID, Email: req.Update}}, nil
|
||||
return []engine.UserDetailEntry{{UserID: req.UserID, Email: req.Update}}
|
||||
case engine.UserTelegram:
|
||||
entry.Details.Telegram = req.Update
|
||||
m.metaUsers[req.UserID] = entry
|
||||
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: req.Update}}, nil
|
||||
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: req.Update}}
|
||||
}
|
||||
|
||||
return []engine.UserDetailEntry{}, nil
|
||||
return []engine.UserDetailEntry{}
|
||||
}
|
||||
|
||||
// listDetails lists all available users details for given siteID
|
||||
func (m *MemData) listDetails(loc store.Locator) ([]engine.UserDetailEntry, error) {
|
||||
func (m *MemData) listDetails(loc store.Locator) []engine.UserDetailEntry {
|
||||
var res []engine.UserDetailEntry
|
||||
for _, u := range m.metaUsers {
|
||||
if u.SiteID == loc.SiteID {
|
||||
res = append(res, u.Details)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
return res
|
||||
}
|
||||
|
||||
// deleteUserDetail deletes requested UserDetail or whole UserDetailEntry,
|
||||
// deletion of the absent entry doesn't produce error.
|
||||
// Trying to delete user with wrong siteID doesn't to anything and doesn't produce error.
|
||||
func (m *MemData) deleteUserDetail(locator store.Locator, userID string, userDetail engine.UserDetail) error {
|
||||
func (m *MemData) deleteUserDetail(locator store.Locator, userID string, userDetail engine.UserDetail) {
|
||||
var entry metaUser
|
||||
if meta, ok := m.metaUsers[userID]; ok {
|
||||
if meta.SiteID != locator.SiteID {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
entry = meta
|
||||
}
|
||||
|
||||
if entry == (metaUser{}) || entry.Details == (engine.UserDetailEntry{}) {
|
||||
// absent entry means that we should not do anything
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
switch userDetail {
|
||||
@@ -529,7 +531,6 @@ func (m *MemData) deleteUserDetail(locator store.Locator, userID string, userDet
|
||||
}
|
||||
|
||||
m.metaUsers[userID] = entry
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MemData) get(loc store.Locator, commentID string) (store.Comment, error) {
|
||||
|
||||
@@ -547,12 +547,12 @@ func TestMemData_FlagListBlocked(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
blockedList := toBlocked(vv)
|
||||
var blockedIds = make([]string, len(blockedList))
|
||||
var blockedIDs = make([]string, len(blockedList))
|
||||
for i, x := range blockedList {
|
||||
blockedIds[i] = x.ID
|
||||
blockedIDs[i] = x.ID
|
||||
}
|
||||
require.Equal(t, 2, len(blockedList), b.metaUsers)
|
||||
assert.ElementsMatch(t, []string{"user1", "user2"}, blockedIds)
|
||||
assert.ElementsMatch(t, []string{"user1", "user2"}, blockedIDs)
|
||||
t.Logf("%+v", blockedList)
|
||||
|
||||
// check block expiration
|
||||
|
||||
@@ -70,6 +70,17 @@ func (m *MemImage) Load(id string) ([]byte, error) {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// Delete image by ID
|
||||
func (m *MemImage) Delete(id string) error {
|
||||
m.mu.Lock()
|
||||
// delete key from permanent and staging storage
|
||||
delete(m.images, id)
|
||||
delete(m.insertTime, id)
|
||||
delete(m.imagesStaging, id)
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit moves image from staging to permanent
|
||||
func (m *MemImage) Commit(id string) error {
|
||||
m.mu.RLock()
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
// gopher png for test, from https://golang.org/src/image/png/example_test.go
|
||||
const gopher = "iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2" +
|
||||
const rawGopher = "iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2" +
|
||||
"+OPbo9d7tsWyiyaZti6eWGAhISoIGKECEKCAiJJkYTiUgTMYSIosYYBBIUIxoSPIINEBDi2VhwkQrVsj1ESgu9doHWdrul7ba" +
|
||||
"73WNm3vOPtsseM9MdwvvrzTs+8/t95ze/33sI5BqiabU6m9En8oNjduLnAEDLUsQXFF8tQ5oxK3vmnNmDSMtrncks9Hhtt" +
|
||||
"/qeWZapHb1ha3UqYSWVl2ZmpWgaXMXGohQAvmeop3bjTRtv6SgaK/Pb9/bFzUrYslbFAmHPp+3WhAYdr+7GN/YnpN46Opv55VDs" +
|
||||
@@ -38,7 +38,9 @@ const gopher = "iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwU
|
||||
"1y98c3D27eppUjsZ6fql3jcd5rUe7+ZIlLNQny3Rd+E5Tct3WVhTM5RBCEdiEK0b6B+/ca2gYU393nFj/n1AygRQxPIUA043M42u85+z2S" +
|
||||
"nssKrPl8Mx76NL3E6eXc3be7OD+H4WHbJkKI8AU8irbITQjZ+0hQcPEgId/Fn/pl9crKH02+5o2b9T/eMx7pKoskYgAAAABJRU5ErkJggg=="
|
||||
|
||||
func gopherPNG() io.Reader { return base64.NewDecoder(base64.StdEncoding, strings.NewReader(gopher)) }
|
||||
func gopherPNG() io.Reader {
|
||||
return base64.NewDecoder(base64.StdEncoding, strings.NewReader(rawGopher))
|
||||
}
|
||||
|
||||
func TestMemImage_LoadAfterSave(t *testing.T) {
|
||||
svc := NewMemImageStore()
|
||||
@@ -57,7 +59,8 @@ func TestMemImage_LoadAfterSave(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, gopher, img)
|
||||
|
||||
svc.ResetCleanupTimer(id)
|
||||
err = svc.ResetCleanupTimer(id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = svc.Commit(id)
|
||||
assert.NoError(t, err)
|
||||
@@ -70,6 +73,26 @@ func TestMemImage_LoadAfterSave(t *testing.T) {
|
||||
assert.Equal(t, gopher, img)
|
||||
}
|
||||
|
||||
func TestMemImage_LoadAfterDelete(t *testing.T) {
|
||||
svc := NewMemImageStore()
|
||||
gopher, err := io.ReadAll(gopherPNG())
|
||||
assert.NoError(t, err)
|
||||
|
||||
id := "test_img"
|
||||
err = svc.Save(id, gopher)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = svc.Delete(id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
img, err := svc.Load(id)
|
||||
assert.EqualError(t, err, "image test_img not found")
|
||||
assert.Empty(t, img)
|
||||
|
||||
err = svc.ResetCleanupTimer(id)
|
||||
assert.EqualError(t, err, "image test_img not found")
|
||||
}
|
||||
|
||||
func TestMemImage_CommitFail(t *testing.T) {
|
||||
svc := NewMemImageStore()
|
||||
err := svc.Commit("test_id")
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
module github.com/umputun/remark42/memory_store
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/go-pkgz/jrpc v0.3.0
|
||||
github.com/go-pkgz/lgr v0.11.0
|
||||
github.com/jessevdk/go-flags v1.5.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/go-pkgz/lgr v0.11.1
|
||||
github.com/jessevdk/go-flags v1.6.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/umputun/remark42/backend v1.1000.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Depado/bfchroma/v2 v2.0.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.9.2 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.13.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/didip/tollbooth/v7 v7.0.1 // indirect
|
||||
github.com/didip/tollbooth/v7 v7.0.2 // indirect
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-pkgz/expirable-cache v1.0.0 // indirect
|
||||
github.com/go-pkgz/rest v1.17.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/go-pkgz/expirable-cache/v3 v3.0.0 // indirect
|
||||
github.com/go-pkgz/rest v1.19.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
golang.org/x/image v0.11.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
github.com/Depado/bfchroma/v2 v2.0.0 h1:IRpN9BPkNwEpR6w1ectIcNWOuhDSLx+8f1pn83fzxx8=
|
||||
github.com/Depado/bfchroma/v2 v2.0.0/go.mod h1:wFwW/Pw8Tnd0irzgO9Zxtxgzp3aPS8qBWlyadxujxmw=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
|
||||
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
|
||||
github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
@@ -17,45 +18,48 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/didip/tollbooth/v7 v7.0.0/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
|
||||
github.com/didip/tollbooth/v7 v7.0.1 h1:TkT4sBKoQoHQFPf7blQ54iHrZiTDnr8TceU+MulVAog=
|
||||
github.com/didip/tollbooth/v7 v7.0.1/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
|
||||
github.com/didip/tollbooth/v7 v7.0.2 h1:WYEfusYI6g64cN0qbZgekDrYfuYBZjUZd5+RlWi69p4=
|
||||
github.com/didip/tollbooth/v7 v7.0.2/go.mod h1:RtRYfEmFGX70+ike5kSndSvLtQ3+F2EAmTI4Un/VXNc=
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f h1:jtKwihcLmUC9BAhoJ9adCUqdSSZcOdH2KL7mPTUm2aw=
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f/go.mod h1:q9C80dnsuVRP2dAskjnXRNWdUJqtGgwG9wNrzt0019s=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||
github.com/go-pkgz/expirable-cache v1.0.0 h1:ns5+1hjY8hntGv8bPaQd9Gr7Jyo+Uw5SLyII40aQdtA=
|
||||
github.com/go-pkgz/expirable-cache v1.0.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||
github.com/go-pkgz/expirable-cache/v3 v3.0.0 h1:u3/gcu3sabLYiTCevoRKv+WzjIn5oo7P8XtiXBeRDLw=
|
||||
github.com/go-pkgz/expirable-cache/v3 v3.0.0/go.mod h1:2OQiDyEGQalYecLWmXprm3maPXeVb5/6/X7yRPYTzec=
|
||||
github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U=
|
||||
github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00=
|
||||
github.com/go-pkgz/lgr v0.11.0 h1:9XH5o+vj09L0sRWEswIGK1lJ6g07xVB4/Z28RV9Z+qM=
|
||||
github.com/go-pkgz/lgr v0.11.0/go.mod h1:4rdRmMSs4yGFjnUg0rSDbKx21LmFNZoH4y8OLl3qDnU=
|
||||
github.com/go-pkgz/lgr v0.11.1 h1:hXFhZcznehI6imLhEa379oMOKFz7TQUmisAqb3oLOSM=
|
||||
github.com/go-pkgz/lgr v0.11.1/go.mod h1:tgDF4RXQnBfIgJqjgkv0yOeTQ3F1yewWIZkpUhHnAkU=
|
||||
github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
|
||||
github.com/go-pkgz/rest v1.17.0 h1:LoBI/lDBMuqwWhOOkc6thM9NnwJO+K9nWvCOjZ7BAgE=
|
||||
github.com/go-pkgz/rest v1.17.0/go.mod h1:HHlLOt02NJc2sgffXBF6hYVMcRo4Gz3vjg43zTzN7VM=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/go-pkgz/rest v1.19.0 h1:FNMi5QX5dDIkuC+/e0r+CWsTuOTwUiWMRSA16Ou+9+A=
|
||||
github.com/go-pkgz/rest v1.19.0/go.mod h1:Po+W6zQzpMPP6XDGLdAN2aW7UKk1IyrLSb48Lp1N3oQ=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
|
||||
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -68,59 +72,56 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
|
||||
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -35,7 +35,6 @@ func (s *RPC) imgResetClnTimerHndl(id uint64, params json.RawMessage) (rr jrpc.R
|
||||
}
|
||||
err := s.img.ResetCleanupTimer(fileID)
|
||||
return jrpc.EncodeResponse(id, nil, err)
|
||||
|
||||
}
|
||||
|
||||
func (s *RPC) imgLoadHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
|
||||
@@ -47,6 +46,16 @@ func (s *RPC) imgLoadHndl(id uint64, params json.RawMessage) (rr jrpc.Response)
|
||||
return jrpc.EncodeResponse(id, value, err)
|
||||
}
|
||||
|
||||
func (s *RPC) imgDeleteHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
|
||||
var fileID string
|
||||
if err := json.Unmarshal(params, &fileID); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
err := s.img.Delete(fileID)
|
||||
return jrpc.EncodeResponse(id, nil, err)
|
||||
|
||||
}
|
||||
|
||||
func (s *RPC) imgCommitHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
|
||||
var fileID string
|
||||
if err := json.Unmarshal(params, &fileID); err != nil {
|
||||
|
||||
@@ -158,4 +158,9 @@ func TestRPC_imgInfoHndl(t *testing.T) {
|
||||
info, err = ri.Info()
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, info.FirstStagingImageTS.IsZero())
|
||||
|
||||
err = ri.Delete("test_img")
|
||||
assert.NoError(t, err)
|
||||
_, err = ri.Load("test_img")
|
||||
assert.EqualError(t, err, "image test_img not found")
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ func (s *RPC) addHandlers() {
|
||||
"save_with_id": s.imgSaveWithIDHndl,
|
||||
"reset_cleanup_timer": s.imgResetClnTimerHndl,
|
||||
"load": s.imgLoadHndl,
|
||||
"delete": s.imgDeleteHndl,
|
||||
"commit": s.imgCommitHndl,
|
||||
"cleanup": s.imgCleanupHndl,
|
||||
"info": s.imgInfoHndl,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
@@ -16,6 +18,10 @@ func TestBackup_Execute(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, r.URL.Path, "/api/v1/admin/export")
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
t.Logf("Authorization header: %+v", r.Header.Get("Authorization"))
|
||||
auth, err := base64.StdEncoding.DecodeString(strings.Split(r.Header.Get("Authorization"), " ")[1])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "admin:secret", string(auth))
|
||||
fmt.Fprint(w, "blah\nblah2\n12345678\n")
|
||||
}))
|
||||
defer ts.Close()
|
||||
@@ -34,6 +40,28 @@ func TestBackup_Execute(t *testing.T) {
|
||||
assert.Equal(t, "blah\nblah2\n12345678\n", string(data))
|
||||
}
|
||||
|
||||
func TestBackup_ExecuteNoPassword(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, r.URL.Path, "/api/v1/admin/export")
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
t.Logf("Authorization: %+v", r.Header.Get("Authorization"))
|
||||
auth, err := base64.StdEncoding.DecodeString(strings.Split(r.Header.Get("Authorization"), " ")[1])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "admin:", string(auth))
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cmd := BackupCommand{}
|
||||
cmd.SetCommon(CommonOpts{RemarkURL: ts.URL})
|
||||
p := flags.NewParser(&cmd, flags.Default)
|
||||
_, err := p.ParseArgs([]string{"--site=remark", "--path=/tmp", "--file={{.SITE}}-test.export"})
|
||||
require.NoError(t, err)
|
||||
err = cmd.Execute(nil)
|
||||
assert.EqualError(t, err, "error response \"401 Unauthorized\", ensure you have set ADMIN_PASSWD and provided it to the command you're running: Unauthorized")
|
||||
}
|
||||
|
||||
func TestBackup_ExecuteFailedStatus(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, r.URL.Path, "/api/v1/admin/export")
|
||||
|
||||
@@ -195,7 +195,7 @@ func cleanupRoutes(t *testing.T, r *chi.Mux, c *cleanedComments) {
|
||||
require.NoError(t, json.NewEncoder(w).Encode(commentsWithInfo))
|
||||
})
|
||||
|
||||
r.HandleFunc("/api/v1/admin/comment/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.HandleFunc("/api/v1/admin/comment/{id}", func(_ http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "DELETE", r.Method)
|
||||
t.Log("delete ", r.URL.Path)
|
||||
c.lock.Lock()
|
||||
@@ -203,7 +203,7 @@ func cleanupRoutes(t *testing.T, r *chi.Mux, c *cleanedComments) {
|
||||
c.lock.Unlock()
|
||||
})
|
||||
|
||||
r.HandleFunc("/api/v1/admin/title/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.HandleFunc("/api/v1/admin/title/{id}", func(_ http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "PUT", r.Method)
|
||||
t.Log("title for ", r.URL.Path)
|
||||
c.lock.Lock()
|
||||
|
||||
@@ -115,6 +115,9 @@ func responseError(resp *http.Response) error {
|
||||
if e != nil {
|
||||
body = []byte("")
|
||||
}
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
return fmt.Errorf("error response %q, ensure you have set ADMIN_PASSWD and provided it to the command you're running: %s", resp.Status, body)
|
||||
}
|
||||
return fmt.Errorf("error response %q, %s", resp.Status, body)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +20,10 @@ func TestImport_Execute(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, r.URL.Path, "/api/v1/admin/import")
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
t.Logf("Authorization header: %+v", r.Header.Get("Authorization"))
|
||||
auth, err := base64.StdEncoding.DecodeString(strings.Split(r.Header.Get("Authorization"), " ")[1])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "admin:secret", string(auth))
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah\nblah2\n12345678\n", string(body))
|
||||
@@ -46,6 +52,42 @@ func TestImport_Execute(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestImport_ExecuteNoPassword(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, r.URL.Path, "/api/v1/admin/import")
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
t.Logf("Authorization header: %+v", r.Header.Get("Authorization"))
|
||||
auth, err := base64.StdEncoding.DecodeString(strings.Split(r.Header.Get("Authorization"), " ")[1])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "admin:", string(auth))
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah\nblah2\n12345678\n", string(body))
|
||||
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cmd := ImportCommand{}
|
||||
cmd.SetCommon(CommonOpts{RemarkURL: ts.URL})
|
||||
|
||||
p := flags.NewParser(&cmd, flags.Default)
|
||||
_, err := p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt"})
|
||||
require.NoError(t, err)
|
||||
err = cmd.Execute(nil)
|
||||
assert.EqualError(t, err, "error response \"401 Unauthorized\", ensure you have set ADMIN_PASSWD and provided it to the command you're running: Unauthorized")
|
||||
|
||||
cmd = ImportCommand{}
|
||||
cmd.SetCommon(CommonOpts{RemarkURL: ts.URL})
|
||||
|
||||
p = flags.NewParser(&cmd, flags.Default)
|
||||
_, err = p.ParseArgs([]string{"--site=remark", "--file=testdata/import.txt.gz"})
|
||||
require.NoError(t, err)
|
||||
err = cmd.Execute(nil)
|
||||
assert.EqualError(t, err, "error response \"401 Unauthorized\", ensure you have set ADMIN_PASSWD and provided it to the command you're running: Unauthorized")
|
||||
}
|
||||
|
||||
func TestImport_ExecuteFailed(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, r.URL.Path, "/api/v1/admin/import")
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
@@ -16,6 +19,10 @@ func TestRemap_Execute(t *testing.T) {
|
||||
assert.Equal(t, r.URL.Path, "/api/v1/admin/remap")
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "remark", r.URL.Query().Get("site"))
|
||||
t.Logf("Authorization header: %+v", r.Header.Get("Authorization"))
|
||||
auth, err := base64.StdEncoding.DecodeString(strings.Split(r.Header.Get("Authorization"), " ")[1])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "admin:secret", string(auth))
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "http://oldsite.com* https://newsite.com*\nhttp://oldsite.com/from-old-page/1 https://newsite.com/to-new-page/1", string(body))
|
||||
@@ -33,3 +40,31 @@ func TestRemap_Execute(t *testing.T) {
|
||||
err = cmd.Execute(nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRemap_ExecuteNoPassword(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, r.URL.Path, "/api/v1/admin/remap")
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "remark", r.URL.Query().Get("site"))
|
||||
t.Logf("Authorization header: %+v", r.Header.Get("Authorization"))
|
||||
auth, err := base64.StdEncoding.DecodeString(strings.Split(r.Header.Get("Authorization"), " ")[1])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "admin:", string(auth))
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "http://oldsite.com* https://newsite.com*\nhttp://oldsite.com/from-old-page/1 https://newsite.com/to-new-page/1", string(body))
|
||||
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cmd := RemapCommand{}
|
||||
cmd.SetCommon(CommonOpts{RemarkURL: ts.URL})
|
||||
|
||||
p := flags.NewParser(&cmd, flags.Default)
|
||||
_, err := p.ParseArgs([]string{"--site=remark", "--file=testdata/remap_urls.txt"})
|
||||
require.NoError(t, err)
|
||||
err = cmd.Execute(nil)
|
||||
assert.EqualError(t, err, "error response \"401 Unauthorized\", ensure you have set ADMIN_PASSWD and provided it to the command you're running: Unauthorized")
|
||||
}
|
||||
|
||||
+164
-106
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/jrpc"
|
||||
"github.com/go-pkgz/lcw/eventbus"
|
||||
"github.com/go-pkgz/lcw/v2/eventbus"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
ntf "github.com/go-pkgz/notify"
|
||||
"github.com/golang-jwt/jwt"
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
"github.com/go-pkgz/auth/provider"
|
||||
"github.com/go-pkgz/auth/provider/sender"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
|
||||
"github.com/umputun/remark42/backend/app/migrator"
|
||||
"github.com/umputun/remark42/backend/app/notify"
|
||||
@@ -58,34 +59,36 @@ type ServerCommand struct {
|
||||
SSL SSLGroup `group:"ssl" namespace:"ssl" env-namespace:"SSL"`
|
||||
ImageProxy ImageProxyGroup `group:"image-proxy" namespace:"image-proxy" env-namespace:"IMAGE_PROXY"`
|
||||
|
||||
Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","`
|
||||
AnonymousVote bool `long:"anon-vote" env:"ANON_VOTE" description:"enable anonymous votes (works only with VOTES_IP enabled)"`
|
||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"`
|
||||
BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"`
|
||||
MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"`
|
||||
LegacyImageProxy bool `long:"img-proxy" env:"IMG_PROXY" description:"[deprecated, use image-proxy.http2https] enable image proxy"`
|
||||
MaxCommentSize int `long:"max-comment" env:"MAX_COMMENT_SIZE" default:"2048" description:"max comment size"`
|
||||
MaxVotes int `long:"max-votes" env:"MAX_VOTES" default:"-1" description:"maximum number of votes per comment"`
|
||||
RestrictVoteIP bool `long:"votes-ip" env:"VOTES_IP" description:"restrict votes from the same ip"`
|
||||
DurationVoteIP time.Duration `long:"votes-ip-time" env:"VOTES_IP_TIME" default:"5m" description:"same ip vote duration"`
|
||||
LowScore int `long:"low-score" env:"LOW_SCORE" default:"-5" description:"low score threshold"`
|
||||
CriticalScore int `long:"critical-score" env:"CRITICAL_SCORE" default:"-10" description:"critical score threshold"`
|
||||
PositiveScore bool `long:"positive-score" env:"POSITIVE_SCORE" description:"enable positive score only"`
|
||||
ReadOnlyAge int `long:"read-age" env:"READONLY_AGE" default:"0" description:"read-only age of comments, days"`
|
||||
EditDuration time.Duration `long:"edit-time" env:"EDIT_TIME" default:"5m" description:"edit window"`
|
||||
AdminEdit bool `long:"admin-edit" env:"ADMIN_EDIT" description:"unlimited edit for admins"`
|
||||
Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"`
|
||||
Address string `long:"address" env:"REMARK_ADDRESS" default:"" description:"listening address"`
|
||||
WebRoot string `long:"web-root" env:"REMARK_WEB_ROOT" default:"./web" description:"web root directory"`
|
||||
UpdateLimit float64 `long:"update-limit" env:"UPDATE_LIMIT" default:"0.5" description:"updates/sec limit"`
|
||||
RestrictedWords []string `long:"restricted-words" env:"RESTRICTED_WORDS" description:"words prohibited to use in comments" env-delim:","`
|
||||
RestrictedNames []string `long:"restricted-names" env:"RESTRICTED_NAMES" description:"names prohibited to use by user" env-delim:","`
|
||||
EnableEmoji bool `long:"emoji" env:"EMOJI" description:"enable emoji"`
|
||||
SimpleView bool `long:"simple-view" env:"SIMPLE_VIEW" description:"minimal comment editor mode"`
|
||||
ProxyCORS bool `long:"proxy-cors" env:"PROXY_CORS" description:"disable internal CORS and delegate it to proxy"`
|
||||
AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments" env-delim:","`
|
||||
SubscribersOnly bool `long:"subscribers-only" env:"SUBSCRIBERS_ONLY" description:"enable commenting only for Patreon subscribers"`
|
||||
DisableSignature bool `long:"disable-signature" env:"DISABLE_SIGNATURE" description:"disable server signature in headers"`
|
||||
Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","`
|
||||
AnonymousVote bool `long:"anon-vote" env:"ANON_VOTE" description:"enable anonymous votes (works only with VOTES_IP enabled)"`
|
||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"`
|
||||
BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"`
|
||||
MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"`
|
||||
LegacyImageProxy bool `long:"img-proxy" env:"IMG_PROXY" description:"[deprecated, use image-proxy.http2https] enable image proxy"`
|
||||
MinCommentSize int `long:"min-comment" env:"MIN_COMMENT_SIZE" default:"0" description:"min comment size"`
|
||||
MaxCommentSize int `long:"max-comment" env:"MAX_COMMENT_SIZE" default:"2048" description:"max comment size"`
|
||||
MaxVotes int `long:"max-votes" env:"MAX_VOTES" default:"-1" description:"maximum number of votes per comment"`
|
||||
RestrictVoteIP bool `long:"votes-ip" env:"VOTES_IP" description:"restrict votes from the same ip"`
|
||||
DurationVoteIP time.Duration `long:"votes-ip-time" env:"VOTES_IP_TIME" default:"5m" description:"same ip vote duration"`
|
||||
LowScore int `long:"low-score" env:"LOW_SCORE" default:"-5" description:"low score threshold"`
|
||||
CriticalScore int `long:"critical-score" env:"CRITICAL_SCORE" default:"-10" description:"critical score threshold"`
|
||||
PositiveScore bool `long:"positive-score" env:"POSITIVE_SCORE" description:"enable positive score only"`
|
||||
ReadOnlyAge int `long:"read-age" env:"READONLY_AGE" default:"0" description:"read-only age of comments, days"`
|
||||
EditDuration time.Duration `long:"edit-time" env:"EDIT_TIME" default:"5m" description:"edit window"`
|
||||
AdminEdit bool `long:"admin-edit" env:"ADMIN_EDIT" description:"unlimited edit for admins"`
|
||||
Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"`
|
||||
Address string `long:"address" env:"REMARK_ADDRESS" default:"" description:"listening address"`
|
||||
WebRoot string `long:"web-root" env:"REMARK_WEB_ROOT" default:"./web" description:"web root directory"`
|
||||
UpdateLimit float64 `long:"update-limit" env:"UPDATE_LIMIT" default:"0.5" description:"updates/sec limit"`
|
||||
RestrictedWords []string `long:"restricted-words" env:"RESTRICTED_WORDS" description:"words prohibited to use in comments" env-delim:","`
|
||||
RestrictedNames []string `long:"restricted-names" env:"RESTRICTED_NAMES" description:"names prohibited to use by user" env-delim:","`
|
||||
EnableEmoji bool `long:"emoji" env:"EMOJI" description:"enable emoji"`
|
||||
SimpleView bool `long:"simple-view" env:"SIMPLE_VIEW" description:"minimal comment editor mode"`
|
||||
ProxyCORS bool `long:"proxy-cors" env:"PROXY_CORS" description:"disable internal CORS and delegate it to proxy"`
|
||||
AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments via CSP 'frame-ancestors''" env-delim:","`
|
||||
SubscribersOnly bool `long:"subscribers-only" env:"SUBSCRIBERS_ONLY" description:"enable commenting only for Patreon subscribers"`
|
||||
DisableSignature bool `long:"disable-signature" env:"DISABLE_SIGNATURE" description:"disable server signature in headers"`
|
||||
DisableFancyTextFormatting bool `long:"disable-fancy-text-formatting" env:"DISABLE_FANCY_TEXT_FORMATTING" description:"disable fancy comments text formatting (replacement of quotes, dashes, fractions, etc)"`
|
||||
|
||||
Auth struct {
|
||||
TTL struct {
|
||||
@@ -102,7 +105,7 @@ type ServerCommand struct {
|
||||
Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"`
|
||||
Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"`
|
||||
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
|
||||
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"`
|
||||
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"[deprecated, doesn't work] Twitter OAuth"`
|
||||
Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"`
|
||||
Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"`
|
||||
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
|
||||
@@ -136,7 +139,7 @@ type ImageProxyGroup struct {
|
||||
|
||||
// AppleGroup defines options for Apple auth params
|
||||
type AppleGroup struct {
|
||||
CID string `long:"cid" env:"CID" description:"Apple client ID"`
|
||||
CID string `long:"cid" env:"CID" description:"Apple client ID (App ID or Services ID)"`
|
||||
TID string `long:"tid" env:"TID" description:"Apple service ID"`
|
||||
KID string `long:"kid" env:"KID" description:"Private key ID"`
|
||||
PrivateKeyFilePath string `long:"private-key-filepath" env:"PRIVATE_KEY_FILEPATH" description:"Private key file location" default:"/srv/var/apple.p8"`
|
||||
@@ -217,14 +220,15 @@ type TelegramGroup struct {
|
||||
|
||||
// SMTPGroup defines options for SMTP server connection, used in auth and notify modules
|
||||
type SMTPGroup struct {
|
||||
Host string `long:"host" env:"HOST" description:"SMTP host"`
|
||||
Port int `long:"port" env:"PORT" description:"SMTP port"`
|
||||
Username string `long:"username" env:"USERNAME" description:"SMTP user name"`
|
||||
Password string `long:"password" env:"PASSWORD" description:"SMTP password"`
|
||||
TLS bool `long:"tls" env:"TLS" description:"enable TLS"`
|
||||
LoginAuth bool `long:"login_auth" env:"LOGIN_AUTH" description:"enable LOGIN auth instead of PLAIN"`
|
||||
StartTLS bool `long:"starttls" env:"STARTTLS" description:"enable StartTLS"`
|
||||
TimeOut time.Duration `long:"timeout" env:"TIMEOUT" default:"10s" description:"SMTP TCP connection timeout"`
|
||||
Host string `long:"host" env:"HOST" description:"SMTP host"`
|
||||
Port int `long:"port" env:"PORT" description:"SMTP port"`
|
||||
Username string `long:"username" env:"USERNAME" description:"SMTP user name"`
|
||||
Password string `long:"password" env:"PASSWORD" description:"SMTP password"`
|
||||
TLS bool `long:"tls" env:"TLS" description:"enable TLS"`
|
||||
InsecureSkipVerify bool `long:"insecure_skip_verify" env:"INSECURE_SKIP_VERIFY" description:"skip certificate verification"`
|
||||
LoginAuth bool `long:"login_auth" env:"LOGIN_AUTH" description:"enable LOGIN auth instead of PLAIN"`
|
||||
StartTLS bool `long:"starttls" env:"STARTTLS" description:"enable StartTLS"`
|
||||
TimeOut time.Duration `long:"timeout" env:"TIMEOUT" default:"10s" description:"SMTP TCP connection timeout"`
|
||||
}
|
||||
|
||||
// NotifyGroup defines options for notification
|
||||
@@ -309,6 +313,7 @@ func (s *ServerCommand) Execute(_ []string) error {
|
||||
log.Printf("[INFO] start server on port %s:%d", s.Address, s.Port)
|
||||
resetEnv(
|
||||
"SECRET",
|
||||
"AUTH_APPLE_KID",
|
||||
"AUTH_GOOGLE_CSEC",
|
||||
"AUTH_GITHUB_CSEC",
|
||||
"AUTH_FACEBOOK_CSEC",
|
||||
@@ -402,6 +407,12 @@ func (s *ServerCommand) HandleDeprecatedFlags() (result []DeprecatedFlag) {
|
||||
if s.Notify.Telegram.API != "https://api.telegram.org/bot" {
|
||||
result = append(result, DeprecatedFlag{Old: "notify.telegram.api", Version: "1.9"})
|
||||
}
|
||||
if s.Auth.Twitter.CID != "" {
|
||||
result = append(result, DeprecatedFlag{Old: "auth.twitter.cid", Version: "1.14"})
|
||||
}
|
||||
if s.Auth.Twitter.CSEC != "" {
|
||||
result = append(result, DeprecatedFlag{Old: "auth.twitter.csec", Version: "1.14"})
|
||||
}
|
||||
return append(result, s.findDeprecatedFlagsCollisions()...)
|
||||
}
|
||||
|
||||
@@ -500,11 +511,12 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
||||
EditDuration: s.EditDuration,
|
||||
AdminEdits: s.AdminEdit,
|
||||
AdminStore: adminStore,
|
||||
MinCommentSize: s.MinCommentSize,
|
||||
MaxCommentSize: s.MaxCommentSize,
|
||||
MaxVotes: s.MaxVotes,
|
||||
PositiveScore: s.PositiveScore,
|
||||
ImageService: imageService,
|
||||
TitleExtractor: service.NewTitleExtractor(http.Client{Timeout: time.Second * 5}),
|
||||
TitleExtractor: service.NewTitleExtractor(http.Client{Timeout: time.Second * 5}, s.getAllowedDomains()),
|
||||
RestrictedWordsMatcher: service.NewRestrictedWordsMatcher(service.StaticRestrictedWordsLister{Words: s.RestrictedWords}),
|
||||
}
|
||||
dataService.RestrictSameIPVotes.Enabled = s.RestrictVoteIP
|
||||
@@ -540,7 +552,7 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
||||
Cache: loadingCache,
|
||||
NativeImporter: &migrator.Native{DataStore: dataService},
|
||||
DisqusImporter: &migrator.Disqus{DataStore: dataService},
|
||||
WordPressImporter: &migrator.WordPress{DataStore: dataService},
|
||||
WordPressImporter: &migrator.WordPress{DataStore: dataService, DisableFancyTextFormatting: s.DisableFancyTextFormatting},
|
||||
CommentoImporter: &migrator.Commento{DataStore: dataService},
|
||||
NativeExporter: &migrator.Native{DataStore: dataService},
|
||||
URLMapperMaker: migrator.NewURLMapper,
|
||||
@@ -575,33 +587,35 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
||||
}
|
||||
|
||||
srv := &api.Rest{
|
||||
Version: s.Revision,
|
||||
DataService: dataService,
|
||||
WebRoot: s.WebRoot,
|
||||
WebFS: webFS,
|
||||
RemarkURL: s.RemarkURL,
|
||||
ImageProxy: imgProxy,
|
||||
CommentFormatter: commentFormatter,
|
||||
Migrator: migr,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
SharedSecret: s.SharedSecret,
|
||||
Authenticator: authenticator,
|
||||
Cache: loadingCache,
|
||||
NotifyService: notifyService,
|
||||
TelegramService: telegramService,
|
||||
SSLConfig: sslConfig,
|
||||
UpdateLimiter: s.UpdateLimit,
|
||||
ImageService: imageService,
|
||||
EmailNotifications: contains("email", s.Notify.Users),
|
||||
TelegramNotifications: contains("telegram", s.Notify.Users) && telegramService != nil,
|
||||
EmojiEnabled: s.EnableEmoji,
|
||||
AnonVote: s.AnonymousVote && s.RestrictVoteIP,
|
||||
SimpleView: s.SimpleView,
|
||||
ProxyCORS: s.ProxyCORS,
|
||||
AllowedAncestors: s.AllowedHosts,
|
||||
SendJWTHeader: s.Auth.SendJWTHeader,
|
||||
SubscribersOnly: s.SubscribersOnly,
|
||||
DisableSignature: s.DisableSignature,
|
||||
Version: s.Revision,
|
||||
DataService: dataService,
|
||||
WebRoot: s.WebRoot,
|
||||
WebFS: webFS,
|
||||
RemarkURL: s.RemarkURL,
|
||||
ImageProxy: imgProxy,
|
||||
CommentFormatter: commentFormatter,
|
||||
Migrator: migr,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
SharedSecret: s.SharedSecret,
|
||||
Authenticator: authenticator,
|
||||
Cache: loadingCache,
|
||||
NotifyService: notifyService,
|
||||
TelegramService: telegramService,
|
||||
SSLConfig: sslConfig,
|
||||
UpdateLimiter: s.UpdateLimit,
|
||||
ImageService: imageService,
|
||||
EmailNotifications: contains("email", s.Notify.Users),
|
||||
TelegramNotifications: contains("telegram", s.Notify.Users) && telegramService != nil,
|
||||
EmojiEnabled: s.EnableEmoji,
|
||||
AnonVote: s.AnonymousVote && s.RestrictVoteIP,
|
||||
SimpleView: s.SimpleView,
|
||||
ProxyCORS: s.ProxyCORS,
|
||||
AllowedAncestors: s.AllowedHosts,
|
||||
SendJWTHeader: s.Auth.SendJWTHeader,
|
||||
SubscribersOnly: s.SubscribersOnly,
|
||||
DisableSignature: s.DisableSignature,
|
||||
DisableFancyTextFormatting: s.DisableFancyTextFormatting,
|
||||
ExternalImageProxy: s.ImageProxy.CacheExternal,
|
||||
}
|
||||
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
||||
@@ -633,6 +647,47 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Extract domains from s.AllowedHosts and second level domain from s.RemarkURL.
|
||||
// It can be and IP like http://127.0.0.1 in which case we need to use whole IP as domain
|
||||
// Beware, if s.RemarkURL is in third-level domain like https://example.co.uk, co.uk will be returned.
|
||||
func (s *ServerCommand) getAllowedDomains() []string {
|
||||
rawDomains := s.AllowedHosts
|
||||
rawDomains = append(rawDomains, s.RemarkURL)
|
||||
allowedDomains := []string{}
|
||||
for _, rawURL := range rawDomains {
|
||||
// case of 'self' AllowedHosts, which is not a valid rawURL name
|
||||
if rawURL == "self" || rawURL == "'self'" || rawURL == "\"self\"" {
|
||||
continue
|
||||
}
|
||||
// AllowedHosts usually don't have https:// prefix, so we're adding it just to make parsing below work the same way as for RemarkURL
|
||||
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
|
||||
rawURL = "https://" + rawURL
|
||||
}
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] failed to parse URL %s for TitleExtract whitelist: %v", rawURL, err)
|
||||
continue
|
||||
}
|
||||
domain := parsedURL.Hostname()
|
||||
|
||||
if domain == "" || // don't add empty domain as it will allow everything to be extracted
|
||||
(len(strings.Split(domain, ".")) < 2 && // don't allow single-word domains like "com"
|
||||
domain != "localhost") { // localhost is an exceptional single-word domain which is allowed
|
||||
continue
|
||||
}
|
||||
|
||||
// Only for RemarkURL if domain is not IP and has more than two levels, extract second level domain.
|
||||
// For AllowedHosts we don't do this as they are exact list of domains which can host comments, but
|
||||
// RemarkURL might be on a subdomain and we must allow parent domain to be used for TitleExtract.
|
||||
if rawURL == s.RemarkURL && net.ParseIP(domain) == nil && len(strings.Split(domain, ".")) > 2 {
|
||||
domain = strings.Join(strings.Split(domain, ".")[len(strings.Split(domain, "."))-2:], ".")
|
||||
}
|
||||
|
||||
allowedDomains = append(allowedDomains, domain)
|
||||
}
|
||||
return allowedDomains
|
||||
}
|
||||
|
||||
// Run all application objects
|
||||
func (a *serverApp) run(ctx context.Context) error {
|
||||
if a.AdminPasswd != "" {
|
||||
@@ -822,27 +877,28 @@ func (s *ServerCommand) makeAdminStore() (admin.Store, error) {
|
||||
|
||||
func (s *ServerCommand) makeCache() (LoadingCache, error) {
|
||||
log.Printf("[INFO] make cache, type=%s", s.Cache.Type)
|
||||
o := cache.NewOpts[[]byte]()
|
||||
switch s.Cache.Type {
|
||||
case "redis_pub_sub":
|
||||
redisPubSub, err := eventbus.NewRedisPubSub(s.Cache.RedisAddr, "remark42-cache")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache backend initialization, redis PubSub initialisation: %w", err)
|
||||
}
|
||||
backend, err := cache.NewLruCache(cache.MaxCacheSize(s.Cache.Max.Size), cache.MaxValSize(s.Cache.Max.Value),
|
||||
cache.MaxKeys(s.Cache.Max.Items), cache.EventBus(redisPubSub))
|
||||
backend, err := cache.NewLruCache(o.MaxCacheSize(s.Cache.Max.Size), o.MaxValSize(s.Cache.Max.Value),
|
||||
o.MaxKeys(s.Cache.Max.Items), o.EventBus(redisPubSub))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache backend initialization: %w", err)
|
||||
}
|
||||
return cache.NewScache(backend), nil
|
||||
return cache.NewScache[[]byte](backend), nil
|
||||
case "mem":
|
||||
backend, err := cache.NewLruCache(cache.MaxCacheSize(s.Cache.Max.Size), cache.MaxValSize(s.Cache.Max.Value),
|
||||
cache.MaxKeys(s.Cache.Max.Items))
|
||||
backend, err := cache.NewLruCache(o.MaxCacheSize(s.Cache.Max.Size), o.MaxValSize(s.Cache.Max.Value),
|
||||
o.MaxKeys(s.Cache.Max.Items))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache backend initialization: %w", err)
|
||||
}
|
||||
return cache.NewScache(backend), nil
|
||||
return cache.NewScache[[]byte](backend), nil
|
||||
case "none":
|
||||
return cache.NewScache(&cache.Nop{}), nil
|
||||
return cache.NewScache[[]byte](&cache.Nop[[]byte]{}), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported cache type %s", s.Cache.Type)
|
||||
}
|
||||
@@ -857,10 +913,9 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
||||
if s.Auth.Apple.CID != "" && s.Auth.Apple.TID != "" && s.Auth.Apple.KID != "" {
|
||||
err := authenticator.AddAppleProvider(
|
||||
provider.AppleConfig{
|
||||
ClientID: s.Auth.Apple.CID,
|
||||
TeamID: s.Auth.Apple.TID,
|
||||
KeyID: s.Auth.Apple.KID,
|
||||
ResponseMode: "query", // default is form_post which wouldn't work here
|
||||
ClientID: s.Auth.Apple.CID,
|
||||
TeamID: s.Auth.Apple.TID,
|
||||
KeyID: s.Auth.Apple.KID,
|
||||
},
|
||||
provider.LoadApplePrivateKeyFromFile(s.Auth.Apple.PrivateKeyFilePath),
|
||||
)
|
||||
@@ -910,18 +965,19 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
||||
|
||||
if s.Auth.Email.Enable {
|
||||
params := sender.EmailParams{
|
||||
Host: s.SMTP.Host,
|
||||
Port: s.SMTP.Port,
|
||||
SMTPUserName: s.SMTP.Username,
|
||||
SMTPPassword: s.SMTP.Password,
|
||||
TimeOut: s.SMTP.TimeOut,
|
||||
StartTLS: s.SMTP.StartTLS,
|
||||
LoginAuth: s.SMTP.LoginAuth,
|
||||
TLS: s.SMTP.TLS,
|
||||
Charset: "UTF-8",
|
||||
From: s.Auth.Email.From,
|
||||
Subject: s.Auth.Email.Subject,
|
||||
ContentType: s.Auth.Email.ContentType,
|
||||
Host: s.SMTP.Host,
|
||||
Port: s.SMTP.Port,
|
||||
SMTPUserName: s.SMTP.Username,
|
||||
SMTPPassword: s.SMTP.Password,
|
||||
TimeOut: s.SMTP.TimeOut,
|
||||
StartTLS: s.SMTP.StartTLS,
|
||||
LoginAuth: s.SMTP.LoginAuth,
|
||||
TLS: s.SMTP.TLS,
|
||||
InsecureSkipVerify: s.SMTP.InsecureSkipVerify,
|
||||
Charset: "UTF-8",
|
||||
From: s.Auth.Email.From,
|
||||
Subject: s.Auth.Email.Subject,
|
||||
ContentType: s.Auth.Email.ContentType,
|
||||
}
|
||||
sndr := sender.NewEmailClient(params, log.Default())
|
||||
tmpl, err := templates.Read(s.Auth.Email.MsgTemplate)
|
||||
@@ -1065,16 +1121,17 @@ func (s *ServerCommand) makeNotifyDestinations(authenticator *auth.Service) ([]n
|
||||
emailParams.AdminEmails = s.Admin.Shared.Email
|
||||
}
|
||||
smtpParams := ntf.SMTPParams{
|
||||
Host: s.SMTP.Host,
|
||||
Port: s.SMTP.Port,
|
||||
TLS: s.SMTP.TLS,
|
||||
StartTLS: s.SMTP.StartTLS,
|
||||
LoginAuth: s.SMTP.LoginAuth,
|
||||
Username: s.SMTP.Username,
|
||||
Password: s.SMTP.Password,
|
||||
TimeOut: s.SMTP.TimeOut,
|
||||
ContentType: "text/html",
|
||||
Charset: "UTF-8",
|
||||
Host: s.SMTP.Host,
|
||||
Port: s.SMTP.Port,
|
||||
TLS: s.SMTP.TLS,
|
||||
StartTLS: s.SMTP.StartTLS,
|
||||
InsecureSkipVerify: s.SMTP.InsecureSkipVerify,
|
||||
LoginAuth: s.SMTP.LoginAuth,
|
||||
Username: s.SMTP.Username,
|
||||
Password: s.SMTP.Password,
|
||||
TimeOut: s.SMTP.TimeOut,
|
||||
ContentType: "text/html",
|
||||
Charset: "UTF-8",
|
||||
}
|
||||
emailService, err := notify.NewEmail(emailParams, smtpParams)
|
||||
if err != nil {
|
||||
@@ -1175,7 +1232,7 @@ func (s *ServerCommand) getAuthenticator(ds *service.DataStore, avas avatar.Stor
|
||||
return c
|
||||
}),
|
||||
AdminPasswd: s.AdminPasswd,
|
||||
Validator: token.ValidatorFunc(func(token string, claims token.Claims) bool { // check on each auth call (in middleware)
|
||||
Validator: token.ValidatorFunc(func(_ string, claims token.Claims) bool { // check on each auth call (in middleware)
|
||||
if claims.User == nil {
|
||||
return false
|
||||
}
|
||||
@@ -1277,11 +1334,12 @@ func splitAtCommas(s string) []string {
|
||||
|
||||
// authRefreshCache used by authenticator to minimize repeatable token refreshes
|
||||
type authRefreshCache struct {
|
||||
cache.LoadingCache
|
||||
cache.LoadingCache[token.Claims]
|
||||
}
|
||||
|
||||
func newAuthRefreshCache() *authRefreshCache {
|
||||
expirableCache, _ := cache.NewExpirableCache(cache.TTL(5 * time.Minute))
|
||||
o := cache.NewOpts[token.Claims]()
|
||||
expirableCache, _ := cache.NewExpirableCache(o.TTL(5 * time.Minute))
|
||||
return &authRefreshCache{LoadingCache: expirableCache}
|
||||
}
|
||||
|
||||
@@ -1292,5 +1350,5 @@ func (c *authRefreshCache) Get(key interface{}) (interface{}, bool) {
|
||||
|
||||
// Set implements cache setter with key converted to string
|
||||
func (c *authRefreshCache) Set(key, value interface{}) {
|
||||
_, _ = c.LoadingCache.Get(key.(string), func() (interface{}, error) { return value, nil })
|
||||
_, _ = c.LoadingCache.Get(key.(string), func() (token.Claims, error) { return value.(token.Claims), nil })
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ func TestServerApp_WithSSL(t *testing.T) {
|
||||
|
||||
client := http.Client{
|
||||
// prevent http redirect
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
|
||||
@@ -457,6 +457,8 @@ func TestServerApp_DeprecatedArgs(t *testing.T) {
|
||||
"--notify.telegram.token=abcd",
|
||||
"--notify.telegram.timeout=3m",
|
||||
"--notify.telegram.api=http://example.org",
|
||||
"--auth.twitter.cid=123",
|
||||
"--auth.twitter.csec=456",
|
||||
}
|
||||
assert.Empty(t, s.SMTP.Host)
|
||||
assert.Empty(t, s.SMTP.Port)
|
||||
@@ -482,6 +484,8 @@ func TestServerApp_DeprecatedArgs(t *testing.T) {
|
||||
{Old: "notify.telegram.token", New: "telegram.token", Version: "1.9"},
|
||||
{Old: "notify.telegram.timeout", New: "telegram.timeout", Version: "1.9"},
|
||||
{Old: "notify.telegram.api", Version: "1.9"},
|
||||
{Old: "auth.twitter.cid", Version: "1.14"},
|
||||
{Old: "auth.twitter.csec", Version: "1.14"},
|
||||
},
|
||||
deprecatedFlags)
|
||||
assert.Equal(t, "smtp.example.org", s.SMTP.Host)
|
||||
@@ -726,6 +730,28 @@ func Test_splitAtCommas(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getAllowedDomains(t *testing.T) {
|
||||
tbl := []struct {
|
||||
s ServerCommand
|
||||
allowedDomains []string
|
||||
}{
|
||||
// correct example, parsed and returned as allowed domain
|
||||
{ServerCommand{AllowedHosts: []string{}, CommonOpts: CommonOpts{RemarkURL: "https://remark42.example.org"}}, []string{"example.org"}},
|
||||
{ServerCommand{AllowedHosts: []string{}, CommonOpts: CommonOpts{RemarkURL: "http://remark42.example.org"}}, []string{"example.org"}},
|
||||
{ServerCommand{AllowedHosts: []string{}, CommonOpts: CommonOpts{RemarkURL: "http://localhost"}}, []string{"localhost"}},
|
||||
// incorrect URLs, so Hostname is empty but returned list doesn't include empty string as it would allow any domain
|
||||
{ServerCommand{AllowedHosts: []string{}, CommonOpts: CommonOpts{RemarkURL: "bad hostname"}}, []string{}},
|
||||
{ServerCommand{AllowedHosts: []string{}, CommonOpts: CommonOpts{RemarkURL: "not_a_hostname"}}, []string{}},
|
||||
// test removal of 'self', multiple AllowedHosts. No deduplication is expected
|
||||
{ServerCommand{AllowedHosts: []string{"'self'", "example.org", "test.example.org", "remark42.com"}, CommonOpts: CommonOpts{RemarkURL: "https://example.org"}}, []string{"example.org", "test.example.org", "remark42.com", "example.org"}},
|
||||
}
|
||||
for i, tt := range tbl {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
assert.Equal(t, tt.allowedDomains, tt.s.getAllowedDomains())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func chooseRandomUnusedPort() (port int) {
|
||||
for i := 0; i < 10; i++ {
|
||||
port = 40000 + int(rand.Int31n(10000))
|
||||
@@ -826,5 +852,10 @@ func createAppFromCmd(t *testing.T, cmd ServerCommand) (*serverApp, context.Cont
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// ignore is added only for GitHub Actions, can't reproduce locally
|
||||
goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("net/http.(*Server).Shutdown"))
|
||||
goleak.VerifyTestMain(
|
||||
m,
|
||||
goleak.IgnoreTopFunction("net/http.(*Server).Shutdown"),
|
||||
// this will be fixed in https://github.com/hashicorp/golang-lru/issues/159
|
||||
goleak.IgnoreTopFunction("github.com/hashicorp/golang-lru/v2/expirable.NewLRU[...].func1"),
|
||||
)
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -55,7 +56,8 @@ func main() {
|
||||
}
|
||||
|
||||
if _, err := p.Parse(); err != nil {
|
||||
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
|
||||
var flagsErr *flags.Error
|
||||
if errors.As(err, &flagsErr) && flagsErr.Type == flags.ErrHelp {
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(1)
|
||||
@@ -97,7 +99,7 @@ func getDump() string {
|
||||
return string(stacktrace[:length])
|
||||
}
|
||||
|
||||
// nolint:gochecknoinits // can't avoid it in this place
|
||||
//nolint:gochecknoinits // can't avoid it in this place
|
||||
func init() {
|
||||
// catch SIGQUIT and print stack traces
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestMain_WithWebhook(t *testing.T) {
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var webhookSent int32
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
atomic.StoreInt32(&webhookSent, 1)
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
@@ -157,5 +157,7 @@ func TestMain(m *testing.M) {
|
||||
m,
|
||||
goleak.IgnoreTopFunction("github.com/umputun/remark42/backend/app.init.0.func1"),
|
||||
goleak.IgnoreTopFunction("net/http.(*Server).Shutdown"),
|
||||
// this will be fixed in https://github.com/hashicorp/golang-lru/issues/159
|
||||
goleak.IgnoreTopFunction("github.com/hashicorp/golang-lru/v2/expirable.NewLRU[...].func1"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/umputun/remark42/backend/app/store"
|
||||
@@ -110,16 +111,28 @@ func (d *Commento) convert(r io.Reader, siteID string) (ch chan store.Comment) {
|
||||
continue
|
||||
}
|
||||
|
||||
parentID := comment.ParentHex
|
||||
// comments with ParentHex == "root" are top-level comments
|
||||
if parentID == "root" {
|
||||
parentID = ""
|
||||
}
|
||||
|
||||
commentURL, e := url.JoinPath("https://", comment.Domain, comment.Path)
|
||||
if e != nil {
|
||||
log.Printf("[WARN] can't construct comment URL in commento import, %s", err.Error())
|
||||
}
|
||||
log.Printf("[ERROR] commentoURL: %s", commentURL)
|
||||
|
||||
c := store.Comment{
|
||||
ID: comment.CommentHex,
|
||||
Locator: store.Locator{
|
||||
URL: comment.Path,
|
||||
URL: commentURL,
|
||||
SiteID: siteID,
|
||||
},
|
||||
User: u,
|
||||
Text: comment.Markdown,
|
||||
Timestamp: comment.CreationDate,
|
||||
ParentID: comment.ParentHex,
|
||||
ParentID: parentID,
|
||||
Imported: true,
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
{
|
||||
"commentHex": "e7069a7dfcfaed43caf62300a9b0edb1c124ad79d0f5887c93649c15d7f69945",
|
||||
"domain": "example.com",
|
||||
"url": "https://example.com/blog/post/1",
|
||||
"url": "/blog/post/1",
|
||||
"commenterHex": "anonymous",
|
||||
"markdown": "Example comment created by user.",
|
||||
"html": "",
|
||||
@@ -18,7 +18,7 @@
|
||||
{
|
||||
"commentHex": "7d77e39fcd813241d6281478cc8f21ab5f807d043c750bc1a936bc23b34fb854",
|
||||
"domain": "example.com",
|
||||
"url": "https://example.com/blog/post/1",
|
||||
"url": "/blog/post/1",
|
||||
"commenterHex": "a1ac58ed1146bd7fe3feff6a7276f73955c3bfd23cacee00e2e0a7a89b1a8c10",
|
||||
"markdown": "Example 2 comment created by user.",
|
||||
"html": "",
|
||||
@@ -32,7 +32,7 @@
|
||||
{
|
||||
"commentHex": "ea5f7bcd6ac9bb7b657f7d0569831104e1bcf9c253d03c1e16bf9654c49a5ce9",
|
||||
"domain": "example.com",
|
||||
"url": "https://example.com/blog/post/1",
|
||||
"url": "/blog/post/1",
|
||||
"commenterHex": "bd1290ab5c858cf2a05903c2a9a61fd63399c6635db38cc6597002195e22e061",
|
||||
"markdown": "Great reply!",
|
||||
"html": "",
|
||||
|
||||
@@ -16,7 +16,8 @@ const wpTimeLayout = "2006-01-02 15:04:05"
|
||||
|
||||
// WordPress implements Importer from WP xml
|
||||
type WordPress struct {
|
||||
DataStore Store
|
||||
DataStore Store
|
||||
DisableFancyTextFormatting bool
|
||||
}
|
||||
|
||||
type wpItem struct {
|
||||
@@ -138,7 +139,7 @@ func (w *WordPress) convert(r io.Reader, siteID string) chan store.Comment {
|
||||
ParentID: comment.PID,
|
||||
Imported: true,
|
||||
}
|
||||
commentsCh <- commentFormatter.Format(c)
|
||||
commentsCh <- commentFormatter.Format(c, w.DisableFancyTextFormatting)
|
||||
stats.inpComments++
|
||||
if stats.inpComments%1000 == 0 {
|
||||
log.Printf("[DEBUG] processed %d comments", stats.inpComments)
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestWordPress_Import(t *testing.T) {
|
||||
|
||||
dataStore := service.DataStore{Engine: b, AdminStore: admin.NewStaticStore("12345", nil, []string{}, "")}
|
||||
defer dataStore.Close()
|
||||
wp := WordPress{DataStore: &dataStore}
|
||||
wp := WordPress{DataStore: &dataStore, DisableFancyTextFormatting: false}
|
||||
size, err := wp.Import(strings.NewReader(xmlTestWP), siteID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, size)
|
||||
@@ -41,7 +41,7 @@ func TestWordPress_Import(t *testing.T) {
|
||||
assert.Equal(t, "e8b1e92bbcf5b9bb88472f9bdb82d1b8c7ed39d6", c.User.IP)
|
||||
ts, _ := time.Parse(wpTimeLayout, "2010-08-18 15:19:14")
|
||||
assert.Equal(t, ts, c.Timestamp)
|
||||
assert.Equal(t, c.Text, "<p>Mekkatorque was over in that tent up to the right</p>\n")
|
||||
assert.Equal(t, "<p>«Mekkatorque» was over in that tent up to the right</p>\n", c.Text)
|
||||
assert.True(t, c.Imported)
|
||||
|
||||
posts, err := dataStore.List(siteID, 0, 0)
|
||||
@@ -54,6 +54,18 @@ func TestWordPress_Import(t *testing.T) {
|
||||
count, err := dataStore.Count(store.Locator{URL: "https://realmenweardress.es/2010/07/do-you-rp/", SiteID: siteID})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, count)
|
||||
|
||||
// test with DisableFancyTextFormatting
|
||||
wp = WordPress{DataStore: &dataStore, DisableFancyTextFormatting: true}
|
||||
size, err = wp.Import(strings.NewReader(xmlTestWP), siteID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, size)
|
||||
|
||||
last, err = dataStore.Last(siteID, 10, time.Time{}, adminUser)
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, 3, len(last), "3 comments imported")
|
||||
|
||||
assert.Equal(t, "<p>"Mekkatorque" was over in that tent up to the right</p>\n", last[0].Text)
|
||||
}
|
||||
|
||||
func TestWordPress_Convert(t *testing.T) {
|
||||
@@ -247,7 +259,7 @@ var xmlTestWP = `
|
||||
<wp:comment_author_IP><![CDATA[128.243.253.117]]></wp:comment_author_IP>
|
||||
<wp:comment_date><![CDATA[2010-08-18 15:19:14]]></wp:comment_date>
|
||||
<wp:comment_date_gmt><![CDATA[2010-08-18 15:19:14]]></wp:comment_date_gmt>
|
||||
<wp:comment_content><![CDATA[Mekkatorque was over in that tent up to the right]]></wp:comment_content>
|
||||
<wp:comment_content><![CDATA["Mekkatorque" was over in that tent up to the right]]></wp:comment_content>
|
||||
<wp:comment_approved><![CDATA[1]]></wp:comment_approved>
|
||||
<wp:comment_type><![CDATA[]]></wp:comment_type>
|
||||
<wp:comment_parent>13</wp:comment_parent>
|
||||
|
||||
@@ -3,6 +3,7 @@ package notify
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
webhookDefaultTemplate = `{"text": "{{.Text}}"}`
|
||||
webhookDefaultTemplate = `{"text": {{.Text | escapeJSONString}}}`
|
||||
)
|
||||
|
||||
// WebhookParams contain settings for webhook notifications
|
||||
@@ -49,7 +50,7 @@ func NewWebhook(params WebhookParams) (*Webhook, error) {
|
||||
params.Template = webhookDefaultTemplate
|
||||
}
|
||||
|
||||
payloadTmpl, err := template.New("webhook").Parse(params.Template)
|
||||
payloadTmpl, err := template.New("webhook").Funcs(template.FuncMap{"escapeJSONString": escapeJSONString}).Parse(params.Template)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse webhook template: %w", err)
|
||||
}
|
||||
@@ -82,3 +83,12 @@ func (w *Webhook) SendVerification(_ context.Context, _ VerificationRequest) err
|
||||
func (w *Webhook) String() string {
|
||||
return fmt.Sprintf("%s to %s", w.Webhook.String(), w.url)
|
||||
}
|
||||
|
||||
// escapeJSONString escapes string for JSON
|
||||
func escapeJSONString(s string) (string, error) {
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -34,6 +37,32 @@ func TestWebhook_NewWebhook(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "unable to parse webhook template")
|
||||
}
|
||||
|
||||
// https://github.com/umputun/remark42/issues/1791
|
||||
func TestWebhook_ReceiveValidJSON(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, r.URL.Path, "/webhook-notify")
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
t.Log("received body", string(body))
|
||||
assert.JSONEq(t, `{"text": "<p>testme</p>\n"}`, string(body))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
wh, err := NewWebhook(WebhookParams{
|
||||
URL: ts.URL + "/webhook-notify",
|
||||
Headers: []string{"Content-Type:application/json,text/plain"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, wh)
|
||||
|
||||
f := store.NewCommentFormatter()
|
||||
c := store.Comment{Text: f.FormatText("testme", false), ParentID: "1", ID: "999"}
|
||||
|
||||
err = wh.Send(context.Background(), Request{Comment: c})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWebhook_Send(t *testing.T) {
|
||||
wh, err := NewWebhook(WebhookParams{
|
||||
URL: "bad-url",
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-pkgz/auth"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/auth/token"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -111,7 +111,7 @@ func TestAdmin_Title(t *testing.T) {
|
||||
ts, srv, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
srv.DataService.TitleExtractor = service.NewTitleExtractor(http.Client{Timeout: time.Second})
|
||||
srv.DataService.TitleExtractor = service.NewTitleExtractor(http.Client{Timeout: time.Second}, []string{"127.0.0.1"})
|
||||
tss := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() == "/post1" {
|
||||
_, err := w.Write([]byte("<html><title>post1 blah 123</title><body> 2222</body></html>"))
|
||||
@@ -348,7 +348,7 @@ func TestAdmin_Block(t *testing.T) {
|
||||
assert.Equal(t, "test test #1", comments.Comments[2].Text, "comment not removed and not cleared")
|
||||
assert.False(t, comments.Comments[2].Deleted, "not deleted")
|
||||
|
||||
srv.pubRest.cache = cache.NewScache(cache.NewNopCache()) // TODO: with lru cache it won't be refreshed and invalidated for long
|
||||
srv.pubRest.cache = cache.NewScache[[]byte](cache.NewNopCache[[]byte]()) // TODO: with lru cache it won't be refreshed and invalidated for long
|
||||
// time
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah&sort=+time")
|
||||
@@ -513,6 +513,7 @@ func TestAdmin_ReadOnlyNoComments(t *testing.T) {
|
||||
_, err = srv.DataService.Info(store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah"}, 0)
|
||||
assert.Error(t, err)
|
||||
|
||||
// test format "tree"
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah&format=tree")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
comments := commentsWithInfo{}
|
||||
@@ -521,6 +522,16 @@ func TestAdmin_ReadOnlyNoComments(t *testing.T) {
|
||||
assert.Equal(t, 0, len(comments.Comments), "should have 0 comments")
|
||||
assert.True(t, comments.Info.ReadOnly)
|
||||
t.Logf("%+v", comments)
|
||||
|
||||
// test format "plain"
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
comments = commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(comments.Comments), "should have 0 comments")
|
||||
assert.True(t, comments.Info.ReadOnly)
|
||||
t.Logf("%+v", comments)
|
||||
}
|
||||
|
||||
func TestAdmin_ReadOnlyWithAge(t *testing.T) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
|
||||
@@ -136,7 +136,6 @@ func (m *Migrator) exportCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
exportFile := fmt.Sprintf("%s-%s.json.gz", siteID, time.Now().Format("20060102"))
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Disposition", "attachment;filename="+exportFile)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
gzWriter := gzip.NewWriter(w)
|
||||
defer func() {
|
||||
if e := gzWriter.Close(); e != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -49,6 +50,22 @@ func TestMigrator_Import(t *testing.T) {
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
|
||||
waitForMigrationCompletion(t, ts)
|
||||
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments := commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, comments.Info.Count)
|
||||
require.Equal(t, 1, len(comments.Comments))
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&format=tree&url=https://radio-t.com/blah1")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments = commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, comments.Info.Count)
|
||||
require.Equal(t, 1, len(comments.Comments))
|
||||
}
|
||||
|
||||
func TestMigrator_ImportForm(t *testing.T) {
|
||||
@@ -84,6 +101,22 @@ func TestMigrator_ImportForm(t *testing.T) {
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
|
||||
waitForMigrationCompletion(t, ts)
|
||||
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments := commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, comments.Info.Count)
|
||||
require.Equal(t, 1, len(comments.Comments))
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&format=tree&url=https://radio-t.com/blah1")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments = commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, comments.Info.Count)
|
||||
require.Equal(t, 1, len(comments.Comments))
|
||||
}
|
||||
|
||||
func TestMigrator_ImportFromWP(t *testing.T) {
|
||||
@@ -108,6 +141,22 @@ func TestMigrator_ImportFromWP(t *testing.T) {
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
|
||||
waitForMigrationCompletion(t, ts)
|
||||
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://realmenweardress.es/2010/07/do-you-rp/")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments := commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, comments.Info.Count)
|
||||
require.Equal(t, 3, len(comments.Comments))
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&format=tree&url=https://realmenweardress.es/2010/07/do-you-rp/")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments = commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, comments.Info.Count)
|
||||
require.Equal(t, 2, len(comments.Comments), "2 comments with 1 reply")
|
||||
}
|
||||
|
||||
func TestMigrator_ImportFromCommento(t *testing.T) {
|
||||
@@ -115,7 +164,7 @@ func TestMigrator_ImportFromCommento(t *testing.T) {
|
||||
defer teardown()
|
||||
|
||||
r := strings.NewReader(`{"version":1,"comments":[{"commentHex":"7d77e39fcd813241d6281478cc8f21ab5f807d043c750bc1a936bc23b34fb854",
|
||||
"domain":"example.com","url":"https://example.com/blog/post/1","commenterHex":"a1ac58ed1146bd7fe3feff6a7276f73955c3bfd23cacee00e2e0a7a89b1a8c10",
|
||||
"domain":"example.com","url":"/blog/post/1","commenterHex":"a1ac58ed1146bd7fe3feff6a7276f73955c3bfd23cacee00e2e0a7a89b1a8c10",
|
||||
"markdown":"Example content","html":"","parentHex":"root","score":0,"state":"approved","creationDate":"2021-03-17T12:09:47.722181Z",
|
||||
"direction":0,"deleted":false}],"commenters":[{"commenterHex":"a1ac58ed1146bd7fe3feff6a7276f73955c3bfd23cacee00e2e0a7a89b1a8c10",
|
||||
"email":"somegreatmail@gmail.com","name":"User5276","link":"https://example.com/profile/257","photo":"https://secure.gravatar.com/avatar/8f279626d26175134b0d5c88648172f7",
|
||||
@@ -137,6 +186,63 @@ func TestMigrator_ImportFromCommento(t *testing.T) {
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
|
||||
waitForMigrationCompletion(t, ts)
|
||||
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://example.com/blog/post/1")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments := commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, comments.Info.Count)
|
||||
require.Equal(t, 1, len(comments.Comments))
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&format=tree&url=https://example.com/blog/post/1")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments = commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, comments.Info.Count)
|
||||
require.Equal(t, 1, len(comments.Comments))
|
||||
}
|
||||
|
||||
func TestMigrator_ImportFromCommentoJSON(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
r, err := os.Open("testdata/commento.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
defer client.CloseIdleConnections()
|
||||
req, err := http.NewRequest("POST", ts.URL+"/api/v1/admin/import?site=remark42&provider=commento", r)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Add("Content-Type", "application/json; charset=utf-8")
|
||||
req.SetBasicAuth("admin", "password")
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"status\":\"import request accepted\"}\n", string(b))
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
|
||||
waitForMigrationCompletion(t, ts)
|
||||
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://example.com/example")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments := commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 7, comments.Info.Count)
|
||||
require.Equal(t, 7, len(comments.Comments))
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&format=tree&url=https://example.com/example")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments = commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 7, comments.Info.Count)
|
||||
require.Equal(t, 5, len(comments.Comments), "five comments with two replies")
|
||||
}
|
||||
|
||||
func TestMigrator_ImportRejected(t *testing.T) {
|
||||
@@ -197,6 +303,20 @@ func TestMigrator_ImportDouble(t *testing.T) {
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusConflict, resp.StatusCode)
|
||||
waitForMigrationCompletion(t, ts)
|
||||
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments := commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 50, comments.Info.Count)
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments = commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 50, comments.Info.Count)
|
||||
}
|
||||
|
||||
func TestMigrator_ImportWaitExpired(t *testing.T) {
|
||||
@@ -236,6 +356,14 @@ func TestMigrator_ImportWaitExpired(t *testing.T) {
|
||||
assert.Equal(t, http.StatusGatewayTimeout, resp.StatusCode)
|
||||
|
||||
waitForMigrationCompletion(t, ts)
|
||||
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://example.com/example")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments := commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, comments.Info.Count)
|
||||
require.Equal(t, 0, len(comments.Comments))
|
||||
}
|
||||
|
||||
func TestMigrator_Export(t *testing.T) {
|
||||
@@ -263,6 +391,16 @@ func TestMigrator_Export(t *testing.T) {
|
||||
require.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||
waitForMigrationCompletion(t, ts)
|
||||
|
||||
// export wrong site, should result in error
|
||||
req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/export?mode=file&site=test", http.NoBody)
|
||||
require.NoError(t, err)
|
||||
req.SetBasicAuth("admin", "password")
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||
require.Equal(t, "application/json", resp.Header.Get("Content-Type"))
|
||||
|
||||
// check file mode
|
||||
req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/export?mode=file&site=remark42", http.NoBody)
|
||||
require.NoError(t, err)
|
||||
@@ -339,6 +477,7 @@ func TestMigrator_Remap(t *testing.T) {
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, comments.Info.Count)
|
||||
require.Equal(t, 2, len(comments.Comments))
|
||||
require.False(t, comments.Info.ReadOnly)
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://remark42.com/demo-another/")
|
||||
@@ -347,6 +486,7 @@ func TestMigrator_Remap(t *testing.T) {
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, comments.Info.Count)
|
||||
require.Equal(t, 1, len(comments.Comments))
|
||||
require.True(t, comments.Info.ReadOnly)
|
||||
|
||||
// we want remap urls to another domain - www.remark42.com
|
||||
@@ -364,6 +504,16 @@ func TestMigrator_Remap(t *testing.T) {
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, comments.Info.Count)
|
||||
require.Equal(t, 2, len(comments.Comments))
|
||||
require.False(t, comments.Info.ReadOnly)
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&format=tree&url=https://www.remark42.com/demo/")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments = commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, comments.Info.Count)
|
||||
require.Equal(t, 2, len(comments.Comments))
|
||||
require.False(t, comments.Info.ReadOnly)
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://www.remark42.com/demo-another/")
|
||||
@@ -372,6 +522,16 @@ func TestMigrator_Remap(t *testing.T) {
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, comments.Info.Count)
|
||||
require.Equal(t, 1, len(comments.Comments))
|
||||
require.True(t, comments.Info.ReadOnly)
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&format=tree&url=https://www.remark42.com/demo-another/")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
comments = commentsWithInfo{}
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, comments.Info.Count)
|
||||
require.Equal(t, 1, len(comments.Comments))
|
||||
require.True(t, comments.Info.ReadOnly)
|
||||
|
||||
// should find nothing from previous url
|
||||
@@ -381,6 +541,7 @@ func TestMigrator_Remap(t *testing.T) {
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, comments.Info.Count)
|
||||
require.Equal(t, 0, len(comments.Comments))
|
||||
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://remark42.com/demo-another/")
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
@@ -388,6 +549,7 @@ func TestMigrator_Remap(t *testing.T) {
|
||||
err = json.Unmarshal([]byte(res), &comments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, comments.Info.Count)
|
||||
require.Equal(t, 0, len(comments.Comments))
|
||||
}
|
||||
|
||||
func TestMigrator_RemapReject(t *testing.T) {
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-pkgz/auth"
|
||||
"github.com/go-pkgz/lcw"
|
||||
"github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/go-pkgz/rest/logger"
|
||||
@@ -59,26 +59,29 @@ type Rest struct {
|
||||
Low int
|
||||
Critical int
|
||||
}
|
||||
UpdateLimiter float64
|
||||
EmailNotifications bool
|
||||
TelegramNotifications bool
|
||||
EmojiEnabled bool
|
||||
SimpleView bool
|
||||
ProxyCORS bool
|
||||
SendJWTHeader bool
|
||||
AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..."
|
||||
SubscribersOnly bool
|
||||
DisableSignature bool // prevent signature from being added to headers
|
||||
UpdateLimiter float64
|
||||
EmailNotifications bool
|
||||
TelegramNotifications bool
|
||||
EmojiEnabled bool
|
||||
SimpleView bool
|
||||
ProxyCORS bool
|
||||
SendJWTHeader bool
|
||||
AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..."
|
||||
SubscribersOnly bool
|
||||
DisableSignature bool // prevent signature from being added to headers
|
||||
DisableFancyTextFormatting bool // disables SmartyPants in the comment text rendering of the posted comments
|
||||
ExternalImageProxy bool
|
||||
|
||||
SSLConfig SSLConfig
|
||||
httpsServer *http.Server
|
||||
httpServer *http.Server
|
||||
lock sync.Mutex
|
||||
|
||||
pubRest public
|
||||
privRest private
|
||||
adminRest admin
|
||||
rssRest rss
|
||||
pubRest public
|
||||
privRest private
|
||||
adminRest admin
|
||||
rssRest rss
|
||||
openRouteLimiter float64
|
||||
}
|
||||
|
||||
// LoadingCache defines interface for caching
|
||||
@@ -89,7 +92,7 @@ type LoadingCache interface {
|
||||
}
|
||||
|
||||
const hardBodyLimit = 1024 * 64 // limit size of body
|
||||
|
||||
const openRouteLimiter = 10 // limit for open routes
|
||||
const lastCommentsScope = "last"
|
||||
|
||||
type commentsWithInfo struct {
|
||||
@@ -97,6 +100,11 @@ type commentsWithInfo struct {
|
||||
Info store.PostInfo `json:"info,omitempty"`
|
||||
}
|
||||
|
||||
type treeWithInfo struct {
|
||||
*service.Tree
|
||||
Info store.PostInfo `json:"info,omitempty"`
|
||||
}
|
||||
|
||||
// Run the lister and request's router, activate rest server
|
||||
func (s *Rest) Run(address string, port int) {
|
||||
if address == "*" {
|
||||
@@ -192,8 +200,13 @@ func (s *Rest) makeHTTPServer(address string, port int, router http.Handler) *ht
|
||||
}
|
||||
|
||||
func (s *Rest) routes() chi.Router {
|
||||
if s.openRouteLimiter == 0 {
|
||||
// set the default open route limiter. Just a safety measure as it should be set by Run method anyway
|
||||
s.openRouteLimiter = openRouteLimiter
|
||||
}
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.Throttle(1000), middleware.RealIP, R.Recoverer(log.Default()))
|
||||
router.Use(securityHeadersMiddleware(s.ExternalImageProxy, s.AllowedAncestors))
|
||||
if !s.DisableSignature {
|
||||
router.Use(R.AppInfo("remark42", "umputun", s.Version))
|
||||
}
|
||||
@@ -215,11 +228,6 @@ func (s *Rest) routes() chi.Router {
|
||||
router.Use(corsMiddleware.Handler)
|
||||
}
|
||||
|
||||
if len(s.AllowedAncestors) > 0 {
|
||||
log.Printf("[INFO] allowed from %+v only", s.AllowedAncestors)
|
||||
router.Use(frameAncestors(s.AllowedAncestors))
|
||||
}
|
||||
|
||||
ipFn := func(ip string) string { return store.HashValue(ip, s.SharedSecret)[:12] } // logger uses it for anonymization
|
||||
logInfoWithBody := logger.New(logger.Log(log.Default()), logger.WithBody, logger.IPfn(ipFn), logger.Prefix("[INFO]")).Handler
|
||||
|
||||
@@ -251,7 +259,7 @@ func (s *Rest) routes() chi.Router {
|
||||
// open routes
|
||||
rapi.Group(func(ropen chi.Router) {
|
||||
ropen.Use(middleware.Timeout(30 * time.Second))
|
||||
ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
|
||||
ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(s.openRouteLimiter, nil)))
|
||||
ropen.Use(authMiddleware.Trace, middleware.NoCache, logInfoWithBody)
|
||||
ropen.Get("/config", s.configCtrl)
|
||||
ropen.Get("/find", s.pubRest.findCommentsCtrl)
|
||||
@@ -369,16 +377,17 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) {
|
||||
}
|
||||
|
||||
privGrp := private{
|
||||
dataService: s.DataService,
|
||||
cache: s.Cache,
|
||||
imageService: s.ImageService,
|
||||
commentFormatter: s.CommentFormatter,
|
||||
readOnlyAge: s.ReadOnlyAge,
|
||||
authenticator: s.Authenticator,
|
||||
notifyService: s.NotifyService,
|
||||
telegramService: s.TelegramService,
|
||||
remarkURL: s.RemarkURL,
|
||||
anonVote: s.AnonVote,
|
||||
dataService: s.DataService,
|
||||
cache: s.Cache,
|
||||
imageService: s.ImageService,
|
||||
commentFormatter: s.CommentFormatter,
|
||||
readOnlyAge: s.ReadOnlyAge,
|
||||
authenticator: s.Authenticator,
|
||||
notifyService: s.NotifyService,
|
||||
telegramService: s.TelegramService,
|
||||
remarkURL: s.RemarkURL,
|
||||
anonVote: s.AnonVote,
|
||||
disableFancyTextFormatting: s.DisableFancyTextFormatting,
|
||||
}
|
||||
|
||||
admGrp := admin{
|
||||
@@ -417,6 +426,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
Version string `json:"version"`
|
||||
EditDuration int `json:"edit_duration"`
|
||||
AdminEdit bool `json:"admin_edit"`
|
||||
MinCommentSize int `json:"min_comment_size"`
|
||||
MaxCommentSize int `json:"max_comment_size"`
|
||||
Admins []string `json:"admins"`
|
||||
AdminEmail string `json:"admin_email"`
|
||||
@@ -437,6 +447,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
Version: s.Version,
|
||||
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
||||
AdminEdit: s.DataService.AdminEdits,
|
||||
MinCommentSize: s.DataService.MinCommentSize,
|
||||
MaxCommentSize: s.DataService.MaxCommentSize,
|
||||
Admins: admins,
|
||||
AdminEmail: emails,
|
||||
@@ -609,19 +620,23 @@ func cacheControl(expiration time.Duration, version string) func(http.Handler) h
|
||||
}
|
||||
}
|
||||
|
||||
// frameAncestors is a middleware setting Content-Security-Policy "frame-ancestors host1 host2 ..."
|
||||
// prevents loading of comments widgets from any other origins. In case if the list of allowed empty, ignored.
|
||||
func frameAncestors(hosts []string) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(hosts) == 0 {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
// securityHeadersMiddleware sets security-related headers: Content-Security-Policy and Permissions-Policy
|
||||
func securityHeadersMiddleware(imageProxyEnabled bool, allowedAncestors []string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
imgSrc := "*"
|
||||
if imageProxyEnabled {
|
||||
imgSrc = "'self'"
|
||||
}
|
||||
w.Header().Set("Content-Security-Policy", "frame-ancestors "+strings.Join(hosts, " ")+";")
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
frameAncestors := "*"
|
||||
if len(allowedAncestors) > 0 {
|
||||
log.Printf("[INFO] frame embedding allowed from %+v only", allowedAncestors)
|
||||
frameAncestors = strings.Join(allowedAncestors, " ")
|
||||
}
|
||||
w.Header().Set("Content-Security-Policy", fmt.Sprintf("default-src 'none'; base-uri 'none'; form-action 'none'; connect-src 'self'; frame-src 'self'; img-src %s; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src data:; object-src 'none'; frame-ancestors %s;", imgSrc, frameAncestors))
|
||||
w.Header().Set("Permissions-Policy", "accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), unload=(), window-management=()")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha1" //nolint:gosec //not used for security
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-pkgz/auth"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/golang-jwt/jwt"
|
||||
@@ -33,16 +34,17 @@ import (
|
||||
)
|
||||
|
||||
type private struct {
|
||||
dataService privStore
|
||||
cache LoadingCache
|
||||
readOnlyAge int
|
||||
commentFormatter *store.CommentFormatter
|
||||
imageService *image.Service
|
||||
notifyService *notify.Service
|
||||
authenticator *auth.Service
|
||||
telegramService telegramService
|
||||
remarkURL string
|
||||
anonVote bool
|
||||
dataService privStore
|
||||
cache LoadingCache
|
||||
readOnlyAge int
|
||||
commentFormatter *store.CommentFormatter
|
||||
imageService *image.Service
|
||||
notifyService *notify.Service
|
||||
authenticator *auth.Service
|
||||
telegramService telegramService
|
||||
remarkURL string
|
||||
anonVote bool
|
||||
disableFancyTextFormatting bool // disables SmartyPants in the comment text rendering of the posted comments
|
||||
}
|
||||
|
||||
// telegramService is a subset of Telegram service used for setting up user telegram notifications
|
||||
@@ -87,7 +89,7 @@ func (s *private) previewCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
comment = s.commentFormatter.Format(comment)
|
||||
comment = s.commentFormatter.Format(comment, s.disableFancyTextFormatting)
|
||||
comment.Sanitize()
|
||||
|
||||
// check if images are valid, omit proxied images as they are lazy-loaded
|
||||
@@ -127,7 +129,7 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentValidation)
|
||||
return
|
||||
}
|
||||
comment = s.commentFormatter.Format(comment)
|
||||
comment = s.commentFormatter.Format(comment, s.disableFancyTextFormatting)
|
||||
|
||||
// check if images are valid, omit proxied images as they are lazy-loaded
|
||||
for _, id := range s.imageService.ExtractNonProxiedPictures(comment.Text) {
|
||||
@@ -150,7 +152,7 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
id, err := s.dataService.Create(comment)
|
||||
if err == service.ErrRestrictedWordsFound {
|
||||
if errors.Is(err, service.ErrRestrictedWordsFound) {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentRestrictWords)
|
||||
return
|
||||
}
|
||||
@@ -211,7 +213,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
editReq := service.EditRequest{
|
||||
Text: s.commentFormatter.FormatText(edit.Text),
|
||||
Text: s.commentFormatter.FormatText(edit.Text, s.disableFancyTextFormatting),
|
||||
Orig: edit.Text,
|
||||
Summary: edit.Summary,
|
||||
Delete: edit.Delete,
|
||||
@@ -219,7 +221,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
res, err := s.dataService.EditComment(locator, id, editReq)
|
||||
if err == service.ErrRestrictedWordsFound {
|
||||
if errors.Is(err, service.ErrRestrictedWordsFound) {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentValidation)
|
||||
return
|
||||
}
|
||||
@@ -244,7 +246,7 @@ func (s *private) userInfoCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't read email for %s, %v", user.ID, err)
|
||||
}
|
||||
if len(email) > 0 {
|
||||
if email != "" {
|
||||
user.EmailSubscription = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ func TestRest_CreateAndPreviewWithImage(t *testing.T) {
|
||||
|
||||
var pngRead bool
|
||||
// server with the test PNG image
|
||||
pngServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
pngServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, e := io.Copy(w, gopherPNG())
|
||||
assert.NoError(t, e)
|
||||
pngRead = true
|
||||
@@ -368,6 +368,56 @@ func TestRest_CreateAndGet(t *testing.T) {
|
||||
assert.Equal(t, store.User{Name: "admin", ID: "admin", Admin: true, Blocked: false, IP: ""}, comment.User, "no ip")
|
||||
}
|
||||
|
||||
func TestRest_CreateWithQuotes(t *testing.T) {
|
||||
ts, srv, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
// create comment with quotes with smartypants
|
||||
resp, err := post(t, ts.URL+"/api/v1/comment",
|
||||
`{"text": "smartpants \"quoted\" text", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
c := R.JSON{}
|
||||
err = json.Unmarshal(b, &c)
|
||||
assert.NoError(t, err)
|
||||
id := c["id"].(string)
|
||||
|
||||
// get created comment by id as non-admin
|
||||
res, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah1", ts.URL, id))
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
comment := store.Comment{}
|
||||
err = json.Unmarshal([]byte(res), &comment)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "<p>smartpants «quoted» text</p>\n", comment.Text)
|
||||
assert.Equal(t, "smartpants \"quoted\" text", comment.Orig)
|
||||
|
||||
// create comment with quotes without smartypants
|
||||
srv.privRest.disableFancyTextFormatting = true
|
||||
resp, err = post(t, ts.URL+"/api/v1/comment",
|
||||
`{"text": "no_smartpants \"quoted\" text", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
b, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
c = R.JSON{}
|
||||
err = json.Unmarshal(b, &c)
|
||||
assert.NoError(t, err)
|
||||
id = c["id"].(string)
|
||||
|
||||
// get created comment by id as non-admin
|
||||
res, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah1", ts.URL, id))
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
comment = store.Comment{}
|
||||
err = json.Unmarshal([]byte(res), &comment)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "<p>no_smartpants "quoted" text</p>\n", comment.Text)
|
||||
assert.Equal(t, "no_smartpants \"quoted\" text", comment.Orig)
|
||||
}
|
||||
|
||||
func TestRest_Update(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/skip2/go-qrcode"
|
||||
@@ -50,6 +50,8 @@ type pubStore interface {
|
||||
|
||||
// GET /find?site=siteID&url=post-url&format=[tree|plain]&sort=[+/-time|+/-score|+/-controversy]&view=[user|all]&since=unix_ts_msec
|
||||
// find comments for given post. Returns in tree or plain formats, sorted
|
||||
//
|
||||
// When `url` parameter is not set (e.g. request is for site-wide comments), does not return deleted comments.
|
||||
func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
|
||||
sort := r.URL.Query().Get("sort")
|
||||
@@ -77,22 +79,36 @@ func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
comments = []store.Comment{} // error should clear comments and continue for post info
|
||||
}
|
||||
comments = s.applyView(comments, view)
|
||||
|
||||
var commentsInfo store.PostInfo
|
||||
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
|
||||
commentsInfo = info
|
||||
}
|
||||
|
||||
if !since.IsZero() { // if since is set, number of comments can be different from total in the DB
|
||||
commentsInfo.Count = 0
|
||||
for _, c := range comments {
|
||||
if !c.Deleted {
|
||||
commentsInfo.Count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// post might be readonly without any comments, Info call will fail then and ReadOnly flag should be checked separately
|
||||
if !commentsInfo.ReadOnly && locator.URL != "" && s.dataService.IsReadOnly(locator) {
|
||||
commentsInfo.ReadOnly = true
|
||||
}
|
||||
|
||||
var b []byte
|
||||
switch format {
|
||||
case "tree":
|
||||
tree := service.MakeTree(comments, sort, s.readOnlyAge)
|
||||
if tree.Nodes == nil { // eliminate json nil serialization
|
||||
tree.Nodes = []*service.Node{}
|
||||
withInfo := treeWithInfo{Tree: service.MakeTree(comments, sort), Info: commentsInfo}
|
||||
if withInfo.Nodes == nil { // eliminate json nil serialization
|
||||
withInfo.Nodes = []*service.Node{}
|
||||
}
|
||||
if s.dataService.IsReadOnly(locator) {
|
||||
tree.Info.ReadOnly = true
|
||||
}
|
||||
b, e = encodeJSONWithHTML(tree)
|
||||
b, e = encodeJSONWithHTML(withInfo)
|
||||
default:
|
||||
withInfo := commentsWithInfo{Comments: comments}
|
||||
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
|
||||
withInfo.Info = info
|
||||
}
|
||||
withInfo := commentsWithInfo{Comments: comments, Info: commentsInfo}
|
||||
b, e = encodeJSONWithHTML(withInfo)
|
||||
}
|
||||
return b, e
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -83,6 +85,24 @@ func TestRest_Preview(t *testing.T) {
|
||||
string(b),
|
||||
"/pics-remark42/staging/dev_user/62/bad_picture: no such file or directory\"}\n",
|
||||
)
|
||||
|
||||
// test quotes with and without smartypants
|
||||
resp, err = post(t, ts.URL+"/api/v1/preview", `{"text": "\"quoted\" text", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
b, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, "<p>«quoted» text</p>\n", string(b))
|
||||
|
||||
srv.privRest.disableFancyTextFormatting = true
|
||||
resp, err = post(t, ts.URL+"/api/v1/preview", `{"text": "\"quoted\" text", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
b, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, "<p>"quoted" text</p>\n", string(b))
|
||||
}
|
||||
|
||||
func TestRest_PreviewWithWrongImage(t *testing.T) {
|
||||
@@ -131,10 +151,10 @@ BKT
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
`<h1>h1</h1>
|
||||
<pre class="chroma"><code><span class="line"><span class="cl">func TestRest_Preview(t *testing.T) {
|
||||
</span></span><span class="line"><span class="cl">srv, ts := prep(t)
|
||||
</span></span><span class="line"><span class="cl"> require.NotNil(t, srv)
|
||||
</span></span><span class="line"><span class="cl">}
|
||||
<pre class="chroma"><code><span class="line"><span class="cl"><span class="k">func</span> <span class="n">TestRest_Preview</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="n">srv</span><span class="p">,</span> <span class="n">ts</span> <span class="p">:</span><span class="o">=</span> <span class="n">prep</span><span class="p">(</span><span class="n">t</span><span class="p">)</span>
|
||||
</span></span><span class="line"><span class="cl"> <span class="n">require</span><span class="o">.</span><span class="n">NotNil</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">srv</span><span class="p">)</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="p">}</span>
|
||||
</span></span></code></pre>`,
|
||||
string(b))
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
@@ -209,7 +229,7 @@ func TestRest_Find(t *testing.T) {
|
||||
assert.Equal(t, id2, comments.Comments[0].ID)
|
||||
|
||||
// get in tree mode
|
||||
tree := service.Tree{}
|
||||
tree := treeWithInfo{}
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
err = json.Unmarshal([]byte(res), &tree)
|
||||
@@ -235,7 +255,7 @@ func TestRest_FindAge(t *testing.T) {
|
||||
_, err = srv.DataService.Create(c2)
|
||||
require.NoError(t, err)
|
||||
|
||||
tree := service.Tree{}
|
||||
tree := treeWithInfo{}
|
||||
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
@@ -278,7 +298,7 @@ func TestRest_FindReadOnly(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
tree := service.Tree{}
|
||||
tree := treeWithInfo{}
|
||||
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
err = json.Unmarshal([]byte(res), &tree)
|
||||
@@ -286,7 +306,7 @@ func TestRest_FindReadOnly(t *testing.T) {
|
||||
assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL)
|
||||
assert.True(t, tree.Info.ReadOnly, "post is ro")
|
||||
|
||||
tree = service.Tree{}
|
||||
tree = treeWithInfo{}
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah2&format=tree")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
err = json.Unmarshal([]byte(res), &tree)
|
||||
@@ -477,6 +497,203 @@ func TestRest_FindUserComments(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRest_FindUserComments_CWE_918(t *testing.T) {
|
||||
ts, srv, teardown := startupT(t)
|
||||
srv.DataService.TitleExtractor = service.NewTitleExtractor(http.Client{Timeout: time.Second}, []string{"radio-t.com"}) // required for extracting the title, bad URL test
|
||||
defer srv.DataService.TitleExtractor.Close()
|
||||
defer teardown()
|
||||
|
||||
backendRequestedArbitraryServer := false
|
||||
arbitraryServer := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("request received: %+v", r)
|
||||
backendRequestedArbitraryServer = true
|
||||
}))
|
||||
defer arbitraryServer.Close()
|
||||
|
||||
arbitraryURLComment := store.Comment{Text: "arbitrary URL request test",
|
||||
Locator: store.Locator{SiteID: "remark42", URL: arbitraryServer.URL}}
|
||||
|
||||
assert.False(t, backendRequestedArbitraryServer)
|
||||
addComment(t, arbitraryURLComment, ts)
|
||||
assert.False(t, backendRequestedArbitraryServer,
|
||||
"no request is expected to the test server as it's not in the list of the allowed domains for the title extractor")
|
||||
|
||||
res, code := get(t, ts.URL+"/api/v1/comments?site=remark42&user=provider1_dev")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
|
||||
resp := struct {
|
||||
Comments []store.Comment
|
||||
Count int
|
||||
}{}
|
||||
|
||||
err := json.Unmarshal([]byte(res), &resp)
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, 1, len(resp.Comments), "should have 2 comments")
|
||||
|
||||
assert.Equal(t, "", resp.Comments[0].PostTitle, "empty from the first post")
|
||||
assert.Equal(t, arbitraryServer.URL, resp.Comments[0].Locator.URL, "arbitrary URL provided by the request")
|
||||
}
|
||||
|
||||
func TestPublic_FindCommentsCtrl_ConsistentCount(t *testing.T) {
|
||||
// test that comment counting is consistent between tree and plain formats
|
||||
ts, srv, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
commentLocator := store.Locator{URL: "test-url", SiteID: "remark42"}
|
||||
|
||||
// vote for comment multiple times
|
||||
setScore := func(locator store.Locator, id string, val int) {
|
||||
abs := func(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
for i := 0; i < abs(val); i++ {
|
||||
_, err := srv.DataService.Vote(service.VoteReq{
|
||||
Locator: locator,
|
||||
CommentID: id,
|
||||
// unique user ID is needed for correct counting of controversial votes
|
||||
UserID: "user" + strconv.Itoa(val) + strconv.Itoa(i),
|
||||
Val: val > 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Adding initial comments (8 to test-url and 1 to another-url) and voting, and delete two of comments to the first post.
|
||||
// With sleep so that at least few millisecond pass between each comment
|
||||
// and later we would be able to use that in "since" filter with millisecond precision
|
||||
ids := make([]string, 9)
|
||||
timestamps := make([]time.Time, 9)
|
||||
c1 := store.Comment{Text: "top-level comment 1", Locator: commentLocator}
|
||||
ids[0], timestamps[0] = addCommentGetCreatedTime(t, c1, ts)
|
||||
// #3 by score
|
||||
setScore(commentLocator, ids[0], 1)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
|
||||
c2 := store.Comment{Text: "top-level comment 2", Locator: commentLocator}
|
||||
ids[1], timestamps[1] = addCommentGetCreatedTime(t, c2, ts)
|
||||
// #2 by score
|
||||
setScore(commentLocator, ids[1], 2)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
|
||||
c3 := store.Comment{Text: "second-level comment 1", ParentID: ids[0], Locator: commentLocator}
|
||||
ids[2], timestamps[2] = addCommentGetCreatedTime(t, c3, ts)
|
||||
// #1 by score
|
||||
setScore(commentLocator, ids[2], 10)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
|
||||
c4 := store.Comment{Text: "third-level comment 1", ParentID: ids[2], Locator: commentLocator}
|
||||
ids[3], timestamps[3] = addCommentGetCreatedTime(t, c4, ts)
|
||||
// #5 by score, #1 by controversy
|
||||
setScore(commentLocator, ids[3], 4)
|
||||
setScore(commentLocator, ids[3], -4)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
|
||||
c5 := store.Comment{Text: "second-level comment 2", ParentID: ids[1], Locator: commentLocator}
|
||||
ids[4], timestamps[4] = addCommentGetCreatedTime(t, c5, ts)
|
||||
// #5 by score, #2 by controversy
|
||||
setScore(commentLocator, ids[4], 2)
|
||||
setScore(commentLocator, ids[4], -3)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
|
||||
c6 := store.Comment{Text: "third-level comment 2", ParentID: ids[4], Locator: commentLocator}
|
||||
ids[5], timestamps[5] = addCommentGetCreatedTime(t, c6, ts)
|
||||
// deleted later so not visible in site-wide requests
|
||||
setScore(commentLocator, ids[5], 10)
|
||||
setScore(commentLocator, ids[5], -10)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
|
||||
c7 := store.Comment{Text: "top-level comment 3", Locator: commentLocator}
|
||||
ids[6], timestamps[6] = addCommentGetCreatedTime(t, c7, ts)
|
||||
// #6 by score, #4 by controversy
|
||||
setScore(commentLocator, ids[6], -3)
|
||||
setScore(commentLocator, ids[6], 1)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
|
||||
c8 := store.Comment{Text: "second-level comment 3", ParentID: ids[6], Locator: commentLocator}
|
||||
ids[7], timestamps[7] = addCommentGetCreatedTime(t, c8, ts)
|
||||
// deleted later so not visible in site-wide requests
|
||||
setScore(commentLocator, ids[7], -20)
|
||||
|
||||
c9 := store.Comment{Text: "comment to post 2", Locator: store.Locator{URL: "another-url", SiteID: "remark42"}}
|
||||
ids[8], timestamps[8] = addCommentGetCreatedTime(t, c9, ts)
|
||||
// #7 by score
|
||||
setScore(store.Locator{URL: "another-url", SiteID: "remark42"}, ids[8], -25)
|
||||
|
||||
// delete two comments bringing the total from 9 to 6
|
||||
err := srv.DataService.Delete(commentLocator, ids[7], store.SoftDelete)
|
||||
assert.NoError(t, err)
|
||||
err = srv.DataService.Delete(commentLocator, ids[5], store.HardDelete)
|
||||
assert.NoError(t, err)
|
||||
srv.Cache.Flush(cache.FlusherRequest{})
|
||||
|
||||
commentLocator.URL = "readonly-test"
|
||||
// set post without comments to read-only
|
||||
assert.NoError(t, srv.DataService.SetReadOnly(commentLocator, true))
|
||||
|
||||
sinceTenSecondsAgo := strconv.FormatInt(time.Now().Add(-time.Second*10).UnixNano()/1000000, 10)
|
||||
sinceTS := make([]string, 9)
|
||||
formattedTS := make([]string, 9)
|
||||
for i, created := range timestamps {
|
||||
sinceTS[i] = strconv.FormatInt(created.UnixNano()/1000000, 10)
|
||||
formattedTS[i] = created.Format(time.RFC3339Nano)
|
||||
}
|
||||
t.Logf("last timestamp: %v", timestamps[7])
|
||||
|
||||
testCases := []struct {
|
||||
params string
|
||||
expectedBody string
|
||||
}{
|
||||
{"", fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
|
||||
{"url=test-url", fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
|
||||
{"format=plain", fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
|
||||
{"format=plain&url=test-url", fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
|
||||
{"since=" + sinceTenSecondsAgo, fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
|
||||
{"url=test-url&since=" + sinceTenSecondsAgo, fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
|
||||
{"since=" + sinceTS[0], fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
|
||||
{"url=test-url&since=" + sinceTS[0], fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
|
||||
{"since=" + sinceTS[1], fmt.Sprintf(`"info":{"count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
|
||||
{"url=test-url&since=" + sinceTS[1], fmt.Sprintf(`"info":{"url":"test-url","count":5,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
|
||||
{"since=" + sinceTS[4], fmt.Sprintf(`"info":{"count":3,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
|
||||
{"url=test-url&since=" + sinceTS[4], fmt.Sprintf(`"info":{"url":"test-url","count":2,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
|
||||
{"format=tree", `"info":{"count":7`},
|
||||
{"format=tree&url=test-url", `"info":{"url":"test-url","count":6`},
|
||||
{"format=tree&sort=+time", `"info":{"count":7`},
|
||||
{"format=tree&url=test-url&sort=+time", `"info":{"url":"test-url","count":6`},
|
||||
{"format=tree&sort=-score", `"info":{"count":7`},
|
||||
{"format=tree&url=test-url&sort=-score", `"info":{"url":"test-url","count":6`},
|
||||
{"sort=+time", fmt.Sprintf(`"score":-25,"vote":0,"time":%q}],"info":{"count":7`, formattedTS[8])},
|
||||
{"sort=-time", fmt.Sprintf(`"score":1,"vote":0,"time":%q}],"info":{"count":7`, formattedTS[0])},
|
||||
{"sort=+score", fmt.Sprintf(`"score":10,"vote":0,"time":%q}],"info":{"count":7`, formattedTS[2])},
|
||||
{"sort=+score&url=test-url", fmt.Sprintf(`"score":10,"vote":0,"time":%q}],"info":{"url":"test-url","count":6`, formattedTS[2])},
|
||||
{"sort=-score", fmt.Sprintf(`"score":-25,"vote":0,"time":%q}],"info":{"count":7`, formattedTS[8])},
|
||||
{"sort=-score&url=test-url", fmt.Sprintf(`"score":-2,"vote":0,"controversy":1.5874010519681994,"time":%q}],"info":{"url":"test-url","count":6`, formattedTS[6])},
|
||||
{"sort=-time&since=" + sinceTS[4], fmt.Sprintf(`"score":-1,"vote":0,"controversy":2.924017738212866,"time":%q}],"info":{"count":3`, formattedTS[4])},
|
||||
{"sort=-score&since=" + sinceTS[3], fmt.Sprintf(`"score":-25,"vote":0,"time":%q}],"info":{"count":4`, formattedTS[8])},
|
||||
{"sort=-score&url=test-url&since=" + sinceTS[3], fmt.Sprintf(`"score":-2,"vote":0,"controversy":1.5874010519681994,"time":%q}],"info":{"url":"test-url","count":3`, formattedTS[6])},
|
||||
{"sort=+controversy&url=test-url&since=" + sinceTS[5], fmt.Sprintf(`"score":-2,"vote":0,"controversy":1.5874010519681994,"time":%q}],"info":{"url":"test-url","count":1`, formattedTS[6])},
|
||||
// three comments of which last one deleted and doesn't have controversy so returned last
|
||||
{"sort=-controversy&url=test-url&since=" + sinceTS[5], fmt.Sprintf(`"score":0,"vote":0,"time":%q,"delete":true}],"info":{"url":"test-url","count":1`, formattedTS[7])},
|
||||
// test readonly status for the post without comments
|
||||
{"url=readonly-test", `"info":{"count":0,"read_only":true`},
|
||||
{"format=tree&url=readonly-test", `"info":{"count":0,"read_only":true`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.params, func(t *testing.T) {
|
||||
url := fmt.Sprintf(ts.URL+"/api/v1/find?site=remark42&%s", tc.params)
|
||||
body, code := get(t, url)
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
assert.Contains(t, body, tc.expectedBody)
|
||||
t.Log(body)
|
||||
// prevent hit limiter from engaging
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRest_UserInfo(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/go-pkgz/auth/avatar"
|
||||
"github.com/go-pkgz/auth/provider"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -128,7 +128,7 @@ func TestRest_RunStaticSSLMode(t *testing.T) {
|
||||
|
||||
client := http.Client{
|
||||
// prevent http redirect
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
|
||||
@@ -178,7 +178,7 @@ func TestRest_RunAutocertModeHTTPOnly(t *testing.T) {
|
||||
|
||||
client := http.Client{
|
||||
// prevent http redirect
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func TestRest_RunAutocertModeHTTPOnly(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRest_rejectAnonUser(t *testing.T) {
|
||||
ts := httptest.NewServer(fakeAuth(rejectAnonUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ts := httptest.NewServer(fakeAuth(rejectAnonUser(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
fmt.Fprintln(w, "Hello")
|
||||
}))))
|
||||
defer ts.Close()
|
||||
@@ -307,7 +307,7 @@ func TestRest_cacheControl(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", tt.url, http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h := cacheControl(tt.exp, tt.version)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
h := cacheControl(tt.exp, tt.version)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
h.ServeHTTP(w, req)
|
||||
resp := w.Result()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
@@ -320,30 +320,54 @@ func TestRest_cacheControl(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRest_frameAncestors(t *testing.T) {
|
||||
tbl := []struct {
|
||||
hosts []string
|
||||
header string
|
||||
}{
|
||||
{[]string{"http://example.com"}, "frame-ancestors http://example.com;"},
|
||||
{[]string{}, ""},
|
||||
{[]string{"http://example.com", "http://example2.com"}, "frame-ancestors http://example.com http://example2.com;"},
|
||||
}
|
||||
ts, _, teardown := startupT(t, func(o *Rest) {
|
||||
o.AllowedAncestors = []string{"'self'", "https://example.com"}
|
||||
})
|
||||
|
||||
for i, tt := range tbl {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://example.com", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
// Test case with frame-ancestors
|
||||
client := http.Client{}
|
||||
resp, err := client.Get(ts.URL + "/web/index.html")
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Contains(t, resp.Header.Get("Content-Security-Policy"), "frame-ancestors 'self' https://example.com;")
|
||||
teardown()
|
||||
|
||||
h := frameAncestors(tt.hosts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
h.ServeHTTP(w, req)
|
||||
resp := w.Result()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
t.Logf("%+v", resp.Header)
|
||||
assert.Equal(t, tt.header, resp.Header.Get("Content-Security-Policy"))
|
||||
})
|
||||
}
|
||||
// Test case without frame-ancestors
|
||||
ts, _, teardown = startupT(t, func(srv *Rest) {
|
||||
srv.AllowedAncestors = []string{}
|
||||
})
|
||||
defer teardown()
|
||||
resp, err = client.Get(ts.URL + "/web/index.html")
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Contains(t, resp.Header.Get("Content-Security-Policy"), "frame-ancestors *;")
|
||||
}
|
||||
|
||||
// check CSP, img-src should be 'self' with proxy enabled and * without it
|
||||
func TestRest_securityHeaders(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
|
||||
// with proxy disabled
|
||||
client := http.Client{}
|
||||
resp, err := client.Get(ts.URL + "/web/index.html")
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Contains(t, resp.Header.Get("Content-Security-Policy"), "img-src *;")
|
||||
teardown()
|
||||
|
||||
// check CSP with proxy enabled
|
||||
ts, _, teardown = startupT(t, func(srv *Rest) {
|
||||
srv.ExternalImageProxy = true
|
||||
})
|
||||
defer teardown()
|
||||
resp, err = client.Get(ts.URL + "/web/index.html")
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Contains(t, resp.Header.Get("Content-Security-Policy"), "img-src 'self';")
|
||||
}
|
||||
|
||||
func TestRest_subscribersOnly(t *testing.T) {
|
||||
@@ -371,7 +395,7 @@ func TestRest_subscribersOnly(t *testing.T) {
|
||||
req = token.SetUserInfo(req, tt.user)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
h := subscribersOnly(tt.subsOnly)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
h := subscribersOnly(tt.subsOnly)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
h.ServeHTTP(w, req)
|
||||
resp := w.Result()
|
||||
assert.Equal(t, tt.status, resp.StatusCode)
|
||||
@@ -403,7 +427,7 @@ func Test_validEmailAuth(t *testing.T) {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://example.com"+tt.req, http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
h := validEmailAuth()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
h := validEmailAuth()(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
h.ServeHTTP(w, req)
|
||||
resp := w.Result()
|
||||
assert.Equal(t, tt.status, resp.StatusCode)
|
||||
@@ -438,7 +462,7 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr
|
||||
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDB, SiteID: "remark42"})
|
||||
require.NoError(t, err)
|
||||
|
||||
memCache := cache.NewScache(cache.NewNopCache())
|
||||
memCache := cache.NewScache[[]byte](cache.NewNopCache[[]byte]())
|
||||
|
||||
astore := adminstore.NewStaticStore("123456", []string{"remark42"}, []string{"a1", "a2"}, "admin@remark-42.com")
|
||||
restrictedWordsMatcher := service.NewRestrictedWordsMatcher(service.StaticRestrictedWordsLister{Words: []string{"duck"}})
|
||||
@@ -458,7 +482,7 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr
|
||||
DataService: dataStore,
|
||||
Authenticator: auth.NewService(auth.Opts{
|
||||
AdminPasswd: "password",
|
||||
SecretReader: token.SecretFunc(func(aud string) (string, error) { return "secret", nil }),
|
||||
SecretReader: token.SecretFunc(func(string) (string, error) { return "secret", nil }),
|
||||
AvatarStore: avatar.NewLocalFS(tmp + "/ava-remark42"),
|
||||
}),
|
||||
Cache: memCache,
|
||||
@@ -487,15 +511,16 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr
|
||||
Cache: memCache,
|
||||
KeyStore: astore,
|
||||
},
|
||||
NotifyService: notify.NopService,
|
||||
EmojiEnabled: true,
|
||||
NotifyService: notify.NopService,
|
||||
EmojiEnabled: true,
|
||||
openRouteLimiter: 100,
|
||||
}
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = -5, -10
|
||||
|
||||
// add some providers. Needed because we don't allow users with unlisted providers to authenticate
|
||||
providers := []string{"provider1", "anonymous", "github", "email"}
|
||||
for _, p := range providers {
|
||||
srv.Authenticator.AddDirectProvider(p, provider.CredCheckerFunc(func(user, password string) (ok bool, err error) {
|
||||
srv.Authenticator.AddDirectProvider(p, provider.CredCheckerFunc(func(_, _ string) (ok bool, err error) {
|
||||
return true, nil
|
||||
}))
|
||||
}
|
||||
@@ -504,7 +529,8 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr
|
||||
h(srv)
|
||||
}
|
||||
|
||||
ts = httptest.NewServer(srv.routes())
|
||||
routes := srv.routes()
|
||||
ts = httptest.NewServer(routes)
|
||||
|
||||
teardown = func() {
|
||||
ts.Close()
|
||||
@@ -599,7 +625,7 @@ func post(t *testing.T, url, body string) (*http.Response, error) {
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string {
|
||||
func addCommentGetCreatedTime(t *testing.T, c store.Comment, ts *httptest.Server) (id string, created time.Time) {
|
||||
b, err := json.Marshal(c)
|
||||
require.NoError(t, err, "can't marshal comment %+v", c)
|
||||
|
||||
@@ -619,7 +645,14 @@ func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string {
|
||||
err = json.Unmarshal(b, &crResp)
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Nanosecond * 10)
|
||||
return crResp["id"].(string)
|
||||
created, err = time.Parse(time.RFC3339, crResp["time"].(string))
|
||||
require.NoError(t, err)
|
||||
return crResp["id"].(string), created
|
||||
}
|
||||
|
||||
func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string {
|
||||
id, _ := addCommentGetCreatedTime(t, c, ts)
|
||||
return id
|
||||
}
|
||||
|
||||
func requireAdminOnly(t *testing.T, req *http.Request) {
|
||||
@@ -658,5 +691,9 @@ func waitForHTTPSServerStart(port int) {
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
goleak.VerifyTestMain(
|
||||
m,
|
||||
// this will be fixed in https://github.com/hashicorp/golang-lru/issues/159
|
||||
goleak.IgnoreTopFunction("github.com/hashicorp/golang-lru/v2/expirable.NewLRU[...].func1"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/gorilla/feeds"
|
||||
|
||||
@@ -56,7 +56,6 @@ func (s *rss) postCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestSSL_Redirect(t *testing.T) {
|
||||
|
||||
client := http.Client{
|
||||
// prevent http redirect
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestSSL_ACME_HTTPChallengeRouter(t *testing.T) {
|
||||
|
||||
client := http.Client{
|
||||
// prevent http redirect
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"version": 1,
|
||||
"comments": [
|
||||
{
|
||||
"commentHex": "e7a2ef4b4aa1414a7ee65a989889aaecd9d5e7e3bca598ea7a967b4dbcaa8e11",
|
||||
"domain": "example.com",
|
||||
"url": "/example",
|
||||
"commenterHex": "018407e4b12b35f43b1d804d82607b341bef80c4325dd047d93f2cbb439cff85",
|
||||
"markdown": "",
|
||||
"html": "",
|
||||
"parentHex": "root",
|
||||
"score": 0,
|
||||
"state": "approved",
|
||||
"creationDate": "2022-10-25T07:25:46.807555Z",
|
||||
"direction": 0,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"commentHex": "a29e741145daceb4ca5b3e5e279e05b56f73c04703d93b944718ef757e15317f",
|
||||
"domain": "example.com",
|
||||
"url": "/example",
|
||||
"commenterHex": "018407e4b12b35f43b1d804d82607b341bef80c4325dd047d93f2cbb439cff85",
|
||||
"markdown": "",
|
||||
"html": "",
|
||||
"parentHex": "root",
|
||||
"score": 0,
|
||||
"state": "approved",
|
||||
"creationDate": "2023-07-26T12:24:55.058552Z",
|
||||
"direction": 0,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"commentHex": "46baf36433830a4e8bda1de56290cf5fd74c08bfa844fee4ec1744985dc77010",
|
||||
"domain": "example.com",
|
||||
"url": "/example",
|
||||
"commenterHex": "018407e4b12b35f43b1d804d82607b341bef80c4325dd047d93f2cbb439cff85",
|
||||
"markdown": "",
|
||||
"html": "",
|
||||
"parentHex": "root",
|
||||
"score": 0,
|
||||
"state": "approved",
|
||||
"creationDate": "2023-10-31T11:03:25.403282Z",
|
||||
"direction": 0,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"commentHex": "6d3bb64ff73b5f9d6a959212ffde472a51abf8bdefaa5ed843659796bceef9de",
|
||||
"domain": "example.com",
|
||||
"url": "/example",
|
||||
"commenterHex": "018407e4b12b35f43b1d804d82607b341bef80c4325dd047d93f2cbb439cff85",
|
||||
"markdown": "",
|
||||
"html": "",
|
||||
"parentHex": "46baf36433830a4e8bda1de56290cf5fd74c08bfa844fee4ec1744985dc77010",
|
||||
"score": 0,
|
||||
"state": "approved",
|
||||
"creationDate": "2023-11-01T22:23:47.112062Z",
|
||||
"direction": 0,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"commentHex": "23fcfcd03745ed71a9d23a9b59387a313df57e5c0faad8ba5dc96112766312c5",
|
||||
"domain": "example.com",
|
||||
"url": "/example",
|
||||
"commenterHex": "018407e4b12b35f43b1d804d82607b341bef80c4325dd047d93f2cbb439cff85",
|
||||
"markdown": "",
|
||||
"html": "",
|
||||
"parentHex": "root",
|
||||
"score": 0,
|
||||
"state": "approved",
|
||||
"creationDate": "2023-10-23T12:33:03.370182Z",
|
||||
"direction": 0,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"commentHex": "d0ad6f11cf0c5f8e17457a378a6bb789f412c6b7ef7ada4ae06ec8451f7a18aa",
|
||||
"domain": "example.com",
|
||||
"url": "/example",
|
||||
"commenterHex": "018407e4b12b35f43b1d804d82607b341bef80c4325dd047d93f2cbb439cff85",
|
||||
"markdown": "",
|
||||
"html": "",
|
||||
"parentHex": "root",
|
||||
"score": 0,
|
||||
"state": "approved",
|
||||
"creationDate": "2023-10-18T01:18:38.193625Z",
|
||||
"direction": 0,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"commentHex": "098960fd01c1fc7c0d3ea428f52fab97ea5c18aa52f3565bba679224daddc687",
|
||||
"domain": "example.com",
|
||||
"url": "/example",
|
||||
"commenterHex": "018407e4b12b35f43b1d804d82607b341bef80c4325dd047d93f2cbb439cff85",
|
||||
"markdown": "",
|
||||
"html": "",
|
||||
"parentHex": "23fcfcd03745ed71a9d23a9b59387a313df57e5c0faad8ba5dc96112766312c5",
|
||||
"score": 0,
|
||||
"state": "approved",
|
||||
"creationDate": "2023-11-01T22:24:04.639965Z",
|
||||
"direction": 0,
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"commenters": [
|
||||
{
|
||||
"commenterHex": "018407e4b12b35f43b1d804d82607b341bef80c4325dd047d93f2cbb439cff85",
|
||||
"email": "undefined",
|
||||
"name": "blank",
|
||||
"link": "undefined",
|
||||
"photo": "undefined",
|
||||
"provider": "anon",
|
||||
"joinDate": "2022-06-09T15:54:29.865919Z",
|
||||
"isModerator": false,
|
||||
"deleted": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (p Image) extract(commentHTML string, imgSrcPred func(string) bool) ([]stri
|
||||
return nil, fmt.Errorf("can't create document: %w", err)
|
||||
}
|
||||
result := []string{}
|
||||
doc.Find("img").Each(func(i int, s *goquery.Selection) {
|
||||
doc.Find("img").Each(func(_ int, s *goquery.Selection) {
|
||||
if im, ok := s.Attr("src"); ok {
|
||||
if imgSrcPred(im) {
|
||||
result = append(result, im)
|
||||
@@ -98,6 +98,10 @@ func (p Image) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
if img == nil {
|
||||
img, err = p.downloadImage(context.Background(), imgURL)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "invalid content type") {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid content type", rest.ErrImgNotFound)
|
||||
return
|
||||
}
|
||||
rest.SendErrorJSON(w, r, http.StatusNotFound, err, "can't get image "+imgURL, rest.ErrAssetNotFound)
|
||||
return
|
||||
}
|
||||
@@ -165,6 +169,11 @@ func (p Image) downloadImage(ctx context.Context, imgURL string) ([]byte, error)
|
||||
return nil, fmt.Errorf("got unsuccessful response status %d while fetching %s", resp.StatusCode, imgURL)
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
return nil, fmt.Errorf("invalid content type %s", contentType)
|
||||
}
|
||||
|
||||
imgData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read image body")
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestImage_Replace(t *testing.T) {
|
||||
|
||||
func TestImage_Routes(t *testing.T) {
|
||||
// no image supposed to be cached
|
||||
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) { return nil, nil }}
|
||||
imageStore := image.StoreMock{LoadFunc: func(string) ([]byte, error) { return nil, nil }}
|
||||
img := Image{
|
||||
HTTP2HTTPS: true,
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
@@ -108,31 +108,45 @@ func TestImage_Routes(t *testing.T) {
|
||||
httpSrv := imgHTTPTestsServer(t)
|
||||
defer httpSrv.Close()
|
||||
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
||||
t.Run("valid image", func(t *testing.T) {
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
|
||||
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
|
||||
})
|
||||
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
|
||||
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
|
||||
t.Run("no image", func(t *testing.T) {
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/no-such-image.png"))
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
encodedImgURL = base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/no-such-image.png"))
|
||||
resp, err = http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
t.Run("bad encoding", func(t *testing.T) {
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "bad encoding"))
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
assert.Equal(t, 2, len(imageStore.LoadCalls()))
|
||||
})
|
||||
|
||||
encodedImgURL = base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "bad encoding"))
|
||||
resp, err = http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
assert.Equal(t, 2, len(imageStore.LoadCalls()))
|
||||
t.Run("non-image reference", func(t *testing.T) {
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte("https://google.com"))
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
assert.Equal(t, 3, len(imageStore.LoadCalls()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
|
||||
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) { return nil, nil }}
|
||||
imageStore := image.StoreMock{LoadFunc: func(string) ([]byte, error) { return nil, nil }}
|
||||
img := Image{
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
RoutePath: "/api/v1/proxy",
|
||||
@@ -158,10 +172,10 @@ func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
|
||||
|
||||
func TestImage_RoutesCachingImage(t *testing.T) {
|
||||
imageStore := image.StoreMock{
|
||||
LoadFunc: func(id string) ([]byte, error) {
|
||||
LoadFunc: func(string) ([]byte, error) {
|
||||
return nil, nil
|
||||
},
|
||||
SaveFunc: func(id string, img []byte) error {
|
||||
SaveFunc: func(string, []byte) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -196,7 +210,7 @@ func TestImage_RoutesCachingImage(t *testing.T) {
|
||||
func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
||||
// In order to validate that cached data used cache "will return" some other data from what http server would
|
||||
testImage := []byte(fmt.Sprintf("%256s", "X"))
|
||||
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) {
|
||||
imageStore := image.StoreMock{LoadFunc: func(string) ([]byte, error) {
|
||||
return testImage, nil
|
||||
}}
|
||||
img := Image{
|
||||
@@ -226,7 +240,7 @@ func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
||||
|
||||
func TestImage_RoutesTimedOut(t *testing.T) {
|
||||
// no image supposed to be cached
|
||||
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) { return nil, nil }}
|
||||
imageStore := image.StoreMock{LoadFunc: func(string) ([]byte, error) { return nil, nil }}
|
||||
img := Image{
|
||||
HTTP2HTTPS: true,
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
log "github.com/go-pkgz/lgr"
|
||||
)
|
||||
|
||||
// NOTE: matryer/moq should be installed globally and works with `go generate ./...`
|
||||
//go:generate moq --out admin_mock.go . Store
|
||||
|
||||
// Store defines interface returning admins info for given site
|
||||
type Store interface {
|
||||
Key(siteID string) (key string, err error)
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Ensure, that StoreMock does implement Store.
|
||||
// If this is not the case, regenerate this file with moq.
|
||||
var _ Store = &StoreMock{}
|
||||
|
||||
// StoreMock is a mock implementation of Store.
|
||||
//
|
||||
// func TestSomethingThatUsesStore(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked Store
|
||||
// mockedStore := &StoreMock{
|
||||
// AdminsFunc: func(siteID string) ([]string, error) {
|
||||
// panic("mock out the Admins method")
|
||||
// },
|
||||
// EmailFunc: func(siteID string) (string, error) {
|
||||
// panic("mock out the Email method")
|
||||
// },
|
||||
// EnabledFunc: func(siteID string) (bool, error) {
|
||||
// panic("mock out the Enabled method")
|
||||
// },
|
||||
// KeyFunc: func(siteID string) (string, error) {
|
||||
// panic("mock out the Key method")
|
||||
// },
|
||||
// OnEventFunc: func(siteID string, et EventType) error {
|
||||
// panic("mock out the OnEvent method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedStore in code that requires Store
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type StoreMock struct {
|
||||
// AdminsFunc mocks the Admins method.
|
||||
AdminsFunc func(siteID string) ([]string, error)
|
||||
|
||||
// EmailFunc mocks the Email method.
|
||||
EmailFunc func(siteID string) (string, error)
|
||||
|
||||
// EnabledFunc mocks the Enabled method.
|
||||
EnabledFunc func(siteID string) (bool, error)
|
||||
|
||||
// KeyFunc mocks the Key method.
|
||||
KeyFunc func(siteID string) (string, error)
|
||||
|
||||
// OnEventFunc mocks the OnEvent method.
|
||||
OnEventFunc func(siteID string, et EventType) error
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// Admins holds details about calls to the Admins method.
|
||||
Admins []struct {
|
||||
// SiteID is the siteID argument value.
|
||||
SiteID string
|
||||
}
|
||||
// Email holds details about calls to the Email method.
|
||||
Email []struct {
|
||||
// SiteID is the siteID argument value.
|
||||
SiteID string
|
||||
}
|
||||
// Enabled holds details about calls to the Enabled method.
|
||||
Enabled []struct {
|
||||
// SiteID is the siteID argument value.
|
||||
SiteID string
|
||||
}
|
||||
// Key holds details about calls to the Key method.
|
||||
Key []struct {
|
||||
// SiteID is the siteID argument value.
|
||||
SiteID string
|
||||
}
|
||||
// OnEvent holds details about calls to the OnEvent method.
|
||||
OnEvent []struct {
|
||||
// SiteID is the siteID argument value.
|
||||
SiteID string
|
||||
// Et is the et argument value.
|
||||
Et EventType
|
||||
}
|
||||
}
|
||||
lockAdmins sync.RWMutex
|
||||
lockEmail sync.RWMutex
|
||||
lockEnabled sync.RWMutex
|
||||
lockKey sync.RWMutex
|
||||
lockOnEvent sync.RWMutex
|
||||
}
|
||||
|
||||
// Admins calls AdminsFunc.
|
||||
func (mock *StoreMock) Admins(siteID string) ([]string, error) {
|
||||
if mock.AdminsFunc == nil {
|
||||
panic("StoreMock.AdminsFunc: method is nil but Store.Admins was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
SiteID string
|
||||
}{
|
||||
SiteID: siteID,
|
||||
}
|
||||
mock.lockAdmins.Lock()
|
||||
mock.calls.Admins = append(mock.calls.Admins, callInfo)
|
||||
mock.lockAdmins.Unlock()
|
||||
return mock.AdminsFunc(siteID)
|
||||
}
|
||||
|
||||
// AdminsCalls gets all the calls that were made to Admins.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.AdminsCalls())
|
||||
func (mock *StoreMock) AdminsCalls() []struct {
|
||||
SiteID string
|
||||
} {
|
||||
var calls []struct {
|
||||
SiteID string
|
||||
}
|
||||
mock.lockAdmins.RLock()
|
||||
calls = mock.calls.Admins
|
||||
mock.lockAdmins.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Email calls EmailFunc.
|
||||
func (mock *StoreMock) Email(siteID string) (string, error) {
|
||||
if mock.EmailFunc == nil {
|
||||
panic("StoreMock.EmailFunc: method is nil but Store.Email was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
SiteID string
|
||||
}{
|
||||
SiteID: siteID,
|
||||
}
|
||||
mock.lockEmail.Lock()
|
||||
mock.calls.Email = append(mock.calls.Email, callInfo)
|
||||
mock.lockEmail.Unlock()
|
||||
return mock.EmailFunc(siteID)
|
||||
}
|
||||
|
||||
// EmailCalls gets all the calls that were made to Email.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.EmailCalls())
|
||||
func (mock *StoreMock) EmailCalls() []struct {
|
||||
SiteID string
|
||||
} {
|
||||
var calls []struct {
|
||||
SiteID string
|
||||
}
|
||||
mock.lockEmail.RLock()
|
||||
calls = mock.calls.Email
|
||||
mock.lockEmail.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Enabled calls EnabledFunc.
|
||||
func (mock *StoreMock) Enabled(siteID string) (bool, error) {
|
||||
if mock.EnabledFunc == nil {
|
||||
panic("StoreMock.EnabledFunc: method is nil but Store.Enabled was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
SiteID string
|
||||
}{
|
||||
SiteID: siteID,
|
||||
}
|
||||
mock.lockEnabled.Lock()
|
||||
mock.calls.Enabled = append(mock.calls.Enabled, callInfo)
|
||||
mock.lockEnabled.Unlock()
|
||||
return mock.EnabledFunc(siteID)
|
||||
}
|
||||
|
||||
// EnabledCalls gets all the calls that were made to Enabled.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.EnabledCalls())
|
||||
func (mock *StoreMock) EnabledCalls() []struct {
|
||||
SiteID string
|
||||
} {
|
||||
var calls []struct {
|
||||
SiteID string
|
||||
}
|
||||
mock.lockEnabled.RLock()
|
||||
calls = mock.calls.Enabled
|
||||
mock.lockEnabled.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Key calls KeyFunc.
|
||||
func (mock *StoreMock) Key(siteID string) (string, error) {
|
||||
if mock.KeyFunc == nil {
|
||||
panic("StoreMock.KeyFunc: method is nil but Store.Key was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
SiteID string
|
||||
}{
|
||||
SiteID: siteID,
|
||||
}
|
||||
mock.lockKey.Lock()
|
||||
mock.calls.Key = append(mock.calls.Key, callInfo)
|
||||
mock.lockKey.Unlock()
|
||||
return mock.KeyFunc(siteID)
|
||||
}
|
||||
|
||||
// KeyCalls gets all the calls that were made to Key.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.KeyCalls())
|
||||
func (mock *StoreMock) KeyCalls() []struct {
|
||||
SiteID string
|
||||
} {
|
||||
var calls []struct {
|
||||
SiteID string
|
||||
}
|
||||
mock.lockKey.RLock()
|
||||
calls = mock.calls.Key
|
||||
mock.lockKey.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// OnEvent calls OnEventFunc.
|
||||
func (mock *StoreMock) OnEvent(siteID string, et EventType) error {
|
||||
if mock.OnEventFunc == nil {
|
||||
panic("StoreMock.OnEventFunc: method is nil but Store.OnEvent was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
SiteID string
|
||||
Et EventType
|
||||
}{
|
||||
SiteID: siteID,
|
||||
Et: et,
|
||||
}
|
||||
mock.lockOnEvent.Lock()
|
||||
mock.calls.OnEvent = append(mock.calls.OnEvent, callInfo)
|
||||
mock.lockOnEvent.Unlock()
|
||||
return mock.OnEventFunc(siteID, et)
|
||||
}
|
||||
|
||||
// OnEventCalls gets all the calls that were made to OnEvent.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.OnEventCalls())
|
||||
func (mock *StoreMock) OnEventCalls() []struct {
|
||||
SiteID string
|
||||
Et EventType
|
||||
} {
|
||||
var calls []struct {
|
||||
SiteID string
|
||||
Et EventType
|
||||
}
|
||||
mock.lockOnEvent.RLock()
|
||||
calls = mock.calls.OnEvent
|
||||
mock.lockOnEvent.RUnlock()
|
||||
return calls
|
||||
}
|
||||
@@ -6,6 +6,20 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStaticStore_StoreWithoutSites(t *testing.T) {
|
||||
var ks Store = NewStaticKeyStore("key123")
|
||||
enabled, err := ks.Enabled("any")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, enabled, "on empty store all sites are enabled")
|
||||
assert.NoError(t, ks.OnEvent("test", EvCreate), "static store does nothing OnEvent")
|
||||
|
||||
// empty key
|
||||
ks = NewStaticKeyStore("")
|
||||
key, err := ks.Key("any")
|
||||
assert.Error(t, err, "empty key")
|
||||
assert.Empty(t, key)
|
||||
}
|
||||
|
||||
func TestStaticStore_Get(t *testing.T) {
|
||||
var ks Store = NewStaticStore("key123", []string{"s1", "s2", "s3"},
|
||||
[]string{"123", "xyz"}, "aa@example.com")
|
||||
|
||||
@@ -45,9 +45,9 @@ type Edit struct {
|
||||
|
||||
// PostInfo holds summary for given post url
|
||||
type PostInfo struct {
|
||||
URL string `json:"url"`
|
||||
URL string `json:"url,omitempty"` // can be attached to site-wide comments but won't be set then
|
||||
Count int `json:"count"`
|
||||
ReadOnly bool `json:"read_only,omitempty" bson:"read_only,omitempty"`
|
||||
ReadOnly bool `json:"read_only,omitempty" bson:"read_only,omitempty"` // can be attached to site-wide comments but won't be set then
|
||||
FirstTS time.Time `json:"first_time,omitempty" bson:"first_time,omitempty"`
|
||||
LastTS time.Time `json:"last_time,omitempty" bson:"last_time,omitempty"`
|
||||
}
|
||||
@@ -98,6 +98,7 @@ func (c *Comment) SetDeleted(mode DeleteMode) {
|
||||
c.Text = ""
|
||||
c.Orig = ""
|
||||
c.Score = 0
|
||||
c.Controversy = 0
|
||||
c.Votes = map[string]bool{}
|
||||
c.VotedIPs = make(map[string]VotedIPInfo)
|
||||
c.Edit = nil
|
||||
@@ -182,8 +183,8 @@ func (c *Comment) escapeHTMLWithSome(inp string) string {
|
||||
return res
|
||||
}
|
||||
|
||||
// SanitizeText used to sanitize any input string
|
||||
// SanitizeText used to sanitize any input string, and removes any HTML tags
|
||||
func (c *Comment) SanitizeText(inp string) string {
|
||||
clean := bluemonday.UGCPolicy().Sanitize(inp)
|
||||
return c.escapeHTMLWithSome(clean)
|
||||
clean := bluemonday.StrictPolicy().Sanitize(inp)
|
||||
return strings.TrimSpace(c.escapeHTMLWithSome(clean))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestComment_Sanitize(t *testing.T) {
|
||||
},
|
||||
out: Comment{
|
||||
Text: "blah XSS\n\t",
|
||||
User: User{ID: `<a href="http://blah.com">username</a>`, Name: "name <b/>"},
|
||||
User: User{ID: `<a href="http://blah.com">username</a>`, Name: "name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -88,6 +88,14 @@ func TestComment_Sanitize(t *testing.T) {
|
||||
inp: Comment{Text: "blah blah", PostTitle: "<script>alert()</script>something"},
|
||||
out: Comment{Text: "blah blah", PostTitle: "something"},
|
||||
},
|
||||
{
|
||||
inp: Comment{Text: "blah blah", PostTitle: "<a href=\"https://example.com\">test</a>"},
|
||||
out: Comment{Text: "blah blah", PostTitle: "test"},
|
||||
},
|
||||
{
|
||||
inp: Comment{Text: "blah blah", PostTitle: "https://example.com/blah"}, // link is left as-is, but not rendered as <a>
|
||||
out: Comment{Text: "blah blah", PostTitle: "https://example.com/blah"},
|
||||
},
|
||||
{
|
||||
inp: Comment{Text: `<blockquote class="twitter-tweet"><p lang="es" dir="ltr">Silicon iMac Concept<a href="https://t.co/7ga95QxVXn">https://t.co/7ga95QxVXn</a> by <a href="https://twitter.com/marcsheep?ref_src=twsrc%5Etfw">@marcsheep</a> <a href="https://t.co/ULnVpG8w55">pic.twitter.com/ULnVpG8w55</a></p>— Andreas Storm (@avstorm) <a href="https://twitter.com/avstorm/status/1325693387798933504?ref_src=twsrc%5Etfw">November 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`, PostTitle: "Twitter quote"},
|
||||
out: Comment{Text: `<blockquote class="twitter-tweet"><p lang="es" dir="ltr">Silicon iMac Concept<a href="https://t.co/7ga95QxVXn" rel="nofollow">https://t.co/7ga95QxVXn</a> by <a href="https://twitter.com/marcsheep?ref_src=twsrc%5Etfw" rel="nofollow">@marcsheep</a> <a href="https://t.co/ULnVpG8w55" rel="nofollow">pic.twitter.com/ULnVpG8w55</a></p>— Andreas Storm (@avstorm) <a href="https://twitter.com/avstorm/status/1325693387798933504?ref_src=twsrc%5Etfw" rel="nofollow">November 9, 2020</a></blockquote> `, PostTitle: "Twitter quote"},
|
||||
@@ -263,7 +271,7 @@ func TestComment_sanitizeText(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"<a href=javascript:alert(document.domain)//>xxx</a>",
|
||||
"xxx</a>",
|
||||
"xxx",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ func (b *BoltDB) Find(req FindRequest) (comments []store.Comment, err error) {
|
||||
return e
|
||||
}
|
||||
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
return bucket.ForEach(func(_, v []byte) error {
|
||||
comment := store.Comment{}
|
||||
if e = json.Unmarshal(v, &comment); e != nil {
|
||||
return fmt.Errorf("failed to unmarshal: %w", e)
|
||||
@@ -306,9 +306,8 @@ func (b *BoltDB) Info(req InfoRequest) ([]store.PostInfo, error) {
|
||||
})
|
||||
|
||||
// set read-only from age and manual bucket
|
||||
readOnlyAge := req.ReadOnlyAge
|
||||
info.ReadOnly = readOnlyAge > 0 && !info.FirstTS.IsZero() && info.FirstTS.AddDate(0, 0, readOnlyAge).Before(time.Now())
|
||||
if b.checkFlag(FlagRequest{Locator: req.Locator, Flag: ReadOnly}) {
|
||||
info.ReadOnly = req.ReadOnlyAge > 0 && !info.FirstTS.IsZero() && info.FirstTS.AddDate(0, 0, req.ReadOnlyAge).Before(time.Now())
|
||||
if !info.ReadOnly && b.checkFlag(FlagRequest{Locator: req.Locator, Flag: ReadOnly}) {
|
||||
info.ReadOnly = true
|
||||
}
|
||||
return []store.PostInfo{info}, err
|
||||
@@ -425,11 +424,11 @@ func (b *BoltDB) Close() error {
|
||||
}
|
||||
|
||||
// Last returns up to max last comments for given siteID
|
||||
func (b *BoltDB) lastComments(siteID string, max int, since time.Time) (comments []store.Comment, err error) {
|
||||
func (b *BoltDB) lastComments(siteID string, maximum int, since time.Time) (comments []store.Comment, err error) {
|
||||
comments = []store.Comment{}
|
||||
|
||||
if max > lastLimit || max == 0 {
|
||||
max = lastLimit
|
||||
if maximum > lastLimit || maximum == 0 {
|
||||
maximum = lastLimit
|
||||
}
|
||||
|
||||
bdb, err := b.db(siteID)
|
||||
@@ -467,7 +466,7 @@ func (b *BoltDB) lastComments(siteID string, max int, since time.Time) (comments
|
||||
continue
|
||||
}
|
||||
comments = append(comments, comment)
|
||||
if len(comments) >= max {
|
||||
if len(comments) >= maximum {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -725,7 +724,7 @@ func (b *BoltDB) listDetails(loc store.Locator) (result []UserDetailEntry, err e
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
var entry UserDetailEntry
|
||||
bucket := tx.Bucket([]byte(userDetailsBucketName))
|
||||
return bucket.ForEach(func(userID, value []byte) error {
|
||||
return bucket.ForEach(func(_, value []byte) error {
|
||||
if err = json.Unmarshal(value, &entry); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal entry: %w", e)
|
||||
}
|
||||
@@ -870,7 +869,7 @@ func (b *BoltDB) deleteUser(bdb *bolt.DB, siteID, userID string, mode store.Dele
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
postsBkt := tx.Bucket([]byte(postsBucketName))
|
||||
postBkt := postsBkt.Bucket([]byte(postInfo.URL))
|
||||
err = postBkt.ForEach(func(postURL []byte, commentVal []byte) error {
|
||||
err = postBkt.ForEach(func(_ []byte, commentVal []byte) error {
|
||||
comment := store.Comment{}
|
||||
if err = json.Unmarshal(commentVal, &comment); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal: %w", err)
|
||||
|
||||
@@ -35,14 +35,16 @@ func NewCommentFormatter(converters ...CommentConverter) *CommentFormatter {
|
||||
}
|
||||
|
||||
// Format comment fields
|
||||
func (f *CommentFormatter) Format(c Comment) Comment {
|
||||
c.Text = f.FormatText(c.Text)
|
||||
func (f *CommentFormatter) Format(c Comment, raw bool) Comment {
|
||||
c.Text = f.FormatText(c.Text, raw)
|
||||
return c
|
||||
}
|
||||
|
||||
// FormatText converts text with markdown processor, applies external converters and shortens links
|
||||
func (f *CommentFormatter) FormatText(txt string) (res string) {
|
||||
mdExt, rend := GetMdExtensionsAndRenderer()
|
||||
//
|
||||
// raw=true disables SmartyPants for HTML rendering (replacement of quotes, dashes, fractions, etc).
|
||||
func (f *CommentFormatter) FormatText(txt string, raw bool) (res string) {
|
||||
mdExt, rend := GetMdExtensionsAndRenderer(raw)
|
||||
res = string(bf.Run([]byte(txt), bf.WithExtensions(mdExt), bf.WithRenderer(rend)))
|
||||
res = f.unEscape(res)
|
||||
|
||||
@@ -55,14 +57,14 @@ func (f *CommentFormatter) FormatText(txt string) (res string) {
|
||||
}
|
||||
|
||||
// Shortens all the automatic links in HTML: auto link has equal "href" and "text" attributes.
|
||||
func (f *CommentFormatter) shortenAutoLinks(commentHTML string, max int) (resHTML string) {
|
||||
func (f *CommentFormatter) shortenAutoLinks(commentHTML string, maximum int) (resHTML string) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(commentHTML))
|
||||
if err != nil {
|
||||
return commentHTML
|
||||
}
|
||||
doc.Find("a").Each(func(i int, s *goquery.Selection) {
|
||||
doc.Find("a").Each(func(_ int, s *goquery.Selection) {
|
||||
if href, ok := s.Attr("href"); ok {
|
||||
if href != s.Text() || len(href) < max+3 || max < 3 {
|
||||
if href != s.Text() || len(href) < maximum+3 || maximum < 3 {
|
||||
return
|
||||
}
|
||||
commentURL, e := url.Parse(href)
|
||||
@@ -75,7 +77,7 @@ func (f *CommentFormatter) shortenAutoLinks(commentHTML string, max int) (resHTM
|
||||
return
|
||||
}
|
||||
|
||||
short := string([]rune(href)[:max-3])
|
||||
short := string([]rune(href)[:maximum-3])
|
||||
if len(short) < len(host) {
|
||||
short = host
|
||||
}
|
||||
@@ -108,7 +110,7 @@ func (f *CommentFormatter) lazyImage(commentHTML string) (resHTML string) {
|
||||
if err != nil {
|
||||
return commentHTML
|
||||
}
|
||||
doc.Find("img").Each(func(i int, s *goquery.Selection) {
|
||||
doc.Find("img").Each(func(_ int, s *goquery.Selection) {
|
||||
s.SetAttr("loading", "lazy")
|
||||
})
|
||||
resHTML, err = doc.Find("body").Html()
|
||||
@@ -120,14 +122,19 @@ func (f *CommentFormatter) lazyImage(commentHTML string) (resHTML string) {
|
||||
|
||||
// GetMdExtensionsAndRenderer returns blackfriday extensions and renderer used for rendering markdown
|
||||
// within store module.
|
||||
func GetMdExtensionsAndRenderer() (bf.Extensions, *bfchroma.Renderer) {
|
||||
//
|
||||
// raw=true disables SmartyPants for HTML rendering (replacement of quotes, dashes, fractions, etc).
|
||||
func GetMdExtensionsAndRenderer(raw bool) (bf.Extensions, *bfchroma.Renderer) {
|
||||
mdExt := bf.NoIntraEmphasis | bf.Tables | bf.FencedCode |
|
||||
bf.Strikethrough | bf.SpaceHeadings | bf.HardLineBreak |
|
||||
bf.BackslashLineBreak | bf.Autolink
|
||||
|
||||
rend := bf.NewHTMLRenderer(bf.HTMLRendererParameters{
|
||||
Flags: bf.Smartypants | bf.SmartypantsFractions | bf.SmartypantsDashes | bf.SmartypantsAngledQuotes,
|
||||
})
|
||||
flags := bf.HTMLFlags(0)
|
||||
if !raw {
|
||||
flags = bf.Smartypants | bf.SmartypantsFractions | bf.SmartypantsDashes | bf.SmartypantsAngledQuotes
|
||||
}
|
||||
|
||||
rend := bf.NewHTMLRenderer(bf.HTMLRendererParameters{Flags: flags})
|
||||
|
||||
extRend := bfchroma.NewRenderer(bfchroma.Extend(rend), bfchroma.ChromaOptions(html.WithClasses(true)))
|
||||
return mdExt, extRend
|
||||
|
||||
@@ -2,6 +2,7 @@ package store
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -35,6 +36,12 @@ func TestFormatter_FormatText(t *testing.T) {
|
||||
"lazy image",
|
||||
},
|
||||
{"— not translated #354", "<p>— not translated #354</p>\n!converted", "mdash"},
|
||||
{`no_smartpants "quoted" text`, "<p>no_smartpants "quoted" text</p>\n!converted", "normal quotes without smartpants"},
|
||||
{`"quoted" text`, "<p>«quoted» text</p>\n!converted", "normal quotes with smartpants"},
|
||||
{`no_smartpants “quoted” text`, "<p>no_smartpants “quoted” text</p>\n!converted", "curly quotes without smartpants"},
|
||||
{`“quoted” text`, "<p>“quoted” text</p>\n!converted", "curly quotes with smartpants"},
|
||||
{`no_smartpants «quoted» text`, "<p>no_smartpants «quoted» text</p>\n!converted", "French guillemets without smartpants"},
|
||||
{`«quoted» text`, "<p>«quoted» text</p>\n!converted", "French guillemets with smartpants"},
|
||||
{"smth\n```go\nfunc main(aa string) int {return 0}\n```", `<p>smth</p>
|
||||
<pre class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
|
||||
</span></span></code></pre>!converted`, "code with language"},
|
||||
@@ -45,20 +52,22 @@ func TestFormatter_FormatText(t *testing.T) {
|
||||
for _, tt := range tbl {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.out, f.FormatText(tt.in))
|
||||
raw := strings.HasPrefix(tt.in, `no_smartpants`)
|
||||
t.Logf("raw: %v", raw)
|
||||
assert.Equal(t, tt.out, f.FormatText(tt.in, raw))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatter_FormatTextNoConverter(t *testing.T) {
|
||||
f := NewCommentFormatter()
|
||||
assert.Equal(t, "<p>12345</p>\n", f.FormatText("12345"))
|
||||
assert.Equal(t, "<p>12345</p>\n", f.FormatText("12345", false))
|
||||
}
|
||||
|
||||
func TestFormatter_FormatTextConverterFunc(t *testing.T) {
|
||||
fn := CommentConverterFunc(func(text string) string { return "zz!" + text })
|
||||
f := NewCommentFormatter(fn)
|
||||
assert.Equal(t, "zz!<p>12345</p>\n", f.FormatText("12345"))
|
||||
assert.Equal(t, "zz!<p>12345</p>\n", f.FormatText("12345", false))
|
||||
}
|
||||
|
||||
func TestFormatter_FormatComment(t *testing.T) {
|
||||
@@ -78,7 +87,7 @@ func TestFormatter_FormatComment(t *testing.T) {
|
||||
f := NewCommentFormatter(mockConverter{})
|
||||
exp := comment
|
||||
exp.Text = "<p>blah</p>\n\n<p>xyz</p>\n!converted"
|
||||
assert.Equal(t, exp, f.Format(comment))
|
||||
assert.Equal(t, exp, f.Format(comment, false))
|
||||
}
|
||||
|
||||
func TestFormatter_ShortenAutoLinks(t *testing.T) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -118,6 +119,18 @@ func (b *Bolt) Load(id string) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Delete image from storage
|
||||
func (b *Bolt) Delete(id string) error {
|
||||
return b.db.Update(func(tx *bolt.Tx) error {
|
||||
// deleting a non-existing key doesn't return an error, so joining errors from deleting an image
|
||||
// from both buckets is safe and will return nil if there are no errors on the real delete
|
||||
// or image is absent in both buckets
|
||||
err := tx.Bucket([]byte(imagesBktName)).Delete([]byte(id))
|
||||
err = errors.Join(err, tx.Bucket([]byte(imagesStagedBktName)).Delete([]byte(id)))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Cleanup runs scan of staging and removes old data based on ttl
|
||||
func (b *Bolt) Cleanup(_ context.Context, ttl time.Duration) error {
|
||||
return b.db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
@@ -58,6 +58,36 @@ func TestBoltStore_LoadAfterSave(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBoltStore_LoadAfterDelete(t *testing.T) {
|
||||
svc, teardown := prepareBoltImageStorageTest(t)
|
||||
defer teardown()
|
||||
|
||||
// delete image from permanent storage
|
||||
id := "test_img"
|
||||
err := svc.Save(id, gopherPNGBytes())
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = svc.Commit(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.Delete(id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = svc.Load(id)
|
||||
assert.Error(t, err)
|
||||
|
||||
// delete staging image
|
||||
id = "staging_img"
|
||||
err = svc.Save(id, gopherPNGBytes())
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = svc.Delete(id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = svc.Load(id)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBoltStore_Cleanup(t *testing.T) {
|
||||
svc, teardown := prepareBoltImageStorageTest(t)
|
||||
defer teardown()
|
||||
|
||||
@@ -23,7 +23,8 @@ type FileSystem struct {
|
||||
Staging string
|
||||
Partitions int
|
||||
|
||||
crc struct {
|
||||
moveLock sync.Mutex // needed only for deleting images or moving them from staging to permanent storage
|
||||
crc struct {
|
||||
*crc64.Table
|
||||
sync.Once
|
||||
mask string
|
||||
@@ -50,6 +51,8 @@ func (f *FileSystem) Save(id string, img []byte) error {
|
||||
|
||||
// Commit file stored in staging location by moving it to permanent location
|
||||
func (f *FileSystem) Commit(id string) error {
|
||||
f.moveLock.Lock()
|
||||
defer f.moveLock.Unlock()
|
||||
log.Printf("[DEBUG] Commit image %s", id)
|
||||
stagingImage, permImage := f.location(f.Staging, id), f.location(f.Location, id)
|
||||
|
||||
@@ -107,11 +110,31 @@ func (f *FileSystem) Load(id string) ([]byte, error) {
|
||||
return io.ReadAll(fh)
|
||||
}
|
||||
|
||||
// Delete image from storage
|
||||
func (f *FileSystem) Delete(id string) error {
|
||||
f.moveLock.Lock()
|
||||
defer f.moveLock.Unlock()
|
||||
staging := f.location(f.Staging, id)
|
||||
// file doesn't exist on staging, delete from permanent location
|
||||
if _, err := os.Stat(staging); os.IsNotExist(err) {
|
||||
file := f.location(f.Location, id)
|
||||
e := os.Remove(file)
|
||||
_ = os.Remove(path.Dir(file)) // try to remove directory
|
||||
return e
|
||||
}
|
||||
// delete file from staging
|
||||
err := os.Remove(staging)
|
||||
_ = os.Remove(path.Dir(staging)) // try to remove directory
|
||||
return err
|
||||
}
|
||||
|
||||
// Cleanup runs scan of staging and removes old files based on ttl
|
||||
func (f *FileSystem) Cleanup(_ context.Context, ttl time.Duration) error {
|
||||
if _, err := os.Stat(f.Staging); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
f.moveLock.Lock()
|
||||
defer f.moveLock.Unlock()
|
||||
|
||||
// we can ignore context as on local FS remove is relatively fast operation
|
||||
err := filepath.Walk(f.Staging, func(fpath string, info os.FileInfo, err error) error {
|
||||
@@ -143,7 +166,7 @@ func (f *FileSystem) Info() (StoreInfo, error) {
|
||||
}
|
||||
|
||||
var ts time.Time
|
||||
err := filepath.Walk(f.Staging, func(fpath string, info os.FileInfo, err error) error {
|
||||
err := filepath.Walk(f.Staging, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -131,6 +131,31 @@ func TestFsStore_LoadAfterCommit(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFsStore_LoadAfterDelete(t *testing.T) {
|
||||
svc, teardown := prepareImageTest(t)
|
||||
defer teardown()
|
||||
|
||||
id := "test_img"
|
||||
err := svc.Save(id, gopherPNGBytes())
|
||||
assert.NoError(t, err)
|
||||
err = svc.Commit(id)
|
||||
require.NoError(t, err)
|
||||
err = svc.Delete(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.Load(id)
|
||||
assert.Error(t, err)
|
||||
|
||||
// create file on staging
|
||||
err = svc.Save(id, gopherPNGBytes())
|
||||
assert.NoError(t, err)
|
||||
err = svc.Delete(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.Load(id)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFsStore_location(t *testing.T) {
|
||||
tbl := []struct {
|
||||
partitions int
|
||||
|
||||
@@ -70,6 +70,7 @@ type Store interface {
|
||||
Info() (StoreInfo, error) // get meta information about storage
|
||||
Save(id string, img []byte) error // store image with passed id to staging
|
||||
Load(id string) ([]byte, error) // load image by ID
|
||||
Delete(id string) error // delete image by ID
|
||||
|
||||
ResetCleanupTimer(id string) error // resets cleanup timer for the image, called on comment preview
|
||||
Commit(id string) error // move image from staging to permanent
|
||||
@@ -212,6 +213,11 @@ func (s *Service) Load(id string) ([]byte, error) {
|
||||
return s.store.Load(id)
|
||||
}
|
||||
|
||||
// Delete wraps storage Delete function.
|
||||
func (s *Service) Delete(id string) error {
|
||||
return s.store.Delete(id)
|
||||
}
|
||||
|
||||
// Save wraps storage Save function, validating and resizing the image before calling it.
|
||||
func (s *Service) Save(userID string, r io.Reader) (id string, err error) {
|
||||
id = path.Join(userID, guid())
|
||||
@@ -244,7 +250,7 @@ func (s *Service) extractImageIDs(commentHTML string, includeProxied bool) (ids
|
||||
log.Printf("[ERROR] can't parse commentHTML to parse images: %q, error: %v", commentHTML, err)
|
||||
return nil
|
||||
}
|
||||
doc.Find("img").Each(func(i int, sl *goquery.Selection) {
|
||||
doc.Find("img").Each(func(_ int, sl *goquery.Selection) {
|
||||
if im, ok := sl.Attr("src"); ok {
|
||||
if strings.Contains(im, s.ImageAPI) {
|
||||
elems := strings.Split(im, "/")
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
context "context"
|
||||
"context"
|
||||
"sync"
|
||||
time "time"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Ensure, that StoreMock does implement Store.
|
||||
@@ -25,6 +25,9 @@ var _ Store = &StoreMock{}
|
||||
// CommitFunc: func(id string) error {
|
||||
// panic("mock out the Commit method")
|
||||
// },
|
||||
// DeleteFunc: func(id string) error {
|
||||
// panic("mock out the Delete method")
|
||||
// },
|
||||
// InfoFunc: func() (StoreInfo, error) {
|
||||
// panic("mock out the Info method")
|
||||
// },
|
||||
@@ -50,6 +53,9 @@ type StoreMock struct {
|
||||
// CommitFunc mocks the Commit method.
|
||||
CommitFunc func(id string) error
|
||||
|
||||
// DeleteFunc mocks the Delete method.
|
||||
DeleteFunc func(id string) error
|
||||
|
||||
// InfoFunc mocks the Info method.
|
||||
InfoFunc func() (StoreInfo, error)
|
||||
|
||||
@@ -76,6 +82,11 @@ type StoreMock struct {
|
||||
// ID is the id argument value.
|
||||
ID string
|
||||
}
|
||||
// Delete holds details about calls to the Delete method.
|
||||
Delete []struct {
|
||||
// ID is the id argument value.
|
||||
ID string
|
||||
}
|
||||
// Info holds details about calls to the Info method.
|
||||
Info []struct {
|
||||
}
|
||||
@@ -99,6 +110,7 @@ type StoreMock struct {
|
||||
}
|
||||
lockCleanup sync.RWMutex
|
||||
lockCommit sync.RWMutex
|
||||
lockDelete sync.RWMutex
|
||||
lockInfo sync.RWMutex
|
||||
lockLoad sync.RWMutex
|
||||
lockResetCleanupTimer sync.RWMutex
|
||||
@@ -173,6 +185,38 @@ func (mock *StoreMock) CommitCalls() []struct {
|
||||
return calls
|
||||
}
|
||||
|
||||
// Delete calls DeleteFunc.
|
||||
func (mock *StoreMock) Delete(id string) error {
|
||||
if mock.DeleteFunc == nil {
|
||||
panic("StoreMock.DeleteFunc: method is nil but Store.Delete was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
ID string
|
||||
}{
|
||||
ID: id,
|
||||
}
|
||||
mock.lockDelete.Lock()
|
||||
mock.calls.Delete = append(mock.calls.Delete, callInfo)
|
||||
mock.lockDelete.Unlock()
|
||||
return mock.DeleteFunc(id)
|
||||
}
|
||||
|
||||
// DeleteCalls gets all the calls that were made to Delete.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.DeleteCalls())
|
||||
func (mock *StoreMock) DeleteCalls() []struct {
|
||||
ID string
|
||||
} {
|
||||
var calls []struct {
|
||||
ID string
|
||||
}
|
||||
mock.lockDelete.RLock()
|
||||
calls = mock.calls.Delete
|
||||
mock.lockDelete.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Info calls InfoFunc.
|
||||
func (mock *StoreMock) Info() (StoreInfo, error) {
|
||||
if mock.InfoFunc == nil {
|
||||
|
||||
@@ -19,10 +19,10 @@ import (
|
||||
|
||||
func TestService_SaveAndLoad(t *testing.T) {
|
||||
store := StoreMock{
|
||||
SaveFunc: func(id string, img []byte) error {
|
||||
SaveFunc: func(string, []byte) error {
|
||||
return nil
|
||||
},
|
||||
LoadFunc: func(id string) ([]byte, error) {
|
||||
LoadFunc: func(string) ([]byte, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func TestService_ExtractPictures(t *testing.T) {
|
||||
|
||||
func TestService_Cleanup(t *testing.T) {
|
||||
store := StoreMock{
|
||||
CleanupFunc: func(ctx context.Context, ttl time.Duration) error {
|
||||
CleanupFunc: func(context.Context, time.Duration) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -141,12 +141,8 @@ func TestService_Cleanup(t *testing.T) {
|
||||
|
||||
func TestService_Submit(t *testing.T) {
|
||||
store := StoreMock{
|
||||
CommitFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
ResetCleanupTimerFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
CommitFunc: func(string) error { return nil },
|
||||
ResetCleanupTimerFunc: func(string) error { return nil },
|
||||
}
|
||||
svc := NewService(&store, ServiceParams{ImageAPI: "/blah/", EditDuration: time.Millisecond * 100})
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
@@ -164,12 +160,8 @@ func TestService_Submit(t *testing.T) {
|
||||
|
||||
func TestService_Close(t *testing.T) {
|
||||
store := StoreMock{
|
||||
CommitFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
ResetCleanupTimerFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
CommitFunc: func(string) error { return nil },
|
||||
ResetCleanupTimerFunc: func(string) error { return nil },
|
||||
}
|
||||
svc := Service{store: &store, ServiceParams: ServiceParams{ImageAPI: "/blah/", EditDuration: time.Hour * 24}}
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
@@ -182,10 +174,8 @@ func TestService_Close(t *testing.T) {
|
||||
|
||||
func TestService_SubmitDelay(t *testing.T) {
|
||||
store := StoreMock{
|
||||
CommitFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
ResetCleanupTimerFunc: func(id string) error {
|
||||
CommitFunc: func(string) error { return nil },
|
||||
ResetCleanupTimerFunc: func(string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ func (r *RPC) Load(id string) ([]byte, error) {
|
||||
return io.ReadAll(base64.NewDecoder(base64.StdEncoding, strings.NewReader(rawImg)))
|
||||
}
|
||||
|
||||
// Delete image from storage
|
||||
func (r *RPC) Delete(id string) error {
|
||||
_, err := r.Call("image.delete", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Commit file stored in staging location by moving it to permanent location
|
||||
func (r *RPC) Commit(id string) error {
|
||||
_, err := r.Call("image.commit", id)
|
||||
|
||||
@@ -41,6 +41,18 @@ func TestRemote_Load(t *testing.T) {
|
||||
assert.Equal(t, gopherPNGBytes(), res)
|
||||
}
|
||||
|
||||
func TestRemote_Delete(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"image.delete","params":"54321","id":1}`, `{}`)
|
||||
defer ts.Close()
|
||||
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
|
||||
|
||||
var a Store = &c
|
||||
_ = a
|
||||
|
||||
err := c.Delete("54321")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRemote_Commit(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"image.commit","params":"gopher_id","id":1}`, `{"id":1}`)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -5,12 +5,13 @@ package service
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/lcw"
|
||||
"github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@@ -27,6 +28,7 @@ type DataStore struct {
|
||||
Engine engine.Interface
|
||||
EditDuration time.Duration
|
||||
AdminStore admin.Store
|
||||
MinCommentSize int
|
||||
MaxCommentSize int
|
||||
MaxVotes int
|
||||
RestrictSameIPVotes struct {
|
||||
@@ -47,7 +49,7 @@ type DataStore struct {
|
||||
}
|
||||
|
||||
repliesCache struct {
|
||||
lcw.LoadingCache
|
||||
lcw.LoadingCache[struct{}]
|
||||
once sync.Once
|
||||
}
|
||||
}
|
||||
@@ -69,7 +71,7 @@ type PostMetaData struct {
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
||||
const defaultCommentMaxSize = 2000
|
||||
const defaultCommentMaxSize = 2048
|
||||
const maxLastCommentsReply = 5000
|
||||
|
||||
// UnlimitedVotes doesn't restrict MaxVotes
|
||||
@@ -532,7 +534,8 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi
|
||||
func (s *DataStore) HasReplies(comment store.Comment) bool {
|
||||
s.repliesCache.once.Do(func() {
|
||||
// default expiration time of 5 minutes and cleanup time of 2.5 minutes
|
||||
s.repliesCache.LoadingCache, _ = lcw.NewExpirableCache(lcw.TTL(5 * time.Minute))
|
||||
o := lcw.NewOpts[struct{}]()
|
||||
s.repliesCache.LoadingCache, _ = lcw.NewExpirableCache[struct{}](o.TTL(5 * time.Minute))
|
||||
})
|
||||
|
||||
if _, found := s.repliesCache.Peek(comment.ID); found {
|
||||
@@ -552,7 +555,7 @@ func (s *DataStore) HasReplies(comment store.Comment) bool {
|
||||
// When this code is reached, key "comment.ID" is not in cache.
|
||||
// Calling cache.Get on it will put it in cache with 5 minutes TTL.
|
||||
// We call it with empty struct as value as we care about keys and not values.
|
||||
_, _ = s.repliesCache.Get(comment.ID, func() (interface{}, error) { return struct{}{}, nil })
|
||||
_, _ = s.repliesCache.Get(comment.ID, func() (struct{}, error) { return struct{}{}, nil })
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -639,17 +642,23 @@ func (s *DataStore) ValidateComment(c *store.Comment) error {
|
||||
if c.Orig == "" {
|
||||
return fmt.Errorf("empty comment text")
|
||||
}
|
||||
if len([]rune(c.Orig)) > maxSize {
|
||||
return fmt.Errorf("comment text exceeded max allowed size %d (%d)", maxSize, len([]rune(c.Orig)))
|
||||
commentLength := len([]rune(c.Orig))
|
||||
if commentLength > maxSize {
|
||||
return fmt.Errorf("comment text exceeded max allowed size %d (%d)", maxSize, commentLength)
|
||||
}
|
||||
if s.MinCommentSize > 0 && commentLength < s.MinCommentSize {
|
||||
return fmt.Errorf("comment text is smaller than min allowed size %d (%d)", s.MinCommentSize, commentLength)
|
||||
}
|
||||
if c.User.ID == "" || c.User.Name == "" {
|
||||
return fmt.Errorf("empty user info")
|
||||
}
|
||||
|
||||
mdExt, rend := store.GetMdExtensionsAndRenderer()
|
||||
// for validation purposes it's not important if SmartyPants formatting is disabled or enabled,
|
||||
// while for storing the comment that flag is set based on user preference
|
||||
mdExt, rend := store.GetMdExtensionsAndRenderer(false)
|
||||
parser := bf.New(bf.WithRenderer(rend), bf.WithExtensions(bf.CommonExtensions), bf.WithExtensions(mdExt))
|
||||
var wrongLinkError error
|
||||
parser.Parse([]byte(c.Orig)).Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
|
||||
parser.Parse([]byte(c.Orig)).Walk(func(node *bf.Node, _ bool) bf.WalkStatus {
|
||||
if len(node.LinkData.Destination) != 0 &&
|
||||
!(strings.HasPrefix(string(node.LinkData.Destination), "http://") ||
|
||||
strings.HasPrefix(string(node.LinkData.Destination), "https://") ||
|
||||
@@ -754,7 +763,23 @@ func (s *DataStore) Info(locator store.Locator, readonlyAge int) (store.PostInfo
|
||||
if len(res) == 0 {
|
||||
return store.PostInfo{}, fmt.Errorf("post %+v not found", locator)
|
||||
}
|
||||
return res[0], nil
|
||||
// URL request
|
||||
if locator.URL != "" {
|
||||
return res[0], nil
|
||||
}
|
||||
// site-wide request which returned multiple store.PostInfo, so that URL and ReadOnly flags don't make sense
|
||||
var info store.PostInfo
|
||||
for _, i := range res {
|
||||
info.Count += i.Count
|
||||
if info.FirstTS.IsZero() || i.FirstTS.Before(info.FirstTS) {
|
||||
info.FirstTS = i.FirstTS
|
||||
}
|
||||
if info.LastTS.IsZero() || i.LastTS.After(info.LastTS) {
|
||||
info.LastTS = i.LastTS
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
|
||||
}
|
||||
|
||||
// Delete comment by id
|
||||
@@ -773,6 +798,34 @@ func (s *DataStore) Delete(locator store.Locator, commentID string, mode store.D
|
||||
s.repliesCache.Delete(commentID)
|
||||
s.repliesCache.Delete(comment.ParentID)
|
||||
}
|
||||
|
||||
// delete images from the comment if they are not reused elsewhere in comments to the same page
|
||||
idsFn := func() []string { // get IDs of all images from the same URL to verify if image from deleted comment was reused
|
||||
comments, e := s.Engine.Find(engine.FindRequest{Locator: locator})
|
||||
if e != nil {
|
||||
log.Printf("[WARN] can't get comments %s text for deleted comment image check, %v", comment.ID, err)
|
||||
return nil
|
||||
}
|
||||
var imgIDs = []string{}
|
||||
for _, cc := range comments {
|
||||
// exclude the comment we are deleting
|
||||
if cc.ID != commentID {
|
||||
imgIDs = append(imgIDs, s.ImageService.ExtractPictures(cc.Text)...)
|
||||
}
|
||||
}
|
||||
return imgIDs
|
||||
}
|
||||
commentImgIDs := s.ImageService.ExtractPictures(comment.Text)
|
||||
pageImgIDs := idsFn()
|
||||
for _, id := range commentImgIDs {
|
||||
if !slices.Contains(pageImgIDs, id) {
|
||||
if err := s.ImageService.Delete(id); err != nil {
|
||||
log.Printf("[WARN] failed to delete image %s on comment %s deletion, %v", id, commentID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("[ERROR] commentImgIDs: %v, pageImgIDs: %v", commentImgIDs, pageImgIDs)
|
||||
|
||||
req := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: mode}
|
||||
return s.Engine.Delete(req)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
@@ -131,7 +132,7 @@ func TestService_CreateFromPartialWithTitle(t *testing.T) {
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
b := DataStore{Engine: eng, AdminStore: ks,
|
||||
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second})}
|
||||
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{"127.0.0.1"})}
|
||||
defer b.Close()
|
||||
|
||||
postPath := "/post/42"
|
||||
@@ -171,6 +172,50 @@ func TestService_CreateFromPartialWithTitle(t *testing.T) {
|
||||
assert.Equal(t, "post blah", res.PostTitle, "keep comment title")
|
||||
}
|
||||
|
||||
func TestService_Put(t *testing.T) {
|
||||
ks := admin.NewStaticKeyStore("secret 123")
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
b := DataStore{Engine: eng, AdminStore: ks}
|
||||
|
||||
comment := store.Comment{
|
||||
ID: "c-1",
|
||||
ParentID: "id-1",
|
||||
Text: "test text",
|
||||
User: store.User{ID: "user2", Name: "user name 2"},
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
}
|
||||
_, err := b.Create(comment)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create new comment with everything different to replace the first one with fields below
|
||||
updatedComment := store.Comment{
|
||||
ID: "c-1",
|
||||
ParentID: "id-new",
|
||||
Text: "new text",
|
||||
User: store.User{ID: "user3", Name: "user name 3"},
|
||||
Locator: store.Locator{URL: "https://example.com", SiteID: "example"},
|
||||
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
}
|
||||
|
||||
err = b.Put(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, updatedComment)
|
||||
require.NoError(t, err)
|
||||
|
||||
// request with wrong user, should not affect the comment user
|
||||
got, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "c-1", store.User{ID: "user1", Name: "user name 1"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "c-1", got.ID)
|
||||
assert.Equal(t, "new text", got.Text)
|
||||
assert.Equal(t, "id-1", got.ParentID, "should be unaltered")
|
||||
assert.Equal(t, "user2", got.User.ID, "should be unaltered")
|
||||
assert.Equal(t, "user name 2", got.User.Name, "should be unaltered")
|
||||
assert.Equal(t, "https://radio-t.com", got.Locator.URL, "should be unaltered")
|
||||
assert.Equal(t, "radio-t", got.Locator.SiteID, "should be unaltered")
|
||||
assert.Equal(t, time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local), got.Timestamp, "should be unaltered")
|
||||
|
||||
}
|
||||
|
||||
func TestService_SetTitle(t *testing.T) {
|
||||
var titleEnable int32
|
||||
tss := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -195,7 +240,7 @@ func TestService_SetTitle(t *testing.T) {
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
b := DataStore{Engine: eng, AdminStore: ks,
|
||||
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second})}
|
||||
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{"127.0.0.1"})}
|
||||
defer b.Close()
|
||||
comment := store.Comment{
|
||||
Text: "text",
|
||||
@@ -545,6 +590,111 @@ func TestService_VoteControversy(t *testing.T) {
|
||||
assert.InDelta(t, 1.73, res[0].Controversy, 0.01)
|
||||
}
|
||||
|
||||
func TestService_RestrictedWords(t *testing.T) {
|
||||
ks := admin.NewStaticKeyStore("secret 123")
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
restictedWordLister := StaticRestrictedWordsLister{Words: []string{"restricted"}}
|
||||
b := DataStore{Engine: eng, AdminStore: ks, RestrictedWordsMatcher: NewRestrictedWordsMatcher(restictedWordLister)}
|
||||
|
||||
// test creating a comment with restricted words which should fail with appropriate error
|
||||
reply := store.Comment{
|
||||
ID: "c-1",
|
||||
ParentID: "id-1",
|
||||
Text: "restricted word",
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user2", Name: "user name 2"},
|
||||
}
|
||||
id, err := b.Create(reply)
|
||||
assert.EqualError(t, err, ErrRestrictedWordsFound.Error(), "should fail with RestrictedWordError")
|
||||
assert.Empty(t, id)
|
||||
}
|
||||
|
||||
func TestDataStore_AdminStoreErrors(t *testing.T) {
|
||||
badKey := true
|
||||
badEnabled := true
|
||||
as := admin.StoreMock{
|
||||
OnEventFunc: func(string, admin.EventType) error { return errors.New("err") },
|
||||
KeyFunc: func(string) (string, error) {
|
||||
if badKey {
|
||||
return "", errors.New("mock key err")
|
||||
}
|
||||
return "secret", nil
|
||||
},
|
||||
EnabledFunc: func(string) (bool, error) {
|
||||
if badEnabled {
|
||||
return false, errors.New("mock enabled err")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
AdminsFunc: func(string) ([]string, error) { return nil, errors.New("mock admins err") },
|
||||
}
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
b := DataStore{Engine: eng, AdminStore: &as, MaxVotes: -1}
|
||||
comment := store.Comment{
|
||||
ID: "c-1",
|
||||
ParentID: "id-1",
|
||||
Text: "restricted word",
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user2", Name: "user name 2"},
|
||||
}
|
||||
|
||||
// Key call error
|
||||
id, err := b.Create(comment)
|
||||
assert.ErrorContainsf(t, err, "mock key err", "should fail with mock error")
|
||||
assert.Empty(t, id)
|
||||
assert.Equal(t, len(as.KeyCalls()), 1)
|
||||
assert.Equal(t, len(as.EnabledCalls()), 0)
|
||||
assert.Equal(t, len(as.OnEventCalls()), 0)
|
||||
|
||||
// Enabled call error
|
||||
badKey = false
|
||||
id, err = b.Create(comment)
|
||||
assert.ErrorContains(t, err, "mock enabled err", "should fail with mock error")
|
||||
assert.Empty(t, id)
|
||||
assert.Equal(t, len(as.KeyCalls()), 2)
|
||||
assert.Equal(t, len(as.EnabledCalls()), 1)
|
||||
assert.Equal(t, len(as.OnEventCalls()), 0)
|
||||
|
||||
// only OnEvent error
|
||||
badEnabled = false
|
||||
id, err = b.Create(comment)
|
||||
assert.NoError(t, err, "OnEvent error should be just logged")
|
||||
assert.Equal(t, id, "c-1")
|
||||
assert.Equal(t, len(as.KeyCalls()), 3)
|
||||
assert.Equal(t, len(as.EnabledCalls()), 2)
|
||||
assert.Equal(t, len(as.OnEventCalls()), 1)
|
||||
|
||||
// OnEvent error on Vote call
|
||||
_, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "c-1",
|
||||
UserID: "user4", Val: true})
|
||||
assert.NoError(t, err, "OnEvent error should be just logged")
|
||||
assert.Equal(t, len(as.KeyCalls()), 4)
|
||||
assert.Equal(t, len(as.EnabledCalls()), 3)
|
||||
assert.Equal(t, len(as.OnEventCalls()), 2)
|
||||
|
||||
// Admins error
|
||||
isAdmin := b.IsAdmin("radio-t", "user2")
|
||||
assert.False(t, isAdmin)
|
||||
assert.Equal(t, len(as.AdminsCalls()), 1)
|
||||
assert.Equal(t, len(as.OnEventCalls()), 2)
|
||||
|
||||
// OnEvent error on EditComment call
|
||||
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "c-1", EditRequest{Text: "new text"})
|
||||
assert.NoError(t, err, "OnEvent error should be just logged")
|
||||
|
||||
// OnEvent error on Delete call
|
||||
err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "c-1", store.SoftDelete)
|
||||
assert.NoError(t, err, "OnEvent error should be just logged")
|
||||
|
||||
// OnEvent error on EditComment Delete call
|
||||
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "c-1", EditRequest{Delete: true})
|
||||
assert.NoError(t, err, "OnEvent error should be just logged")
|
||||
}
|
||||
|
||||
func TestService_VoteSameIP(t *testing.T) {
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
@@ -774,7 +924,7 @@ func TestService_EditCommentAdmin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_ValidateComment(t *testing.T) {
|
||||
b := DataStore{MaxCommentSize: 2000, AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
b := DataStore{MinCommentSize: 6, MaxCommentSize: 2000, AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
longText := fmt.Sprintf("%4000s", "X")
|
||||
|
||||
tbl := []struct {
|
||||
@@ -784,6 +934,7 @@ func TestService_ValidateComment(t *testing.T) {
|
||||
{inp: store.Comment{}, err: "empty comment text"},
|
||||
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid", Name: "name"}}, err: ""},
|
||||
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid"}}, err: "empty user info"},
|
||||
{inp: store.Comment{Orig: "short", User: store.User{ID: "myid", Name: "name"}}, err: "comment text is smaller than min allowed size 6 (5)"},
|
||||
{inp: store.Comment{Orig: longText, User: store.User{ID: "myid", Name: "name"}}, err: "comment text exceeded max allowed size 2000 (4000)"},
|
||||
{inp: store.Comment{Orig: "here is a link with relative URL: [google.com](url)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
|
||||
{inp: store.Comment{Orig: "here is a link with relative URL: [google.com](url)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
|
||||
@@ -1156,7 +1307,7 @@ func TestService_Find(t *testing.T) {
|
||||
assert.InDelta(t, 0, res[1].Controversy, 0.01)
|
||||
|
||||
// make sure title sanitized
|
||||
assert.Equal(t, "some title, <a href=\"http://radio-t.com\" rel=\"nofollow\">link</a>", res[0].PostTitle)
|
||||
assert.Equal(t, "some title, link", res[0].PostTitle)
|
||||
}
|
||||
|
||||
func TestService_FindSince(t *testing.T) {
|
||||
@@ -1185,18 +1336,55 @@ func TestService_Info(t *testing.T) {
|
||||
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
|
||||
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
|
||||
|
||||
info, err := b.Info(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, 0)
|
||||
// add one more comment for another URL to test site-wide Info request
|
||||
comment := store.Comment{
|
||||
ID: "123456xyz",
|
||||
Text: `some text, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com/another", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user2", Name: "user name"},
|
||||
}
|
||||
_, err := b.Create(comment)
|
||||
require.NoError(t, err)
|
||||
|
||||
// get non-existing URL info
|
||||
info, err := b.Info(store.Locator{URL: "bad", SiteID: "radio-t"}, 0)
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, info)
|
||||
|
||||
// get non-existing site info
|
||||
info, err = b.Info(store.Locator{SiteID: "bad"}, 0)
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, info)
|
||||
|
||||
// test two initially created comments and store first comment FirstTS
|
||||
info, err = b.Info(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://radio-t.com", info.URL)
|
||||
assert.Equal(t, 2, info.Count)
|
||||
assert.False(t, info.ReadOnly)
|
||||
assert.True(t, info.LastTS.After(info.FirstTS))
|
||||
firstTS := info.FirstTS
|
||||
|
||||
time.Sleep(1 * time.Second) // make post RO in 1sec
|
||||
info, err = b.Info(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://radio-t.com", info.URL)
|
||||
assert.True(t, info.ReadOnly)
|
||||
|
||||
// get last created comment LastTS
|
||||
info, err = b.Info(store.Locator{URL: "https://radio-t.com/another", SiteID: "radio-t"}, 0)
|
||||
require.NoError(t, err)
|
||||
lastTS := info.LastTS
|
||||
|
||||
// site-level request
|
||||
info, err = b.Info(store.Locator{SiteID: "radio-t"}, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, info.Count)
|
||||
assert.Empty(t, info.URL, "site-level request should not set URL")
|
||||
assert.False(t, info.ReadOnly, "site-level request should not set ReadOnly")
|
||||
assert.Equal(t, firstTS, info.FirstTS, "site-level request should have FirstTS from the first post")
|
||||
assert.Equal(t, lastTS, info.LastTS, "site-level request should have LastTS from the last post")
|
||||
}
|
||||
|
||||
func TestService_Delete(t *testing.T) {
|
||||
@@ -1218,6 +1406,80 @@ func TestService_Delete(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestService_deleteImagesOnCommentDelete(t *testing.T) {
|
||||
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
|
||||
|
||||
mockStore := image.StoreMock{
|
||||
DeleteFunc: func(string) error { return nil },
|
||||
CommitFunc: func(string) error { return nil },
|
||||
ResetCleanupTimerFunc: func(string) error { return nil },
|
||||
}
|
||||
imgSvc := image.NewService(&mockStore,
|
||||
image.ServiceParams{
|
||||
EditDuration: 50 * time.Millisecond,
|
||||
ImageAPI: "/images/dev/",
|
||||
ProxyAPI: "/non_existent",
|
||||
})
|
||||
defer imgSvc.Close(context.TODO())
|
||||
|
||||
// two comments for https://radio-t.com
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
b := DataStore{Engine: eng, EditDuration: 50 * time.Millisecond,
|
||||
AdminStore: admin.NewStaticKeyStore("secret 123"), ImageService: imgSvc}
|
||||
|
||||
c := store.Comment{
|
||||
ID: "id-22",
|
||||
Text: `some text <img src="/images/dev/pic1.png"/> xx <img src="/images/dev/pic2.png"/>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err := b.Engine.Create(c) // create directly with engine, doesn't call submitImages
|
||||
assert.NoError(t, err)
|
||||
b.submitImages(c)
|
||||
// reply to the first comment with one new image and one existing one
|
||||
c = store.Comment{
|
||||
ID: "id-23",
|
||||
ParentID: "id-22",
|
||||
Text: `some text <img src="/images/dev/pic2.png"/> xx <img src="/images/dev/pic3.png"/>`,
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err = b.Engine.Create(c) // create directly with engine, doesn't call submitImages
|
||||
assert.NoError(t, err)
|
||||
b.submitImages(c)
|
||||
|
||||
// verify that images are in staging store
|
||||
assert.Equal(t, 4, len(mockStore.ResetCleanupTimerCalls()))
|
||||
assert.Equal(t, "dev/pic1.png", mockStore.ResetCleanupTimerCalls()[0].ID)
|
||||
assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[1].ID)
|
||||
assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[2].ID)
|
||||
assert.Equal(t, "dev/pic3.png", mockStore.ResetCleanupTimerCalls()[3].ID)
|
||||
time.Sleep(b.EditDuration + 100*time.Millisecond)
|
||||
// verify that they got into the main store
|
||||
assert.Equal(t, 4, len(mockStore.CommitCalls()))
|
||||
assert.Equal(t, "dev/pic1.png", mockStore.CommitCalls()[0].ID)
|
||||
assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[1].ID)
|
||||
assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[2].ID)
|
||||
assert.Equal(t, "dev/pic3.png", mockStore.CommitCalls()[3].ID)
|
||||
|
||||
// delete the first comment
|
||||
err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-22", store.SoftDelete)
|
||||
assert.NoError(t, err)
|
||||
// verify that images are deleted from the main store
|
||||
assert.Equal(t, 1, len(mockStore.DeleteCalls()))
|
||||
assert.Equal(t, "dev/pic1.png", mockStore.DeleteCalls()[0].ID)
|
||||
|
||||
// delete the second comment
|
||||
err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-23", store.SoftDelete)
|
||||
assert.NoError(t, err)
|
||||
// verify that images are deleted from the main store
|
||||
assert.Equal(t, 3, len(mockStore.DeleteCalls()))
|
||||
assert.Equal(t, "dev/pic2.png", mockStore.DeleteCalls()[1].ID)
|
||||
assert.Equal(t, "dev/pic3.png", mockStore.DeleteCalls()[2].ID)
|
||||
}
|
||||
|
||||
// DeleteUser removes all comments from user
|
||||
func TestService_DeleteUser(t *testing.T) {
|
||||
// two comments for https://radio-t.com, no reply
|
||||
@@ -1398,8 +1660,8 @@ func TestService_submitImages(t *testing.T) {
|
||||
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
|
||||
|
||||
mockStore := image.StoreMock{
|
||||
CommitFunc: func(id string) error { return nil },
|
||||
ResetCleanupTimerFunc: func(id string) error { return nil },
|
||||
CommitFunc: func(string) error { return nil },
|
||||
ResetCleanupTimerFunc: func(string) error { return nil },
|
||||
}
|
||||
imgSvc := image.NewService(&mockStore,
|
||||
image.ServiceParams{
|
||||
@@ -1440,10 +1702,10 @@ func TestService_ResubmitStagingImages(t *testing.T) {
|
||||
InfoFunc: func() (image.StoreInfo, error) {
|
||||
return image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil
|
||||
},
|
||||
CommitFunc: func(id string) error {
|
||||
CommitFunc: func(string) error {
|
||||
return nil
|
||||
},
|
||||
ResetCleanupTimerFunc: func(id string) error { return nil },
|
||||
ResetCleanupTimerFunc: func(string) error { return nil },
|
||||
}
|
||||
imgSvc := image.NewService(&mockStore,
|
||||
image.ServiceParams{
|
||||
@@ -1535,7 +1797,7 @@ func TestService_ResubmitStagingImages_EngineError(t *testing.T) {
|
||||
|
||||
first := true
|
||||
engineMock := engine.InterfaceMock{
|
||||
FindFunc: func(req engine.FindRequest) ([]store.Comment, error) {
|
||||
FindFunc: func(engine.FindRequest) ([]store.Comment, error) {
|
||||
if first {
|
||||
first = false
|
||||
return nil, nil
|
||||
@@ -1560,7 +1822,7 @@ func TestService_ResubmitStagingImages_EngineError(t *testing.T) {
|
||||
|
||||
func TestService_alterComment(t *testing.T) {
|
||||
engineMock := engine.InterfaceMock{
|
||||
FlagFunc: func(req engine.FlagRequest) (bool, error) {
|
||||
FlagFunc: func(engine.FlagRequest) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
@@ -1582,7 +1844,7 @@ func TestService_alterComment(t *testing.T) {
|
||||
|
||||
first := true
|
||||
engineMock = engine.InterfaceMock{
|
||||
FlagFunc: func(req engine.FlagRequest) (bool, error) {
|
||||
FlagFunc: func(engine.FlagRequest) (bool, error) {
|
||||
if first {
|
||||
first = false
|
||||
return false, nil
|
||||
@@ -1600,7 +1862,7 @@ func TestService_alterComment(t *testing.T) {
|
||||
|
||||
first = true
|
||||
engineMock = engine.InterfaceMock{
|
||||
FlagFunc: func(req engine.FlagRequest) (bool, error) {
|
||||
FlagFunc: func(engine.FlagRequest) (bool, error) {
|
||||
if first {
|
||||
first = false
|
||||
return true, nil
|
||||
@@ -1658,10 +1920,10 @@ func TestService_DoubleClose_Static(t *testing.T) {
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
b := DataStore{Engine: eng, AdminStore: ks,
|
||||
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second})}
|
||||
b.Close()
|
||||
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{})}
|
||||
assert.NoError(t, b.Close())
|
||||
// second call should not result in panic or errors
|
||||
b.Close()
|
||||
assert.NoError(t, b.Close())
|
||||
}
|
||||
|
||||
// makes new boltdb, put two records
|
||||
|
||||
+2
-8
@@ -242,11 +242,5 @@
|
||||
"time": "2017-12-25T19:47:22Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"url": "url",
|
||||
"count": 12,
|
||||
"first_time": "2017-12-25T19:46:01Z",
|
||||
"last_time": "2017-12-25T19:47:22Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+2
-8
@@ -176,11 +176,5 @@
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}],
|
||||
"info": {
|
||||
"url": "url",
|
||||
"count": 8,
|
||||
"first_time": "2017-12-25T19:46:01Z",
|
||||
"last_time": "2017-12-25T19:47:05Z"
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/lcw"
|
||||
"github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
@@ -19,32 +20,53 @@ const (
|
||||
|
||||
// TitleExtractor gets html title from remote page, cached
|
||||
type TitleExtractor struct {
|
||||
client http.Client
|
||||
cache lcw.LoadingCache
|
||||
client http.Client
|
||||
cache lcw.LoadingCache[string]
|
||||
allowedDomains []string
|
||||
}
|
||||
|
||||
// NewTitleExtractor makes extractor with cache. If memory cache failed, switching to no-cache
|
||||
func NewTitleExtractor(client http.Client) *TitleExtractor {
|
||||
func NewTitleExtractor(client http.Client, allowedDomains []string) *TitleExtractor {
|
||||
log.Printf("[DEBUG] creating extractor, allowed domains %+v", allowedDomains)
|
||||
res := TitleExtractor{
|
||||
client: client,
|
||||
client: client,
|
||||
allowedDomains: allowedDomains,
|
||||
}
|
||||
var err error
|
||||
res.cache, err = lcw.NewExpirableCache(lcw.TTL(teCacheTTL), lcw.MaxKeySize(teCacheMaxRecs))
|
||||
o := lcw.NewOpts[string]()
|
||||
res.cache, err = lcw.NewExpirableCache(o.TTL(teCacheTTL), o.MaxKeySize(teCacheMaxRecs))
|
||||
if err != nil {
|
||||
log.Printf("[WARN] failed to make cache, caching disabled for titles, %v", err)
|
||||
res.cache = &lcw.Nop{}
|
||||
res.cache = &lcw.Nop[string]{}
|
||||
}
|
||||
return &res
|
||||
}
|
||||
|
||||
// Get page for url and return title
|
||||
func (t *TitleExtractor) Get(url string) (string, error) {
|
||||
func (t *TitleExtractor) Get(pageURL string) (string, error) {
|
||||
// parse domain of the URL and check if it's in the allowed list
|
||||
u, err := url.Parse(pageURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse url %s: %w", pageURL, err)
|
||||
}
|
||||
allowed := false
|
||||
for _, domain := range t.allowedDomains {
|
||||
if u.Hostname() == domain ||
|
||||
(strings.HasSuffix(u.Hostname(), domain) && // suffix match, e.g. "example.com" matches "www.example.com"
|
||||
u.Hostname()[len(u.Hostname())-len(domain)-1] == '.') { // but we should not match "notexample.com"
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return "", fmt.Errorf("domain %s is not allowed", u.Host)
|
||||
}
|
||||
client := http.Client{Timeout: t.client.Timeout, Transport: t.client.Transport}
|
||||
defer client.CloseIdleConnections()
|
||||
b, err := t.cache.Get(url, func() (interface{}, error) {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load page %s: %w", url, err)
|
||||
b, err := t.cache.Get(pageURL, func() (string, error) {
|
||||
resp, e := client.Get(pageURL)
|
||||
if e != nil {
|
||||
return "", fmt.Errorf("failed to load page %s: %w", pageURL, e)
|
||||
}
|
||||
defer func() {
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
@@ -52,23 +74,23 @@ func (t *TitleExtractor) Get(url string) (string, error) {
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("can't load page %s, code %d", url, resp.StatusCode)
|
||||
return "", fmt.Errorf("can't load page %s, code %d", pageURL, resp.StatusCode)
|
||||
}
|
||||
|
||||
title, ok := t.getTitle(resp.Body)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't get title for %s", url)
|
||||
return "", fmt.Errorf("can't get title for %s", pageURL)
|
||||
}
|
||||
return title, nil
|
||||
})
|
||||
|
||||
// on error save result (empty string) to cache too and return "" title
|
||||
if err != nil {
|
||||
_, _ = t.cache.Get(url, func() (interface{}, error) { return "", nil })
|
||||
_, _ = t.cache.Get(pageURL, func() (string, error) { return "", nil })
|
||||
return "", err
|
||||
}
|
||||
|
||||
return b.(string), nil
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Close title extractor
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestTitle_GetTitle(t *testing.T) {
|
||||
{`<html><body> 2222</body></html>`, false, ""},
|
||||
}
|
||||
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second})
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{})
|
||||
defer ex.Close()
|
||||
for i, tt := range tbl {
|
||||
tt := tt
|
||||
@@ -41,7 +41,7 @@ func TestTitle_GetTitle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTitle_Get(t *testing.T) {
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second})
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{"127.0.0.1"})
|
||||
defer ex.Close()
|
||||
var hits int32
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -75,7 +75,7 @@ func TestTitle_GetConcurrent(t *testing.T) {
|
||||
for n := 0; n < 1000; n++ {
|
||||
body += "something something blah blah\n"
|
||||
}
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second})
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{"127.0.0.1"})
|
||||
defer ex.Close()
|
||||
var hits int32
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -104,10 +104,10 @@ func TestTitle_GetConcurrent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTitle_GetFailed(t *testing.T) {
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second})
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{"127.0.0.1"})
|
||||
defer ex.Close()
|
||||
var hits int32
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
atomic.AddInt32(&hits, 1)
|
||||
w.WriteHeader(404)
|
||||
}))
|
||||
@@ -124,9 +124,9 @@ func TestTitle_GetFailed(t *testing.T) {
|
||||
assert.Equal(t, int32(1), atomic.LoadInt32(&hits), "hit once, errors cached")
|
||||
}
|
||||
|
||||
func TestTitle_DoubleClosed(*testing.T) {
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second})
|
||||
ex.Close()
|
||||
func TestTitle_DoubleClosed(t *testing.T) {
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{})
|
||||
assert.NoError(t, ex.Close())
|
||||
// second call should not result in panic
|
||||
ex.Close()
|
||||
assert.NoError(t, ex.Close())
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import (
|
||||
|
||||
// Tree is formatter making tree from the list of comments
|
||||
type Tree struct {
|
||||
Nodes []*Node `json:"comments"`
|
||||
Info store.PostInfo `json:"info,omitempty"`
|
||||
Nodes []*Node `json:"comments"`
|
||||
}
|
||||
|
||||
// Node is a comment with optional replies
|
||||
@@ -30,22 +29,12 @@ type recurData struct {
|
||||
}
|
||||
|
||||
// MakeTree gets unsorted list of comments and produces Tree
|
||||
// It will make store.PostInfo by itself and will mark Info.ReadOnly based on passed readOnlyAge
|
||||
// Tree maker is local and has no access to the data store. By this reason it has to make Info and won't be able
|
||||
// to handle store's read-only status. This status should be set by caller.
|
||||
func MakeTree(comments []store.Comment, sortType string, readOnlyAge int) *Tree {
|
||||
func MakeTree(comments []store.Comment, sortType string) *Tree {
|
||||
if len(comments) == 0 {
|
||||
return &Tree{}
|
||||
}
|
||||
|
||||
res := Tree{
|
||||
Info: store.PostInfo{
|
||||
URL: comments[0].Locator.URL,
|
||||
FirstTS: comments[0].Timestamp,
|
||||
LastTS: comments[0].Timestamp,
|
||||
},
|
||||
}
|
||||
res.Info.Count = len(res.filter(comments, func(c store.Comment) bool { return !c.Deleted }))
|
||||
res := Tree{}
|
||||
|
||||
topComments := res.filter(comments, func(c store.Comment) bool { return c.ParentID == "" })
|
||||
|
||||
@@ -54,23 +43,12 @@ func MakeTree(comments []store.Comment, sortType string, readOnlyAge int) *Tree
|
||||
node := Node{Comment: rootComment}
|
||||
|
||||
rd := recurData{}
|
||||
commentsTree, tsModified, tsCreated := res.proc(comments, &node, &rd, rootComment.ID)
|
||||
// skip deleted with no sub-comments ar all sub-comments deleted
|
||||
commentsTree := res.proc(comments, &node, &rd, rootComment.ID)
|
||||
// skip deleted with no sub-comments and all sub-comments deleted
|
||||
if rootComment.Deleted && (len(commentsTree.Replies) == 0 || !rd.visible) {
|
||||
continue
|
||||
}
|
||||
|
||||
commentsTree.tsModified, commentsTree.tsCreated = tsModified, tsCreated
|
||||
if commentsTree.tsCreated.Before(res.Info.FirstTS) {
|
||||
res.Info.FirstTS = commentsTree.tsCreated
|
||||
}
|
||||
if commentsTree.tsModified.After(res.Info.LastTS) {
|
||||
res.Info.LastTS = commentsTree.tsModified
|
||||
}
|
||||
|
||||
res.Info.ReadOnly = readOnlyAge > 0 && !res.Info.FirstTS.IsZero() &&
|
||||
res.Info.FirstTS.AddDate(0, 0, readOnlyAge).Before(time.Now())
|
||||
|
||||
res.Nodes = append(res.Nodes, commentsTree)
|
||||
}
|
||||
|
||||
@@ -79,7 +57,7 @@ func MakeTree(comments []store.Comment, sortType string, readOnlyAge int) *Tree
|
||||
}
|
||||
|
||||
// proc makes tree for one top-level comment recursively
|
||||
func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentID string) (result *Node, modified, created time.Time) {
|
||||
func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentID string) (result *Node) {
|
||||
if rd.tsModified.IsZero() || rd.tsCreated.IsZero() {
|
||||
rd.tsModified, rd.tsCreated = node.Comment.Timestamp, node.Comment.Timestamp
|
||||
}
|
||||
@@ -106,7 +84,8 @@ func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentI
|
||||
sort.Slice(node.Replies, func(i, j int) bool {
|
||||
return node.Replies[i].Comment.Timestamp.Before(node.Replies[j].Comment.Timestamp)
|
||||
})
|
||||
return node, rd.tsModified, rd.tsCreated
|
||||
node.tsModified, node.tsCreated = rd.tsModified, rd.tsCreated
|
||||
return node
|
||||
}
|
||||
|
||||
// filter returns comments for parentID
|
||||
|
||||
@@ -37,19 +37,15 @@ func TestMakeTree(t *testing.T) {
|
||||
{Locator: loc, ID: "611", ParentID: "61", Deleted: true},
|
||||
}
|
||||
|
||||
res := MakeTree(comments, "time", 0)
|
||||
res := MakeTree(comments, "time")
|
||||
resJSON, err := json.Marshal(&res)
|
||||
require.NoError(t, err)
|
||||
|
||||
expJSON := mustLoadJSONFile(t, "testdata/tree.json")
|
||||
assert.Equal(t, expJSON, resJSON)
|
||||
assert.Equal(t, store.PostInfo{URL: "url", Count: 12, FirstTS: ts(46, 1), LastTS: ts(47, 22)}, res.Info)
|
||||
|
||||
res = MakeTree([]store.Comment{}, "time", 0)
|
||||
res = MakeTree([]store.Comment{}, "time")
|
||||
assert.Equal(t, &Tree{}, res)
|
||||
|
||||
res = MakeTree(comments, "time", 10)
|
||||
assert.Equal(t, store.PostInfo{URL: "url", Count: 12, FirstTS: ts(46, 1), LastTS: ts(47, 22), ReadOnly: true}, res.Info)
|
||||
}
|
||||
|
||||
func TestMakeEmptySubtree(t *testing.T) {
|
||||
@@ -79,7 +75,7 @@ func TestMakeEmptySubtree(t *testing.T) {
|
||||
{Locator: loc, ID: "3", Timestamp: ts(48, 1), Deleted: true}, // deleted top level
|
||||
}
|
||||
|
||||
res := MakeTree(comments, "time", 0)
|
||||
res := MakeTree(comments, "time")
|
||||
resJSON, err := json.Marshal(&res)
|
||||
require.NoError(t, err)
|
||||
t.Log(string(resJSON))
|
||||
@@ -108,50 +104,50 @@ func TestTreeSortNodes(t *testing.T) {
|
||||
{ID: "5", Deleted: true, Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 150, time.UTC)},
|
||||
}
|
||||
|
||||
res := MakeTree(comments, "+active", 0)
|
||||
res := MakeTree(comments, "+active")
|
||||
assert.Equal(t, "2", res.Nodes[0].Comment.ID)
|
||||
t.Log(res.Nodes[0].Comment.ID, res.Nodes[0].tsModified)
|
||||
|
||||
res = MakeTree(comments, "-active", 0)
|
||||
res = MakeTree(comments, "-active")
|
||||
t.Log(res.Nodes[0].Comment.ID, res.Nodes[0].tsModified)
|
||||
assert.Equal(t, "1", res.Nodes[0].Comment.ID)
|
||||
|
||||
res = MakeTree(comments, "+time", 0)
|
||||
res = MakeTree(comments, "+time")
|
||||
t.Log(res.Nodes[0].Comment.ID, res.Nodes[0].tsModified)
|
||||
assert.Equal(t, "1", res.Nodes[0].Comment.ID)
|
||||
|
||||
res = MakeTree(comments, "-time", 0)
|
||||
res = MakeTree(comments, "-time")
|
||||
assert.Equal(t, "6", res.Nodes[0].Comment.ID)
|
||||
|
||||
res = MakeTree(comments, "score", 0)
|
||||
res = MakeTree(comments, "score")
|
||||
assert.Equal(t, "4", res.Nodes[0].Comment.ID)
|
||||
assert.Equal(t, "3", res.Nodes[1].Comment.ID)
|
||||
assert.Equal(t, "6", res.Nodes[2].Comment.ID)
|
||||
assert.Equal(t, "1", res.Nodes[3].Comment.ID)
|
||||
|
||||
res = MakeTree(comments, "+score", 0)
|
||||
res = MakeTree(comments, "+score")
|
||||
assert.Equal(t, "4", res.Nodes[0].Comment.ID)
|
||||
|
||||
res = MakeTree(comments, "-score", 0)
|
||||
res = MakeTree(comments, "-score")
|
||||
assert.Equal(t, "2", res.Nodes[0].Comment.ID)
|
||||
assert.Equal(t, "1", res.Nodes[1].Comment.ID)
|
||||
assert.Equal(t, "3", res.Nodes[2].Comment.ID)
|
||||
assert.Equal(t, "6", res.Nodes[3].Comment.ID)
|
||||
|
||||
res = MakeTree(comments, "+controversy", 0)
|
||||
res = MakeTree(comments, "+controversy")
|
||||
assert.Equal(t, "3", res.Nodes[0].Comment.ID)
|
||||
assert.Equal(t, "6", res.Nodes[1].Comment.ID)
|
||||
assert.Equal(t, "2", res.Nodes[2].Comment.ID)
|
||||
assert.Equal(t, "4", res.Nodes[3].Comment.ID)
|
||||
assert.Equal(t, "1", res.Nodes[4].Comment.ID)
|
||||
|
||||
res = MakeTree(comments, "-controversy", 0)
|
||||
res = MakeTree(comments, "-controversy")
|
||||
assert.Equal(t, "1", res.Nodes[0].Comment.ID)
|
||||
assert.Equal(t, "4", res.Nodes[1].Comment.ID)
|
||||
assert.Equal(t, "2", res.Nodes[2].Comment.ID)
|
||||
assert.Equal(t, "3", res.Nodes[3].Comment.ID)
|
||||
|
||||
res = MakeTree(comments, "undefined", 0)
|
||||
res = MakeTree(comments, "undefined")
|
||||
t.Log(res.Nodes[0].Comment.ID, res.Nodes[0].tsModified)
|
||||
assert.Equal(t, "1", res.Nodes[0].Comment.ID)
|
||||
}
|
||||
@@ -164,7 +160,7 @@ func BenchmarkTree(b *testing.B) {
|
||||
assert.NoError(b, err)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
res := MakeTree(comments, "time", 0)
|
||||
res := MakeTree(comments, "time")
|
||||
assert.NotNil(b, res)
|
||||
}
|
||||
}
|
||||
|
||||
+39
-43
@@ -1,78 +1,74 @@
|
||||
module github.com/umputun/remark42/backend
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/Depado/bfchroma/v2 v2.0.0
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/alecthomas/chroma/v2 v2.8.0
|
||||
github.com/didip/tollbooth/v7 v7.0.1
|
||||
github.com/PuerkitoBio/goquery v1.9.2
|
||||
github.com/alecthomas/chroma/v2 v2.13.0
|
||||
github.com/didip/tollbooth/v7 v7.0.2
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-pkgz/auth v1.21.1-0.20230820223834-cea049c06aa5
|
||||
github.com/go-pkgz/auth v1.24.2
|
||||
github.com/go-pkgz/jrpc v0.3.0
|
||||
github.com/go-pkgz/lcw v1.0.2
|
||||
github.com/go-pkgz/lgr v0.11.0
|
||||
github.com/go-pkgz/notify v0.2.0
|
||||
github.com/go-pkgz/lcw/v2 v2.0.0
|
||||
github.com/go-pkgz/lgr v0.11.1
|
||||
github.com/go-pkgz/notify v1.1.1
|
||||
github.com/go-pkgz/repeater v1.1.3
|
||||
github.com/go-pkgz/rest v1.17.0
|
||||
github.com/go-pkgz/rest v1.19.0
|
||||
github.com/go-pkgz/syncs v1.3.2
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/feeds v1.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/feeds v1.2.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jessevdk/go-flags v1.5.0
|
||||
github.com/kyokomi/emoji/v2 v2.2.12
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
github.com/jessevdk/go-flags v1.6.1
|
||||
github.com/kyokomi/emoji/v2 v2.2.13
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/rs/xid v1.5.0
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/image v0.11.0
|
||||
golang.org/x/net v0.14.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.etcd.io/bbolt v1.3.10
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/net v0.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.2 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.3 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2 // indirect
|
||||
github.com/go-pkgz/email v0.4.1 // indirect
|
||||
github.com/go-pkgz/expirable-cache v1.0.0 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/go-pkgz/email v0.5.0 // indirect
|
||||
github.com/go-pkgz/expirable-cache/v3 v3.0.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/slack-go/slack v0.12.2 // indirect
|
||||
github.com/redis/go-redis/v9 v9.4.0 // indirect
|
||||
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d // indirect
|
||||
github.com/slack-go/slack v0.12.5 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||
go.mongodb.org/mongo-driver v1.12.1 // indirect
|
||||
golang.org/x/oauth2 v0.11.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.mongodb.org/mongo-driver v1.16.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+121
-155
@@ -1,60 +1,58 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
|
||||
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
github.com/Depado/bfchroma/v2 v2.0.0 h1:IRpN9BPkNwEpR6w1ectIcNWOuhDSLx+8f1pn83fzxx8=
|
||||
github.com/Depado/bfchroma/v2 v2.0.0/go.mod h1:wFwW/Pw8Tnd0irzgO9Zxtxgzp3aPS8qBWlyadxujxmw=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis/v2 v2.22.0 h1:lIHHiSkEyS1MkKHCHzN+0mWrA4YdbGdimE5iZ2sHSzo=
|
||||
github.com/alicebob/miniredis/v2 v2.22.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88=
|
||||
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
|
||||
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
|
||||
github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y=
|
||||
github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dghubble/oauth1 v0.7.2 h1:pwcinOZy8z6XkNxvPmUDY52M7RDPxt0Xw1zgZ6Cl5JA=
|
||||
github.com/dghubble/oauth1 v0.7.2/go.mod h1:9erQdIhqhOHG/7K9s/tgh9Ks/AfoyrO5mW/43Lu2+kE=
|
||||
github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE=
|
||||
github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/didip/tollbooth/v7 v7.0.0/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
|
||||
github.com/didip/tollbooth/v7 v7.0.1 h1:TkT4sBKoQoHQFPf7blQ54iHrZiTDnr8TceU+MulVAog=
|
||||
github.com/didip/tollbooth/v7 v7.0.1/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
|
||||
github.com/didip/tollbooth/v7 v7.0.2 h1:WYEfusYI6g64cN0qbZgekDrYfuYBZjUZd5+RlWi69p4=
|
||||
github.com/didip/tollbooth/v7 v7.0.2/go.mod h1:RtRYfEmFGX70+ike5kSndSvLtQ3+F2EAmTI4Un/VXNc=
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f h1:jtKwihcLmUC9BAhoJ9adCUqdSSZcOdH2KL7mPTUm2aw=
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f/go.mod h1:q9C80dnsuVRP2dAskjnXRNWdUJqtGgwG9wNrzt0019s=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
@@ -62,32 +60,37 @@ github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQKqBQ=
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
|
||||
github.com/go-pkgz/auth v1.21.1-0.20230820223834-cea049c06aa5 h1:iSiyXXA0sbLJGb1ZgR8PRTizs1wK2QZ/TXG1frUEprw=
|
||||
github.com/go-pkgz/auth v1.21.1-0.20230820223834-cea049c06aa5/go.mod h1:knVxcFhXlZ9AUvB1nlOR0kWghVBcgfFH1ucPW9CjCmE=
|
||||
github.com/go-pkgz/email v0.4.1 h1:2vtP2gibsSzqhz6eD5DklSp11m657XEVf17fuXaxMvk=
|
||||
github.com/go-pkgz/email v0.4.1/go.mod h1:BdxglsQnymzhfdbnncEE72a6DrucZHy6I+42LK2jLEc=
|
||||
github.com/go-pkgz/auth v1.24.0 h1:jo7Ke18nFGTWHdzmMKeF8zqXIC383Ptu/OXCkcQOZmI=
|
||||
github.com/go-pkgz/auth v1.24.0/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
|
||||
github.com/go-pkgz/auth v1.24.1-0.20240919232608-9e446b888187 h1:1oHLySWdk0HfDIFzXFNqA6dEK7sTgZ/7s+l/YxYql7Q=
|
||||
github.com/go-pkgz/auth v1.24.1-0.20240919232608-9e446b888187/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
|
||||
github.com/go-pkgz/auth v1.24.1 h1:izSFGxwNEZ2MujKJWXddKc+lUW+kVP02JBXouQIW8b4=
|
||||
github.com/go-pkgz/auth v1.24.1/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
|
||||
github.com/go-pkgz/auth v1.24.2-0.20240921022538-30916c085e04 h1:DZzcdFb/EzXjQex8lDq/NqubVqwCXeyGxU5swYEVAYk=
|
||||
github.com/go-pkgz/auth v1.24.2-0.20240921022538-30916c085e04/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
|
||||
github.com/go-pkgz/auth v1.24.2 h1:imMjUvTM0c8iOvP/GNGcuNcB/7gF3jFTF9dIPzlAOqI=
|
||||
github.com/go-pkgz/auth v1.24.2/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
|
||||
github.com/go-pkgz/email v0.5.0 h1:fdtMDGJ8NwyBACLR0LYHaCIK/OeUwZHMhH7Q0+oty9U=
|
||||
github.com/go-pkgz/email v0.5.0/go.mod h1:BdxglsQnymzhfdbnncEE72a6DrucZHy6I+42LK2jLEc=
|
||||
github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||
github.com/go-pkgz/expirable-cache v1.0.0 h1:ns5+1hjY8hntGv8bPaQd9Gr7Jyo+Uw5SLyII40aQdtA=
|
||||
github.com/go-pkgz/expirable-cache v1.0.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||
github.com/go-pkgz/expirable-cache/v3 v3.0.0 h1:u3/gcu3sabLYiTCevoRKv+WzjIn5oo7P8XtiXBeRDLw=
|
||||
github.com/go-pkgz/expirable-cache/v3 v3.0.0/go.mod h1:2OQiDyEGQalYecLWmXprm3maPXeVb5/6/X7yRPYTzec=
|
||||
github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U=
|
||||
github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00=
|
||||
github.com/go-pkgz/lcw v1.0.2 h1:68T8xRkVWSo1KIbMfFXBY1xbFF3XWl7YD+ub3jkUlNE=
|
||||
github.com/go-pkgz/lcw v1.0.2/go.mod h1:CPJJzunpmGToOtD0Ga82TV152eL69sYEIIPcy9fbxlU=
|
||||
github.com/go-pkgz/lgr v0.11.0 h1:9XH5o+vj09L0sRWEswIGK1lJ6g07xVB4/Z28RV9Z+qM=
|
||||
github.com/go-pkgz/lgr v0.11.0/go.mod h1:4rdRmMSs4yGFjnUg0rSDbKx21LmFNZoH4y8OLl3qDnU=
|
||||
github.com/go-pkgz/notify v0.2.0 h1:mxHjcLc3goT+k1qnBPJ06PpNuVUDcu21Xy+6hEo4IaU=
|
||||
github.com/go-pkgz/notify v0.2.0/go.mod h1:uB4bcohkZB+P8zT279KQv5aXqQEkeMXI3Ey0Y3r0SQk=
|
||||
github.com/go-pkgz/lcw/v2 v2.0.0 h1:gTwXpiJBhQeA1rXuqkRuLcV79uATFna8CckH8ZBBrH0=
|
||||
github.com/go-pkgz/lcw/v2 v2.0.0/go.mod h1:yxJHOn+IbQBQHxUqkCtMrbGjIfdYcsBAZcVCBaL1Va8=
|
||||
github.com/go-pkgz/lgr v0.11.1 h1:hXFhZcznehI6imLhEa379oMOKFz7TQUmisAqb3oLOSM=
|
||||
github.com/go-pkgz/lgr v0.11.1/go.mod h1:tgDF4RXQnBfIgJqjgkv0yOeTQ3F1yewWIZkpUhHnAkU=
|
||||
github.com/go-pkgz/notify v1.1.1 h1:xjd5r0a9WUIKutrHkHfuNWdoXZsm84WNAoG35MD8QZA=
|
||||
github.com/go-pkgz/notify v1.1.1/go.mod h1:8ri5Zx+IAjc87HB9kF9+KnVTm0yO5agsphvPbcRo54U=
|
||||
github.com/go-pkgz/repeater v1.1.3 h1:q6+JQF14ESSy28Dd7F+wRelY4F+41HJ0LEy/szNnMiE=
|
||||
github.com/go-pkgz/repeater v1.1.3/go.mod h1:hVTavuO5x3Gxnu8zW7d6sQBfAneKV8X2FjU48kGfpKw=
|
||||
github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
|
||||
github.com/go-pkgz/rest v1.17.0 h1:LoBI/lDBMuqwWhOOkc6thM9NnwJO+K9nWvCOjZ7BAgE=
|
||||
github.com/go-pkgz/rest v1.17.0/go.mod h1:HHlLOt02NJc2sgffXBF6hYVMcRo4Gz3vjg43zTzN7VM=
|
||||
github.com/go-pkgz/rest v1.19.0 h1:FNMi5QX5dDIkuC+/e0r+CWsTuOTwUiWMRSA16Ou+9+A=
|
||||
github.com/go-pkgz/rest v1.19.0/go.mod h1:Po+W6zQzpMPP6XDGLdAN2aW7UKk1IyrLSb48Lp1N3oQ=
|
||||
github.com/go-pkgz/syncs v1.3.2 h1:gmioASlJNy3gNosPlgvWOM2QP0Hdjzn2u+/sUShgd8E=
|
||||
github.com/go-pkgz/syncs v1.3.2/go.mod h1:qjgzpp7OpuhDf7BWsW/FHCu9DLjE32NPy6/vXAXT/Cw=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
@@ -101,90 +104,78 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
|
||||
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
|
||||
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kyokomi/emoji/v2 v2.2.12 h1:sSVA5nH9ebR3Zji1o31wu3yOwD1zKXQA2z0zUyeit60=
|
||||
github.com/kyokomi/emoji/v2 v2.2.12/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc=
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d h1:l3+2LWCbVxn5itfvXAfH9n4YL9jh8l1g5zcncbIc1cs=
|
||||
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d/go.mod h1:TbpErkob6SY7cyozRVSGoB3OlO2qOAgVN8O3KAJ4fMI=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
@@ -194,8 +185,8 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ=
|
||||
github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/slack-go/slack v0.12.5 h1:ddZ6uz6XVaB+3MTDhoW04gG+Vc/M/X1ctC+wssy2cqs=
|
||||
github.com/slack-go/slack v0.12.5/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -203,32 +194,38 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
|
||||
github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo=
|
||||
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
||||
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||
github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI=
|
||||
github.com/tidwall/buntdb v1.3.1 h1:HKoDF01/aBhl9RjYtbaLnvX9/OuenwvQiC3OP1CcL4o=
|
||||
github.com/tidwall/buntdb v1.3.1/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
|
||||
github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
|
||||
github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo=
|
||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
|
||||
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
|
||||
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
|
||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
|
||||
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
|
||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
|
||||
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
|
||||
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
|
||||
@@ -248,85 +245,66 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw=
|
||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
|
||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
|
||||
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
|
||||
go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
|
||||
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
|
||||
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -335,8 +313,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -349,44 +327,32 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -103,6 +103,14 @@ GET {{host}}/api/v1/admin/blocked?site={{site}}
|
||||
### delete comment by id
|
||||
DELETE {{host}}/api/v1/admin/comment/3665976683?site={{site}}&url={{url}}
|
||||
|
||||
### export site (for backup)
|
||||
GET {{host}}/api/v1/admin/export?site={{site}}&mode=stream
|
||||
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
|
||||
|
||||
### export site (for backup) to .gz file
|
||||
GET {{host}}/api/v1/admin/export?site={{site}}&mode=file
|
||||
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
|
||||
|
||||
### get post info
|
||||
GET {{host}}/api/v1/info?site={{site}}&url={{url}}
|
||||
|
||||
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+26
@@ -1,5 +1,31 @@
|
||||
# Changes
|
||||
|
||||
## [0.5.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.4.0...compute/metadata/v0.5.0) (2024-07-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compute/metadata:** Add sys check for windows OnGCE ([#10521](https://github.com/googleapis/google-cloud-go/issues/10521)) ([3b9a830](https://github.com/googleapis/google-cloud-go/commit/3b9a83063960d2a2ac20beb47cc15818a68bd302))
|
||||
|
||||
## [0.4.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.3.0...compute/metadata/v0.4.0) (2024-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compute/metadata:** Add context for all functions/methods ([#10370](https://github.com/googleapis/google-cloud-go/issues/10370)) ([66b8efe](https://github.com/googleapis/google-cloud-go/commit/66b8efe7ad877e052b2987bb4475477e38c67bb3))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **compute/metadata:** Update OnGCE description ([#10408](https://github.com/googleapis/google-cloud-go/issues/10408)) ([6a46dca](https://github.com/googleapis/google-cloud-go/commit/6a46dca4eae4f88ec6f88822e01e5bf8aeca787f))
|
||||
|
||||
## [0.3.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.3...compute/metadata/v0.3.0) (2024-04-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compute/metadata:** Add context aware functions ([#9733](https://github.com/googleapis/google-cloud-go/issues/9733)) ([e4eb5b4](https://github.com/googleapis/google-cloud-go/commit/e4eb5b46ee2aec9d2fc18300bfd66015e25a0510))
|
||||
|
||||
## [0.2.3](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.2...compute/metadata/v0.2.3) (2022-12-15)
|
||||
|
||||
|
||||
|
||||
+376
-83
@@ -23,12 +23,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -88,16 +87,16 @@ func (suffix NotDefinedError) Error() string {
|
||||
return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix))
|
||||
}
|
||||
|
||||
func (c *cachedValue) get(cl *Client) (v string, err error) {
|
||||
func (c *cachedValue) get(ctx context.Context, cl *Client) (v string, err error) {
|
||||
defer c.mu.Unlock()
|
||||
c.mu.Lock()
|
||||
if c.v != "" {
|
||||
return c.v, nil
|
||||
}
|
||||
if c.trim {
|
||||
v, err = cl.getTrimmed(c.k)
|
||||
v, err = cl.getTrimmed(ctx, c.k)
|
||||
} else {
|
||||
v, err = cl.Get(c.k)
|
||||
v, err = cl.GetWithContext(ctx, c.k)
|
||||
}
|
||||
if err == nil {
|
||||
c.v = v
|
||||
@@ -110,7 +109,9 @@ var (
|
||||
onGCE bool
|
||||
)
|
||||
|
||||
// OnGCE reports whether this process is running on Google Compute Engine.
|
||||
// OnGCE reports whether this process is running on Google Compute Platforms.
|
||||
// NOTE: True returned from `OnGCE` does not guarantee that the metadata server
|
||||
// is accessible from this process and have all the metadata defined.
|
||||
func OnGCE() bool {
|
||||
onGCEOnce.Do(initOnGCE)
|
||||
return onGCE
|
||||
@@ -188,78 +189,213 @@ func testOnGCE() bool {
|
||||
return <-resc
|
||||
}
|
||||
|
||||
// systemInfoSuggestsGCE reports whether the local system (without
|
||||
// doing network requests) suggests that we're running on GCE. If this
|
||||
// returns true, testOnGCE tries a bit harder to reach its metadata
|
||||
// server.
|
||||
func systemInfoSuggestsGCE() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
// We don't have any non-Linux clues available, at least yet.
|
||||
return false
|
||||
}
|
||||
slurp, _ := ioutil.ReadFile("/sys/class/dmi/id/product_name")
|
||||
name := strings.TrimSpace(string(slurp))
|
||||
return name == "Google" || name == "Google Compute Engine"
|
||||
}
|
||||
|
||||
// Subscribe calls Client.Subscribe on the default client.
|
||||
// Subscribe calls Client.SubscribeWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [SubscribeWithContext].
|
||||
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||
return defaultClient.Subscribe(suffix, fn)
|
||||
return defaultClient.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
|
||||
}
|
||||
|
||||
// Get calls Client.Get on the default client.
|
||||
func Get(suffix string) (string, error) { return defaultClient.Get(suffix) }
|
||||
// SubscribeWithContext calls Client.SubscribeWithContext on the default client.
|
||||
func SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
|
||||
return defaultClient.SubscribeWithContext(ctx, suffix, fn)
|
||||
}
|
||||
|
||||
// Get calls Client.GetWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [GetWithContext].
|
||||
func Get(suffix string) (string, error) {
|
||||
return defaultClient.GetWithContext(context.Background(), suffix)
|
||||
}
|
||||
|
||||
// GetWithContext calls Client.GetWithContext on the default client.
|
||||
func GetWithContext(ctx context.Context, suffix string) (string, error) {
|
||||
return defaultClient.GetWithContext(ctx, suffix)
|
||||
}
|
||||
|
||||
// ProjectID returns the current instance's project ID string.
|
||||
func ProjectID() (string, error) { return defaultClient.ProjectID() }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ProjectIDWithContext].
|
||||
func ProjectID() (string, error) {
|
||||
return defaultClient.ProjectIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ProjectIDWithContext returns the current instance's project ID string.
|
||||
func ProjectIDWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.ProjectIDWithContext(ctx)
|
||||
}
|
||||
|
||||
// NumericProjectID returns the current instance's numeric project ID.
|
||||
func NumericProjectID() (string, error) { return defaultClient.NumericProjectID() }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [NumericProjectIDWithContext].
|
||||
func NumericProjectID() (string, error) {
|
||||
return defaultClient.NumericProjectIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// NumericProjectIDWithContext returns the current instance's numeric project ID.
|
||||
func NumericProjectIDWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.NumericProjectIDWithContext(ctx)
|
||||
}
|
||||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
func InternalIP() (string, error) { return defaultClient.InternalIP() }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InternalIPWithContext].
|
||||
func InternalIP() (string, error) {
|
||||
return defaultClient.InternalIPWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InternalIPWithContext returns the instance's primary internal IP address.
|
||||
func InternalIPWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.InternalIPWithContext(ctx)
|
||||
}
|
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
func ExternalIP() (string, error) { return defaultClient.ExternalIP() }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ExternalIPWithContext].
|
||||
func ExternalIP() (string, error) {
|
||||
return defaultClient.ExternalIPWithContext(context.Background())
|
||||
}
|
||||
|
||||
// Email calls Client.Email on the default client.
|
||||
func Email(serviceAccount string) (string, error) { return defaultClient.Email(serviceAccount) }
|
||||
// ExternalIPWithContext returns the instance's primary external (public) IP address.
|
||||
func ExternalIPWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.ExternalIPWithContext(ctx)
|
||||
}
|
||||
|
||||
// Email calls Client.EmailWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [EmailWithContext].
|
||||
func Email(serviceAccount string) (string, error) {
|
||||
return defaultClient.EmailWithContext(context.Background(), serviceAccount)
|
||||
}
|
||||
|
||||
// EmailWithContext calls Client.EmailWithContext on the default client.
|
||||
func EmailWithContext(ctx context.Context, serviceAccount string) (string, error) {
|
||||
return defaultClient.EmailWithContext(ctx, serviceAccount)
|
||||
}
|
||||
|
||||
// Hostname returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func Hostname() (string, error) { return defaultClient.Hostname() }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [HostnameWithContext].
|
||||
func Hostname() (string, error) {
|
||||
return defaultClient.HostnameWithContext(context.Background())
|
||||
}
|
||||
|
||||
// HostnameWithContext returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func HostnameWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.HostnameWithContext(ctx)
|
||||
}
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func InstanceTags() ([]string, error) { return defaultClient.InstanceTags() }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceTagsWithContext].
|
||||
func InstanceTags() ([]string, error) {
|
||||
return defaultClient.InstanceTagsWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceTagsWithContext returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func InstanceTagsWithContext(ctx context.Context) ([]string, error) {
|
||||
return defaultClient.InstanceTagsWithContext(ctx)
|
||||
}
|
||||
|
||||
// InstanceID returns the current VM's numeric instance ID.
|
||||
func InstanceID() (string, error) { return defaultClient.InstanceID() }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceIDWithContext].
|
||||
func InstanceID() (string, error) {
|
||||
return defaultClient.InstanceIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceIDWithContext returns the current VM's numeric instance ID.
|
||||
func InstanceIDWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.InstanceIDWithContext(ctx)
|
||||
}
|
||||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
func InstanceName() (string, error) { return defaultClient.InstanceName() }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceNameWithContext].
|
||||
func InstanceName() (string, error) {
|
||||
return defaultClient.InstanceNameWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceNameWithContext returns the current VM's instance ID string.
|
||||
func InstanceNameWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.InstanceNameWithContext(ctx)
|
||||
}
|
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
func Zone() (string, error) { return defaultClient.Zone() }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ZoneWithContext].
|
||||
func Zone() (string, error) {
|
||||
return defaultClient.ZoneWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceAttributes calls Client.InstanceAttributes on the default client.
|
||||
func InstanceAttributes() ([]string, error) { return defaultClient.InstanceAttributes() }
|
||||
// ZoneWithContext returns the current VM's zone, such as "us-central1-b".
|
||||
func ZoneWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.ZoneWithContext(ctx)
|
||||
}
|
||||
|
||||
// ProjectAttributes calls Client.ProjectAttributes on the default client.
|
||||
func ProjectAttributes() ([]string, error) { return defaultClient.ProjectAttributes() }
|
||||
// InstanceAttributes calls Client.InstanceAttributesWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceAttributesWithContext.
|
||||
func InstanceAttributes() ([]string, error) {
|
||||
return defaultClient.InstanceAttributesWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceAttributeValue calls Client.InstanceAttributeValue on the default client.
|
||||
// InstanceAttributesWithContext calls Client.ProjectAttributesWithContext on the default client.
|
||||
func InstanceAttributesWithContext(ctx context.Context) ([]string, error) {
|
||||
return defaultClient.InstanceAttributesWithContext(ctx)
|
||||
}
|
||||
|
||||
// ProjectAttributes calls Client.ProjectAttributesWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ProjectAttributesWithContext].
|
||||
func ProjectAttributes() ([]string, error) {
|
||||
return defaultClient.ProjectAttributesWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ProjectAttributesWithContext calls Client.ProjectAttributesWithContext on the default client.
|
||||
func ProjectAttributesWithContext(ctx context.Context) ([]string, error) {
|
||||
return defaultClient.ProjectAttributesWithContext(ctx)
|
||||
}
|
||||
|
||||
// InstanceAttributeValue calls Client.InstanceAttributeValueWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceAttributeValueWithContext].
|
||||
func InstanceAttributeValue(attr string) (string, error) {
|
||||
return defaultClient.InstanceAttributeValue(attr)
|
||||
return defaultClient.InstanceAttributeValueWithContext(context.Background(), attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue calls Client.ProjectAttributeValue on the default client.
|
||||
// InstanceAttributeValueWithContext calls Client.InstanceAttributeValueWithContext on the default client.
|
||||
func InstanceAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
|
||||
return defaultClient.InstanceAttributeValueWithContext(ctx, attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue calls Client.ProjectAttributeValueWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ProjectAttributeValueWithContext].
|
||||
func ProjectAttributeValue(attr string) (string, error) {
|
||||
return defaultClient.ProjectAttributeValue(attr)
|
||||
return defaultClient.ProjectAttributeValueWithContext(context.Background(), attr)
|
||||
}
|
||||
|
||||
// Scopes calls Client.Scopes on the default client.
|
||||
func Scopes(serviceAccount string) ([]string, error) { return defaultClient.Scopes(serviceAccount) }
|
||||
// ProjectAttributeValueWithContext calls Client.ProjectAttributeValueWithContext on the default client.
|
||||
func ProjectAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
|
||||
return defaultClient.ProjectAttributeValueWithContext(ctx, attr)
|
||||
}
|
||||
|
||||
// Scopes calls Client.ScopesWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ScopesWithContext].
|
||||
func Scopes(serviceAccount string) ([]string, error) {
|
||||
return defaultClient.ScopesWithContext(context.Background(), serviceAccount)
|
||||
}
|
||||
|
||||
// ScopesWithContext calls Client.ScopesWithContext on the default client.
|
||||
func ScopesWithContext(ctx context.Context, serviceAccount string) ([]string, error) {
|
||||
return defaultClient.ScopesWithContext(ctx, serviceAccount)
|
||||
}
|
||||
|
||||
func strsContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
@@ -282,14 +418,12 @@ func NewClient(c *http.Client) *Client {
|
||||
if c == nil {
|
||||
return defaultClient
|
||||
}
|
||||
|
||||
return &Client{hc: c}
|
||||
}
|
||||
|
||||
// getETag returns a value from the metadata service as well as the associated ETag.
|
||||
// This func is otherwise equivalent to Get.
|
||||
func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
||||
ctx := context.TODO()
|
||||
func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string, err error) {
|
||||
// Using a fixed IP makes it very difficult to spoof the metadata service in
|
||||
// a container, which is an important use-case for local testing of cloud
|
||||
// deployments. To enable spoofing of the metadata service, the environment
|
||||
@@ -306,7 +440,7 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
||||
}
|
||||
suffix = strings.TrimLeft(suffix, "/")
|
||||
u := "http://" + host + "/computeMetadata/v1/" + suffix
|
||||
req, err := http.NewRequest("GET", u, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -336,7 +470,7 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
||||
if res.StatusCode == http.StatusNotFound {
|
||||
return "", "", NotDefinedError(suffix)
|
||||
}
|
||||
all, err := ioutil.ReadAll(res.Body)
|
||||
all, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -354,19 +488,37 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
||||
//
|
||||
// If the requested metadata is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.GetWithContext].
|
||||
func (c *Client) Get(suffix string) (string, error) {
|
||||
val, _, err := c.getETag(suffix)
|
||||
return c.GetWithContext(context.Background(), suffix)
|
||||
}
|
||||
|
||||
// GetWithContext returns a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
//
|
||||
// If the GCE_METADATA_HOST environment variable is not defined, a default of
|
||||
// 169.254.169.254 will be used instead.
|
||||
//
|
||||
// If the requested metadata is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// NOTE: Without an extra deadline in the context this call can take in the
|
||||
// worst case, with internal backoff retries, up to 15 seconds (e.g. when server
|
||||
// is responding slowly). Pass context with additional timeouts when needed.
|
||||
func (c *Client) GetWithContext(ctx context.Context, suffix string) (string, error) {
|
||||
val, _, err := c.getETag(ctx, suffix)
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (c *Client) getTrimmed(suffix string) (s string, err error) {
|
||||
s, err = c.Get(suffix)
|
||||
func (c *Client) getTrimmed(ctx context.Context, suffix string) (s string, err error) {
|
||||
s, err = c.GetWithContext(ctx, suffix)
|
||||
s = strings.TrimSpace(s)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) lines(suffix string) ([]string, error) {
|
||||
j, err := c.Get(suffix)
|
||||
func (c *Client) lines(ctx context.Context, suffix string) ([]string, error) {
|
||||
j, err := c.GetWithContext(ctx, suffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -378,45 +530,104 @@ func (c *Client) lines(suffix string) ([]string, error) {
|
||||
}
|
||||
|
||||
// ProjectID returns the current instance's project ID string.
|
||||
func (c *Client) ProjectID() (string, error) { return projID.get(c) }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ProjectIDWithContext].
|
||||
func (c *Client) ProjectID() (string, error) { return c.ProjectIDWithContext(context.Background()) }
|
||||
|
||||
// ProjectIDWithContext returns the current instance's project ID string.
|
||||
func (c *Client) ProjectIDWithContext(ctx context.Context) (string, error) { return projID.get(ctx, c) }
|
||||
|
||||
// NumericProjectID returns the current instance's numeric project ID.
|
||||
func (c *Client) NumericProjectID() (string, error) { return projNum.get(c) }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.NumericProjectIDWithContext].
|
||||
func (c *Client) NumericProjectID() (string, error) {
|
||||
return c.NumericProjectIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// NumericProjectIDWithContext returns the current instance's numeric project ID.
|
||||
func (c *Client) NumericProjectIDWithContext(ctx context.Context) (string, error) {
|
||||
return projNum.get(ctx, c)
|
||||
}
|
||||
|
||||
// InstanceID returns the current VM's numeric instance ID.
|
||||
func (c *Client) InstanceID() (string, error) { return instID.get(c) }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceIDWithContext].
|
||||
func (c *Client) InstanceID() (string, error) {
|
||||
return c.InstanceIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceIDWithContext returns the current VM's numeric instance ID.
|
||||
func (c *Client) InstanceIDWithContext(ctx context.Context) (string, error) {
|
||||
return instID.get(ctx, c)
|
||||
}
|
||||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InternalIPWithContext].
|
||||
func (c *Client) InternalIP() (string, error) {
|
||||
return c.getTrimmed("instance/network-interfaces/0/ip")
|
||||
return c.InternalIPWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InternalIPWithContext returns the instance's primary internal IP address.
|
||||
func (c *Client) InternalIPWithContext(ctx context.Context) (string, error) {
|
||||
return c.getTrimmed(ctx, "instance/network-interfaces/0/ip")
|
||||
}
|
||||
|
||||
// Email returns the email address associated with the service account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.EmailWithContext].
|
||||
func (c *Client) Email(serviceAccount string) (string, error) {
|
||||
return c.EmailWithContext(context.Background(), serviceAccount)
|
||||
}
|
||||
|
||||
// EmailWithContext returns the email address associated with the service account.
|
||||
// The serviceAccount parameter default value (empty string or "default" value)
|
||||
// will use the instance's main account.
|
||||
func (c *Client) EmailWithContext(ctx context.Context, serviceAccount string) (string, error) {
|
||||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
return c.getTrimmed("instance/service-accounts/" + serviceAccount + "/email")
|
||||
return c.getTrimmed(ctx, "instance/service-accounts/"+serviceAccount+"/email")
|
||||
}
|
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ExternalIPWithContext].
|
||||
func (c *Client) ExternalIP() (string, error) {
|
||||
return c.getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip")
|
||||
return c.ExternalIPWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ExternalIPWithContext returns the instance's primary external (public) IP address.
|
||||
func (c *Client) ExternalIPWithContext(ctx context.Context) (string, error) {
|
||||
return c.getTrimmed(ctx, "instance/network-interfaces/0/access-configs/0/external-ip")
|
||||
}
|
||||
|
||||
// Hostname returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.HostnameWithContext].
|
||||
func (c *Client) Hostname() (string, error) {
|
||||
return c.getTrimmed("instance/hostname")
|
||||
return c.HostnameWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
// HostnameWithContext returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func (c *Client) HostnameWithContext(ctx context.Context) (string, error) {
|
||||
return c.getTrimmed(ctx, "instance/hostname")
|
||||
}
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceTagsWithContext].
|
||||
func (c *Client) InstanceTags() ([]string, error) {
|
||||
return c.InstanceTagsWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceTagsWithContext returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func (c *Client) InstanceTagsWithContext(ctx context.Context) ([]string, error) {
|
||||
var s []string
|
||||
j, err := c.Get("instance/tags")
|
||||
j, err := c.GetWithContext(ctx, "instance/tags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -427,13 +638,27 @@ func (c *Client) InstanceTags() ([]string, error) {
|
||||
}
|
||||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceNameWithContext].
|
||||
func (c *Client) InstanceName() (string, error) {
|
||||
return c.getTrimmed("instance/name")
|
||||
return c.InstanceNameWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceNameWithContext returns the current VM's instance ID string.
|
||||
func (c *Client) InstanceNameWithContext(ctx context.Context) (string, error) {
|
||||
return c.getTrimmed(ctx, "instance/name")
|
||||
}
|
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ZoneWithContext].
|
||||
func (c *Client) Zone() (string, error) {
|
||||
zone, err := c.getTrimmed("instance/zone")
|
||||
return c.ZoneWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ZoneWithContext returns the current VM's zone, such as "us-central1-b".
|
||||
func (c *Client) ZoneWithContext(ctx context.Context) (string, error) {
|
||||
zone, err := c.getTrimmed(ctx, "instance/zone")
|
||||
// zone is of the form "projects/<projNum>/zones/<zoneName>".
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -444,12 +669,34 @@ func (c *Client) Zone() (string, error) {
|
||||
// InstanceAttributes returns the list of user-defined attributes,
|
||||
// assigned when initially creating a GCE VM instance. The value of an
|
||||
// attribute can be obtained with InstanceAttributeValue.
|
||||
func (c *Client) InstanceAttributes() ([]string, error) { return c.lines("instance/attributes/") }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceAttributesWithContext].
|
||||
func (c *Client) InstanceAttributes() ([]string, error) {
|
||||
return c.InstanceAttributesWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceAttributesWithContext returns the list of user-defined attributes,
|
||||
// assigned when initially creating a GCE VM instance. The value of an
|
||||
// attribute can be obtained with InstanceAttributeValue.
|
||||
func (c *Client) InstanceAttributesWithContext(ctx context.Context) ([]string, error) {
|
||||
return c.lines(ctx, "instance/attributes/")
|
||||
}
|
||||
|
||||
// ProjectAttributes returns the list of user-defined attributes
|
||||
// applying to the project as a whole, not just this VM. The value of
|
||||
// an attribute can be obtained with ProjectAttributeValue.
|
||||
func (c *Client) ProjectAttributes() ([]string, error) { return c.lines("project/attributes/") }
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ProjectAttributesWithContext].
|
||||
func (c *Client) ProjectAttributes() ([]string, error) {
|
||||
return c.ProjectAttributesWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ProjectAttributesWithContext returns the list of user-defined attributes
|
||||
// applying to the project as a whole, not just this VM. The value of
|
||||
// an attribute can be obtained with ProjectAttributeValue.
|
||||
func (c *Client) ProjectAttributesWithContext(ctx context.Context) ([]string, error) {
|
||||
return c.lines(ctx, "project/attributes/")
|
||||
}
|
||||
|
||||
// InstanceAttributeValue returns the value of the provided VM
|
||||
// instance attribute.
|
||||
@@ -459,8 +706,22 @@ func (c *Client) ProjectAttributes() ([]string, error) { return c.lines("project
|
||||
//
|
||||
// InstanceAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceAttributeValueWithContext].
|
||||
func (c *Client) InstanceAttributeValue(attr string) (string, error) {
|
||||
return c.Get("instance/attributes/" + attr)
|
||||
return c.InstanceAttributeValueWithContext(context.Background(), attr)
|
||||
}
|
||||
|
||||
// InstanceAttributeValueWithContext returns the value of the provided VM
|
||||
// instance attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// InstanceAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) InstanceAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
|
||||
return c.GetWithContext(ctx, "instance/attributes/"+attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue returns the value of the provided
|
||||
@@ -471,39 +732,71 @@ func (c *Client) InstanceAttributeValue(attr string) (string, error) {
|
||||
//
|
||||
// ProjectAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ProjectAttributeValueWithContext].
|
||||
func (c *Client) ProjectAttributeValue(attr string) (string, error) {
|
||||
return c.Get("project/attributes/" + attr)
|
||||
return c.ProjectAttributeValueWithContext(context.Background(), attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValueWithContext returns the value of the provided
|
||||
// project attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// ProjectAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) ProjectAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
|
||||
return c.GetWithContext(ctx, "project/attributes/"+attr)
|
||||
}
|
||||
|
||||
// Scopes returns the service account scopes for the given account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ScopesWithContext].
|
||||
func (c *Client) Scopes(serviceAccount string) ([]string, error) {
|
||||
return c.ScopesWithContext(context.Background(), serviceAccount)
|
||||
}
|
||||
|
||||
// ScopesWithContext returns the service account scopes for the given account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
func (c *Client) ScopesWithContext(ctx context.Context, serviceAccount string) ([]string, error) {
|
||||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
return c.lines("instance/service-accounts/" + serviceAccount + "/scopes")
|
||||
return c.lines(ctx, "instance/service-accounts/"+serviceAccount+"/scopes")
|
||||
}
|
||||
|
||||
// Subscribe subscribes to a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
// The suffix may contain query parameters.
|
||||
//
|
||||
// Subscribe calls fn with the latest metadata value indicated by the provided
|
||||
// suffix. If the metadata value is deleted, fn is called with the empty string
|
||||
// and ok false. Subscribe blocks until fn returns a non-nil error or the value
|
||||
// is deleted. Subscribe returns the error value returned from the last call to
|
||||
// fn, which may be nil when ok == false.
|
||||
// Deprecated: Please use the context aware variant [Client.SubscribeWithContext].
|
||||
func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||
return c.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
|
||||
}
|
||||
|
||||
// SubscribeWithContext subscribes to a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
// The suffix may contain query parameters.
|
||||
//
|
||||
// SubscribeWithContext calls fn with the latest metadata value indicated by the
|
||||
// provided suffix. If the metadata value is deleted, fn is called with the
|
||||
// empty string and ok false. Subscribe blocks until fn returns a non-nil error
|
||||
// or the value is deleted. Subscribe returns the error value returned from the
|
||||
// last call to fn, which may be nil when ok == false.
|
||||
func (c *Client) SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
|
||||
const failedSubscribeSleep = time.Second * 5
|
||||
|
||||
// First check to see if the metadata value exists at all.
|
||||
val, lastETag, err := c.getETag(suffix)
|
||||
val, lastETag, err := c.getETag(ctx, suffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(val, true); err != nil {
|
||||
if err := fn(ctx, val, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -514,7 +807,7 @@ func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) erro
|
||||
suffix += "?wait_for_change=true&last_etag="
|
||||
}
|
||||
for {
|
||||
val, etag, err := c.getETag(suffix + url.QueryEscape(lastETag))
|
||||
val, etag, err := c.getETag(ctx, suffix+url.QueryEscape(lastETag))
|
||||
if err != nil {
|
||||
if _, deleted := err.(NotDefinedError); !deleted {
|
||||
time.Sleep(failedSubscribeSleep)
|
||||
@@ -524,7 +817,7 @@ func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) erro
|
||||
}
|
||||
lastETag = etag
|
||||
|
||||
if err := fn(val, ok); err != nil || !ok {
|
||||
if err := fn(ctx, val, ok); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
syscallRetryable = func(err error) bool { return false }
|
||||
syscallRetryable = func(error) bool { return false }
|
||||
)
|
||||
|
||||
// defaultBackoff is basically equivalent to gax.Backoff without the need for
|
||||
|
||||
Generated
Vendored
+10
-7
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Google LLC
|
||||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,12 +12,15 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file, and the {{.RootMod}} import, won't actually become part of
|
||||
// the resultant binary.
|
||||
//go:build modhack
|
||||
// +build modhack
|
||||
//go:build !windows && !linux
|
||||
|
||||
package metadata
|
||||
|
||||
// Necessary for safely adding multi-module repo. See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
|
||||
import _ "cloud.google.com/go/compute/internal"
|
||||
// systemInfoSuggestsGCE reports whether the local system (without
|
||||
// doing network requests) suggests that we're running on GCE. If this
|
||||
// returns true, testOnGCE tries a bit harder to reach its metadata
|
||||
// server.
|
||||
func systemInfoSuggestsGCE() bool {
|
||||
// We don't currently have checks for other GOOS
|
||||
return false
|
||||
}
|
||||
Generated
Vendored
+14
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Google LLC
|
||||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,7 +12,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
//go:build linux
|
||||
|
||||
// Version is the current tagged release of the library.
|
||||
const Version = "1.23.0"
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func systemInfoSuggestsGCE() bool {
|
||||
b, _ := os.ReadFile("/sys/class/dmi/id/product_name")
|
||||
name := strings.TrimSpace(string(b))
|
||||
return name == "Google" || name == "Google Compute Engine"
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
func systemInfoSuggestsGCE() bool {
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\HardwareConfig\Current`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
s, _, err := k.GetStringValue("SystemProductName")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
return strings.HasPrefix(s, "Google")
|
||||
}
|
||||
+7
-3
@@ -22,7 +22,7 @@ Syntax-wise, it is as close as possible to jQuery, with the same function names
|
||||
|
||||
## Installation
|
||||
|
||||
Please note that because of the net/html dependency, goquery requires Go1.1+ and is tested on Go1.7+.
|
||||
Please note that starting with version `v1.9.0` of goquery, Go 1.18+ is required due to the use of generics. For previous goquery versions, a Go version of 1.1+ was required because of the `net/html` dependency. Ongoing goquery development is tested on the latest 2 versions of Go.
|
||||
|
||||
$ go get github.com/PuerkitoBio/goquery
|
||||
|
||||
@@ -40,6 +40,9 @@ Please note that because of the net/html dependency, goquery requires Go1.1+ and
|
||||
|
||||
**Note that goquery's API is now stable, and will not break.**
|
||||
|
||||
* **2024-04-29 (v1.9.2)** : Update `go.mod` dependencies.
|
||||
* **2024-02-29 (v1.9.1)** : Improve allocation and performance of the `Map` function and `Selection.Map` method, better document the cascadia differences (thanks [@jwilsson](https://github.com/jwilsson)).
|
||||
* **2024-02-22 (v1.9.0)** : Add a generic `Map` function, **goquery now requires Go version 1.18+** (thanks [@Fesaa](https://github.com/Fesaa)).
|
||||
* **2023-02-18 (v1.8.1)** : Update `go.mod` dependencies, update CI workflow.
|
||||
* **2021-10-25 (v1.8.0)** : Add `Render` function to render a `Selection` to an `io.Writer` (thanks [@anthonygedeon](https://github.com/anthonygedeon)).
|
||||
* **2021-07-11 (v1.7.1)** : Update go.mod dependencies and add dependabot config (thanks [@jauderho](https://github.com/jauderho)).
|
||||
@@ -88,7 +91,7 @@ Utility functions that are not in jQuery but are useful in Go are implemented as
|
||||
|
||||
The complete [package reference documentation can be found here][doc].
|
||||
|
||||
Please note that Cascadia's selectors do not necessarily match all supported selectors of jQuery (Sizzle). See the [cascadia project][cascadia] for details. Invalid selector strings compile to a `Matcher` that fails to match any node. Behaviour of the various functions that take a selector string as argument follows from that fact, e.g. (where `~` is an invalid selector string):
|
||||
Please note that Cascadia's selectors do not necessarily match all supported selectors of jQuery (Sizzle). See the [cascadia project][cascadia] for details. Also, the selectors work more like the DOM's `querySelectorAll`, than jQuery's matchers - they have no concept of contextual matching (for some concrete examples of what that means, see [this ticket](https://github.com/andybalholm/cascadia/issues/61)). In practice, it doesn't matter very often but it's something worth mentioning. Invalid selector strings compile to a `Matcher` that fails to match any node. Behaviour of the various functions that take a selector string as argument follows from that fact, e.g. (where `~` is an invalid selector string):
|
||||
|
||||
* `Find("~")` returns an empty selection because the selector string doesn't match anything.
|
||||
* `Add("~")` returns a new selection that holds the same nodes as the original selection, because it didn't add any node (selector string didn't match anything).
|
||||
@@ -157,6 +160,7 @@ func main() {
|
||||
- [stitcherd](https://github.com/vhodges/stitcherd), A server for doing server side includes using css selectors and DOM updates.
|
||||
- [goskyr](https://github.com/jakopako/goskyr), an easily configurable command-line scraper written in Go.
|
||||
- [goGetJS](https://github.com/davemolk/goGetJS), a tool for extracting, searching, and saving JavaScript files (with optional headless browser).
|
||||
- [fitter](https://github.com/PxyUp/fitter), a tool for selecting values from JSON, XML, HTML and XPath formatted pages.
|
||||
|
||||
## Support
|
||||
|
||||
@@ -165,7 +169,7 @@ There are a number of ways you can support the project:
|
||||
* Use it, star it, build something with it, spread the word!
|
||||
- If you do build something open-source or otherwise publicly-visible, let me know so I can add it to the [Related Projects](#related-projects) section!
|
||||
* Raise issues to improve the project (note: doc typos and clarifications are issues too!)
|
||||
- Please search existing issues before opening a new one - it may have already been adressed.
|
||||
- Please search existing issues before opening a new one - it may have already been addressed.
|
||||
* Pull requests: please discuss new code in an issue first, unless the fix is really trivial.
|
||||
- Make sure new code is tested.
|
||||
- Be mindful of existing code - PRs that break existing code have a high probability of being declined, unless it fixes a serious issue.
|
||||
|
||||
+9
-1
@@ -31,8 +31,16 @@ func (s *Selection) EachWithBreak(f func(int, *Selection) bool) *Selection {
|
||||
// element in that selection starting at 0, and a *Selection that contains
|
||||
// only that element.
|
||||
func (s *Selection) Map(f func(int, *Selection) string) (result []string) {
|
||||
return Map(s, f)
|
||||
}
|
||||
|
||||
// Map is the generic version of Selection.Map, allowing any type to be
|
||||
// returned.
|
||||
func Map[E any](s *Selection, f func(int, *Selection) E) (result []E) {
|
||||
result = make([]E, len(s.Nodes))
|
||||
|
||||
for i, n := range s.Nodes {
|
||||
result = append(result, f(i, newSingleSelection(n, s.document)))
|
||||
result[i] = f(i, newSingleSelection(n, s.document))
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
+6
@@ -20,6 +20,12 @@ const (
|
||||
// Find gets the descendants of each element in the current set of matched
|
||||
// elements, filtered by a selector. It returns a new Selection object
|
||||
// containing these matched elements.
|
||||
//
|
||||
// Note that as for all methods accepting a selector string, the selector is
|
||||
// compiled and applied by the cascadia package and inherits its behavior and
|
||||
// constraints regarding supported selectors. See the note on cascadia in
|
||||
// the goquery documentation here:
|
||||
// https://github.com/PuerkitoBio/goquery?tab=readme-ov-file#api
|
||||
func (s *Selection) Find(selector string) *Selection {
|
||||
return pushStack(s, findWithMatcher(s.Nodes, compileMatcher(selector)))
|
||||
}
|
||||
|
||||
+4
@@ -11,3 +11,7 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = false
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# Binaries for programs and plugins
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
.hermit
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
@@ -18,3 +22,4 @@ _models/
|
||||
|
||||
_examples/
|
||||
*.min.*
|
||||
build/
|
||||
|
||||
+2
@@ -49,6 +49,8 @@ linters:
|
||||
- nosnakecase
|
||||
- testableexamples
|
||||
- musttag
|
||||
- depguard
|
||||
- goconst
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
VERSION = %(git describe --tags --dirty --always)%
|
||||
export CGOENABLED = 0
|
||||
|
||||
tokentype_enumer.go: types.go
|
||||
build: go generate
|
||||
|
||||
# Regenerate the list of lexers in the README
|
||||
README.md: lexers/*.go lexers/*/*.xml table.py
|
||||
build: ./table.py
|
||||
-clean
|
||||
|
||||
implicit %{1}%{2}.min.%{3}: **/*.{css,js}
|
||||
build: esbuild --bundle %{IN} --minify --outfile=%{OUT}
|
||||
|
||||
implicit build/%{1}: cmd/*
|
||||
cd cmd/%{1}
|
||||
inputs: cmd/%{1}/**/* **/*.go
|
||||
build: go build -ldflags="-X 'main.version=%{VERSION}'" -o ../../build/%{1} .
|
||||
|
||||
#upload: chromad
|
||||
# build:
|
||||
# scp chromad root@swapoff.org:
|
||||
# ssh root@swapoff.org 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'
|
||||
# touch upload
|
||||
+4
-4
@@ -13,11 +13,11 @@ tokentype_string.go: types.go
|
||||
go generate
|
||||
|
||||
chromad:
|
||||
rm -f chromad
|
||||
rm -rf build
|
||||
esbuild --bundle cmd/chromad/static/index.js --minify --outfile=cmd/chromad/static/index.min.js
|
||||
esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css
|
||||
(export CGOENABLED=0 ; cd ./cmd/chromad && go build -ldflags="-X 'main.version=$(VERSION)'" -o ../../chromad .)
|
||||
(export CGOENABLED=0 ; cd ./cmd/chromad && go build -ldflags="-X 'main.version=$(VERSION)'" -o ../../build/chromad .)
|
||||
|
||||
upload: chromad
|
||||
scp chromad root@swapoff.org: && \
|
||||
upload: build/chromad
|
||||
scp build/chromad root@swapoff.org: && \
|
||||
ssh root@swapoff.org 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'
|
||||
|
||||
+68
-76
@@ -1,7 +1,6 @@
|
||||
# Chroma — A general purpose syntax highlighter in pure Go
|
||||
[](https://godoc.org/github.com/alecthomas/chroma) [](https://github.com/alecthomas/chroma/actions/workflows/ci.yml) [](https://invite.slack.golangbridge.org/)
|
||||
|
||||
> **NOTE:** As Chroma has just been released, its API is still in flux. That said, the high-level interface should not change significantly.
|
||||
[](https://godoc.org/github.com/alecthomas/chroma) [](https://github.com/alecthomas/chroma/actions/workflows/ci.yml) [](https://invite.slack.golangbridge.org/)
|
||||
|
||||
Chroma takes source code and other structured text and converts it into syntax
|
||||
highlighted HTML, ANSI-coloured text, etc.
|
||||
@@ -9,72 +8,72 @@ highlighted HTML, ANSI-coloured text, etc.
|
||||
Chroma is based heavily on [Pygments](http://pygments.org/), and includes
|
||||
translators for Pygments lexers and styles.
|
||||
|
||||
<a id="markdown-table-of-contents" name="table-of-contents"></a>
|
||||
## Table of Contents
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
1. [Table of Contents](#table-of-contents)
|
||||
2. [Supported languages](#supported-languages)
|
||||
3. [Try it](#try-it)
|
||||
4. [Using the library](#using-the-library)
|
||||
1. [Quick start](#quick-start)
|
||||
2. [Identifying the language](#identifying-the-language)
|
||||
3. [Formatting the output](#formatting-the-output)
|
||||
4. [The HTML formatter](#the-html-formatter)
|
||||
5. [More detail](#more-detail)
|
||||
1. [Lexers](#lexers)
|
||||
2. [Formatters](#formatters)
|
||||
3. [Styles](#styles)
|
||||
6. [Command-line interface](#command-line-interface)
|
||||
7. [Testing lexers](#testing-lexers)
|
||||
8. [What's missing compared to Pygments?](#whats-missing-compared-to-pygments)
|
||||
1. [Supported languages](#supported-languages)
|
||||
2. [Try it](#try-it)
|
||||
3. [Using the library](#using-the-library)
|
||||
1. [Quick start](#quick-start)
|
||||
2. [Identifying the language](#identifying-the-language)
|
||||
3. [Formatting the output](#formatting-the-output)
|
||||
4. [The HTML formatter](#the-html-formatter)
|
||||
4. [More detail](#more-detail)
|
||||
1. [Lexers](#lexers)
|
||||
2. [Formatters](#formatters)
|
||||
3. [Styles](#styles)
|
||||
5. [Command-line interface](#command-line-interface)
|
||||
6. [Testing lexers](#testing-lexers)
|
||||
7. [What's missing compared to Pygments?](#whats-missing-compared-to-pygments)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
<a id="markdown-supported-languages" name="supported-languages"></a>
|
||||
## Supported languages
|
||||
|
||||
Prefix | Language
|
||||
:----: | --------
|
||||
A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, Awk
|
||||
B | Ballerina, Bash, Batchfile, BibTeX, Bicep, BlitzBasic, BNF, Brainfuck, BQN
|
||||
C | C, C#, C++, Caddyfile, Caddyfile Directives, Cap'n Proto, Cassandra CQL, Ceylon, CFEngine3, cfstatement, ChaiScript, Chapel, Cheetah, Clojure, CMake, COBOL, CoffeeScript, Common Lisp, Coq, Crystal, CSS, Cython
|
||||
D | D, Dart, Diff, Django/Jinja, Docker, DTD, Dylan
|
||||
E | EBNF, Elixir, Elm, EmacsLisp, Erlang
|
||||
F | Factor, Fish, Forth, Fortran, FSharp
|
||||
G | GAS, GDScript, Genshi, Genshi HTML, Genshi Text, Gherkin, GLSL, Gnuplot, Go, Go HTML Template, Go Text Template, GraphQL, Groff, Groovy
|
||||
H | Handlebars, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HTML, HTTP, Hy
|
||||
I | Idris, Igor, INI, Io
|
||||
J | J, Java, JavaScript, JSON, Julia, Jungle
|
||||
K | Kotlin
|
||||
L | Lighttpd configuration file, LLVM, Lua
|
||||
M | Makefile, Mako, markdown, Mason, Mathematica, Matlab, MiniZinc, MLIR, Modula-2, MonkeyC, MorrowindScript, Myghty, MySQL
|
||||
N | NASM, Newspeak, Nginx configuration file, Nim, Nix
|
||||
O | Objective-C, OCaml, Octave, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode
|
||||
P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerShell, Prolog, PromQL, Properties, Protocol Buffer, PSL, Puppet, Python 2, Python
|
||||
Q | QBasic
|
||||
R | R, Racket, Ragel, Raku, react, ReasonML, reg, reStructuredText, Rexx, Ruby, Rust
|
||||
S | SAS, Sass, Scala, Scheme, Scilab, SCSS, Sed, Smali, Smalltalk, Smarty, Snobol, Solidity, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog
|
||||
T | TableGen, TASM, Tcl, Tcsh, Termcap, Terminfo, Terraform, TeX, Thrift, TOML, TradingView, Transact-SQL, Turing, Turtle, Twig, TypeScript, TypoScript, TypoScriptCssData, TypoScriptHtmlData
|
||||
V | VB.net, verilog, VHDL, VHS, VimL, vue
|
||||
W | WDTE
|
||||
X | XML, Xorg
|
||||
Y | YAML, YANG
|
||||
Z | Zig
|
||||
|
||||
| Prefix | Language |
|
||||
| :----: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Agda, AL, Alloy, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, ArmAsm, AutoHotkey, AutoIt, Awk |
|
||||
| B | Ballerina, Bash, Bash Session, Batchfile, BibTeX, Bicep, BlitzBasic, BNF, BQN, Brainfuck |
|
||||
| C | C, C#, C++, Caddyfile, Caddyfile Directives, Cap'n Proto, Cassandra CQL, Ceylon, CFEngine3, cfstatement, ChaiScript, Chapel, Cheetah, Clojure, CMake, COBOL, CoffeeScript, Common Lisp, Coq, Crystal, CSS, Cython |
|
||||
| D | D, Dart, Dax, Desktop Entry, Diff, Django/Jinja, dns, Docker, DTD, Dylan |
|
||||
| E | EBNF, Elixir, Elm, EmacsLisp, Erlang |
|
||||
| F | Factor, Fennel, Fish, Forth, Fortran, FortranFixed, FSharp |
|
||||
| G | GAS, GDScript, Genshi, Genshi HTML, Genshi Text, Gherkin, GLSL, Gnuplot, Go, Go HTML Template, Go Text Template, GraphQL, Groff, Groovy |
|
||||
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hy |
|
||||
| I | Idris, Igor, INI, Io, ISCdhcpd |
|
||||
| J | J, Java, JavaScript, JSON, Julia, Jungle |
|
||||
| K | Kotlin |
|
||||
| L | Lighttpd configuration file, LLVM, Lua |
|
||||
| M | Makefile, Mako, markdown, Mason, Materialize SQL dialect, Mathematica, Matlab, mcfunction, Meson, Metal, MiniZinc, MLIR, Modula-2, MonkeyC, MorrowindScript, Myghty, MySQL |
|
||||
| N | NASM, Natural, Newspeak, Nginx configuration file, Nim, Nix |
|
||||
| O | Objective-C, OCaml, Octave, Odin, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode |
|
||||
| P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Plutus Core, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerQuery, PowerShell, Prolog, PromQL, Promela, properties, Protocol Buffer, PRQL, PSL, Puppet, Python, Python 2 |
|
||||
| Q | QBasic, QML |
|
||||
| R | R, Racket, Ragel, Raku, react, ReasonML, reg, Rego, reStructuredText, Rexx, RPMSpec, Ruby, Rust |
|
||||
| S | SAS, Sass, Scala, Scheme, Scilab, SCSS, Sed, Sieve, Smali, Smalltalk, Smarty, Snobol, Solidity, SourcePawn, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog |
|
||||
| T | TableGen, Tal, TASM, Tcl, Tcsh, Termcap, Terminfo, Terraform, TeX, Thrift, TOML, TradingView, Transact-SQL, Turing, Turtle, Twig, TypeScript, TypoScript, TypoScriptCssData, TypoScriptHtmlData |
|
||||
| V | V, V shell, Vala, VB.net, verilog, VHDL, VHS, VimL, vue |
|
||||
| W | WDTE, WebGPU Shading Language, Whiley |
|
||||
| X | XML, Xorg |
|
||||
| Y | YAML, YANG |
|
||||
| Z | Z80 Assembly, Zed, Zig |
|
||||
|
||||
_I will attempt to keep this section up to date, but an authoritative list can be
|
||||
displayed with `chroma --list`._
|
||||
|
||||
<a id="markdown-try-it" name="try-it"></a>
|
||||
## Try it
|
||||
|
||||
Try out various languages and styles on the [Chroma Playground](https://swapoff.org/chroma/playground/).
|
||||
|
||||
<a id="markdown-using-the-library" name="using-the-library"></a>
|
||||
## Using the library
|
||||
|
||||
This is version 2 of Chroma, use the import path:
|
||||
|
||||
```go
|
||||
import "github.com/alecthomas/chroma/v2"
|
||||
```
|
||||
|
||||
Chroma, like Pygments, has the concepts of
|
||||
[lexers](https://github.com/alecthomas/chroma/tree/master/lexers),
|
||||
[formatters](https://github.com/alecthomas/chroma/tree/master/formatters) and
|
||||
@@ -93,7 +92,6 @@ In all cases, if a lexer, formatter or style can not be determined, `nil` will
|
||||
be returned. In this situation you may want to default to the `Fallback`
|
||||
value in each respective package, which provides sane defaults.
|
||||
|
||||
<a id="markdown-quick-start" name="quick-start"></a>
|
||||
### Quick start
|
||||
|
||||
A convenience function exists that can be used to simply format some source
|
||||
@@ -103,7 +101,6 @@ text, without any effort:
|
||||
err := quick.Highlight(os.Stdout, someSourceCode, "go", "html", "monokai")
|
||||
```
|
||||
|
||||
<a id="markdown-identifying-the-language" name="identifying-the-language"></a>
|
||||
### Identifying the language
|
||||
|
||||
To highlight code, you'll first have to identify what language the code is
|
||||
@@ -111,21 +108,21 @@ written in. There are three primary ways to do that:
|
||||
|
||||
1. Detect the language from its filename.
|
||||
|
||||
```go
|
||||
lexer := lexers.Match("foo.go")
|
||||
```
|
||||
```go
|
||||
lexer := lexers.Match("foo.go")
|
||||
```
|
||||
|
||||
3. Explicitly specify the language by its Chroma syntax ID (a full list is available from `lexers.Names()`).
|
||||
2. Explicitly specify the language by its Chroma syntax ID (a full list is available from `lexers.Names()`).
|
||||
|
||||
```go
|
||||
lexer := lexers.Get("go")
|
||||
```
|
||||
```go
|
||||
lexer := lexers.Get("go")
|
||||
```
|
||||
|
||||
3. Detect the language from its content.
|
||||
|
||||
```go
|
||||
lexer := lexers.Analyse("package main\n\nfunc main()\n{\n}\n")
|
||||
```
|
||||
```go
|
||||
lexer := lexers.Analyse("package main\n\nfunc main()\n{\n}\n")
|
||||
```
|
||||
|
||||
In all cases, `nil` will be returned if the language can not be identified.
|
||||
|
||||
@@ -143,7 +140,6 @@ token types into a single token:
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
```
|
||||
|
||||
<a id="markdown-formatting-the-output" name="formatting-the-output"></a>
|
||||
### Formatting the output
|
||||
|
||||
Once a language is identified you will need to pick a formatter and a style (theme).
|
||||
@@ -172,7 +168,6 @@ And finally, format the tokens from the iterator:
|
||||
err := formatter.Format(w, style, iterator)
|
||||
```
|
||||
|
||||
<a id="markdown-the-html-formatter" name="the-html-formatter"></a>
|
||||
### The HTML formatter
|
||||
|
||||
By default the `html` registered formatter generates standalone HTML with
|
||||
@@ -197,10 +192,8 @@ formatter := html.New(html.WithClasses(true))
|
||||
err := formatter.WriteCSS(w, style)
|
||||
```
|
||||
|
||||
<a id="markdown-more-detail" name="more-detail"></a>
|
||||
## More detail
|
||||
|
||||
<a id="markdown-lexers" name="lexers"></a>
|
||||
### Lexers
|
||||
|
||||
See the [Pygments documentation](http://pygments.org/docs/lexerdevelopment/)
|
||||
@@ -220,7 +213,6 @@ python3 _tools/pygments2chroma_xml.py \
|
||||
See notes in [pygments-lexers.txt](https://github.com/alecthomas/chroma/blob/master/pygments-lexers.txt)
|
||||
for a list of lexers, and notes on some of the issues importing them.
|
||||
|
||||
<a id="markdown-formatters" name="formatters"></a>
|
||||
### Formatters
|
||||
|
||||
Chroma supports HTML output, as well as terminal output in 8 colour, 256 colour, and true-colour.
|
||||
@@ -228,7 +220,6 @@ Chroma supports HTML output, as well as terminal output in 8 colour, 256 colour,
|
||||
A `noop` formatter is included that outputs the token text only, and a `tokens`
|
||||
formatter outputs raw tokens. The latter is useful for debugging lexers.
|
||||
|
||||
<a id="markdown-styles" name="styles"></a>
|
||||
### Styles
|
||||
|
||||
Chroma styles are defined in XML. The style entries use the
|
||||
@@ -237,7 +228,7 @@ Chroma styles are defined in XML. The style entries use the
|
||||
All Pygments styles have been converted to Chroma using the `_tools/style.py`
|
||||
script.
|
||||
|
||||
When you work with one of [Chroma's styles](https://github.com/alecthomas/chroma/tree/master/styles),
|
||||
When you work with one of [Chroma's styles](https://github.com/alecthomas/chroma/tree/master/styles),
|
||||
know that the `Background` token type provides the default style for tokens. It does so
|
||||
by defining a foreground color and background color.
|
||||
|
||||
@@ -252,7 +243,6 @@ Also, token types in a style file are hierarchical. For instance, when `CommentS
|
||||
|
||||
For a quick overview of the available styles and how they look, check out the [Chroma Style Gallery](https://xyproto.github.io/splash/docs/).
|
||||
|
||||
<a id="markdown-command-line-interface" name="command-line-interface"></a>
|
||||
## Command-line interface
|
||||
|
||||
A command-line interface to Chroma is included.
|
||||
@@ -277,29 +267,31 @@ on under the hood for easy integration with [lesspipe shipping with
|
||||
Debian and derivatives](https://manpages.debian.org/lesspipe#USER_DEFINED_FILTERS);
|
||||
for that setup the `chroma` executable can be just symlinked to `~/.lessfilter`.
|
||||
|
||||
<a id="markdown-whats-missing-compared-to-pygments" name="whats-missing-compared-to-pygments"></a>
|
||||
|
||||
<a id="markdown-testing-lexers" name="testing-lexers"></a>
|
||||
## Testing lexers
|
||||
|
||||
If you edit some lexers and want to try it, open a shell in `cmd/chromad` and run:
|
||||
|
||||
```shell
|
||||
go run .
|
||||
```
|
||||
|
||||
A Link will be printed. Open it in your Browser. Now you can test on the Playground with your local changes.
|
||||
|
||||
If you want to run the tests and the lexers, open a shell in the root directory and run:
|
||||
|
||||
```shell
|
||||
go test ./lexers
|
||||
```
|
||||
|
||||
When updating or adding a lexer, please add tests. See [lexers/README.md](lexers/README.md) for more.
|
||||
|
||||
## What's missing compared to Pygments?
|
||||
|
||||
- Quite a few lexers, for various reasons (pull-requests welcome):
|
||||
- Pygments lexers for complex languages often include custom code to
|
||||
handle certain aspects, such as Raku's ability to nest code inside
|
||||
regular expressions. These require time and effort to convert.
|
||||
- I mostly only converted languages I had heard of, to reduce the porting cost.
|
||||
- Pygments lexers for complex languages often include custom code to
|
||||
handle certain aspects, such as Raku's ability to nest code inside
|
||||
regular expressions. These require time and effort to convert.
|
||||
- I mostly only converted languages I had heard of, to reduce the porting cost.
|
||||
- Some more esoteric features of Pygments are omitted for simplicity.
|
||||
- Though the Chroma API supports content detection, very few languages support them.
|
||||
I have plans to implement a statistical analyser at some point, but not enough time.
|
||||
|
||||
+70
-11
@@ -5,7 +5,9 @@ import (
|
||||
"html"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
)
|
||||
@@ -132,6 +134,7 @@ func New(options ...Option) *Formatter {
|
||||
baseLineNumber: 1,
|
||||
preWrapper: defaultPreWrapper,
|
||||
}
|
||||
f.styleCache = newStyleCache(f)
|
||||
for _, option := range options {
|
||||
option(f)
|
||||
}
|
||||
@@ -188,6 +191,7 @@ var (
|
||||
|
||||
// Formatter that generates HTML.
|
||||
type Formatter struct {
|
||||
styleCache *styleCache
|
||||
standalone bool
|
||||
prefix string
|
||||
Classes bool // Exported field to detect when classes are being used
|
||||
@@ -220,12 +224,7 @@ func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Ite
|
||||
//
|
||||
// OTOH we need to be super careful about correct escaping...
|
||||
func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.Token) (err error) { // nolint: gocyclo
|
||||
css := f.styleToCSS(style)
|
||||
if !f.Classes {
|
||||
for t, style := range css {
|
||||
css[t] = compressStyle(style)
|
||||
}
|
||||
}
|
||||
css := f.styleCache.get(style, true)
|
||||
if f.standalone {
|
||||
fmt.Fprint(w, "<html>\n")
|
||||
if f.Classes {
|
||||
@@ -243,7 +242,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
|
||||
wrapInTable := f.lineNumbers && f.lineNumbersInTable
|
||||
|
||||
lines := chroma.SplitTokensIntoLines(tokens)
|
||||
lineDigits := len(fmt.Sprintf("%d", f.baseLineNumber+len(lines)-1))
|
||||
lineDigits := len(strconv.Itoa(f.baseLineNumber + len(lines) - 1))
|
||||
highlightIndex := 0
|
||||
|
||||
if wrapInTable {
|
||||
@@ -251,7 +250,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
|
||||
fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.PreWrapper))
|
||||
fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
|
||||
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD))
|
||||
fmt.Fprintf(w, f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
|
||||
for index := range lines {
|
||||
line := f.baseLineNumber + index
|
||||
highlight, next := f.shouldHighlight(highlightIndex, line)
|
||||
@@ -273,7 +272,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
|
||||
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD, "width:100%"))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
|
||||
|
||||
highlightIndex = 0
|
||||
for index, tokens := range lines {
|
||||
@@ -323,7 +322,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
|
||||
fmt.Fprint(w, `</span>`) // End of Line
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, f.preWrapper.End(true))
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.End(true))
|
||||
|
||||
if wrapInTable {
|
||||
fmt.Fprint(w, "</td></tr></table>\n")
|
||||
@@ -419,7 +418,7 @@ func (f *Formatter) tabWidthStyle() string {
|
||||
|
||||
// WriteCSS writes CSS style definitions (without any surrounding HTML).
|
||||
func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
|
||||
css := f.styleToCSS(style)
|
||||
css := f.styleCache.get(style, false)
|
||||
// Special-case background as it is mapped to the outer ".chroma" class.
|
||||
if _, err := fmt.Fprintf(w, "/* %s */ .%sbg { %s }\n", chroma.Background, f.prefix, css[chroma.Background]); err != nil {
|
||||
return err
|
||||
@@ -562,3 +561,63 @@ func compressStyle(s string) string {
|
||||
}
|
||||
return strings.Join(out, ";")
|
||||
}
|
||||
|
||||
const styleCacheLimit = 32
|
||||
|
||||
type styleCacheEntry struct {
|
||||
style *chroma.Style
|
||||
compressed bool
|
||||
cache map[chroma.TokenType]string
|
||||
}
|
||||
|
||||
type styleCache struct {
|
||||
mu sync.Mutex
|
||||
// LRU cache of compiled (and possibly compressed) styles. This is a slice
|
||||
// because the cache size is small, and a slice is sufficiently fast for
|
||||
// small N.
|
||||
cache []styleCacheEntry
|
||||
f *Formatter
|
||||
}
|
||||
|
||||
func newStyleCache(f *Formatter) *styleCache {
|
||||
return &styleCache{f: f}
|
||||
}
|
||||
|
||||
func (l *styleCache) get(style *chroma.Style, compress bool) map[chroma.TokenType]string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
// Look for an existing entry.
|
||||
for i := len(l.cache) - 1; i >= 0; i-- {
|
||||
entry := l.cache[i]
|
||||
if entry.style == style && entry.compressed == compress {
|
||||
// Top of the cache, no need to adjust the order.
|
||||
if i == len(l.cache)-1 {
|
||||
return entry.cache
|
||||
}
|
||||
// Move this entry to the end of the LRU
|
||||
copy(l.cache[i:], l.cache[i+1:])
|
||||
l.cache[len(l.cache)-1] = entry
|
||||
return entry.cache
|
||||
}
|
||||
}
|
||||
|
||||
// No entry, create one.
|
||||
cached := l.f.styleToCSS(style)
|
||||
if !l.f.Classes {
|
||||
for t, style := range cached {
|
||||
cached[t] = compressStyle(style)
|
||||
}
|
||||
}
|
||||
if compress {
|
||||
for t, style := range cached {
|
||||
cached[t] = compressStyle(style)
|
||||
}
|
||||
}
|
||||
// Evict the oldest entry.
|
||||
if len(l.cache) >= styleCacheLimit {
|
||||
l.cache = l.cache[0:copy(l.cache, l.cache[1:])]
|
||||
}
|
||||
l.cache = append(l.cache, styleCacheEntry{style: style, cache: cached, compressed: compress})
|
||||
return cached
|
||||
}
|
||||
|
||||
+20
@@ -57,6 +57,26 @@ type Config struct {
|
||||
//
|
||||
// If this is 0 it will be treated as a default of 1.
|
||||
Priority float32 `xml:"priority,omitempty"`
|
||||
|
||||
// Analyse is a list of regexes to match against the input.
|
||||
//
|
||||
// If a match is found, the score is returned if single attribute is set to true,
|
||||
// otherwise the sum of all the score of matching patterns will be
|
||||
// used as the final score.
|
||||
Analyse *AnalyseConfig `xml:"analyse,omitempty"`
|
||||
}
|
||||
|
||||
// AnalyseConfig defines the list of regexes analysers.
|
||||
type AnalyseConfig struct {
|
||||
Regexes []RegexConfig `xml:"regex,omitempty"`
|
||||
// If true, the first matching score is returned.
|
||||
First bool `xml:"first,attr"`
|
||||
}
|
||||
|
||||
// RegexConfig defines a single regex pattern and its score in case of match.
|
||||
type RegexConfig struct {
|
||||
Pattern string `xml:"pattern,attr"`
|
||||
Score float32 `xml:"score,attr"`
|
||||
}
|
||||
|
||||
// Token output to formatter.
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package lexers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// TODO(moorereason): can this be factored away?
|
||||
var bashAnalyserRe = regexp.MustCompile(`(?m)^#!.*/bin/(?:env |)(?:bash|zsh|sh|ksh)`)
|
||||
|
||||
func init() { // nolint: gochecknoinits
|
||||
Get("bash").SetAnalyser(func(text string) float32 {
|
||||
if bashAnalyserRe.FindString(text) != "" {
|
||||
return 1.0
|
||||
}
|
||||
return 0.0
|
||||
})
|
||||
}
|
||||
+120
-60
@@ -4,52 +4,82 @@ import (
|
||||
. "github.com/alecthomas/chroma/v2" // nolint
|
||||
)
|
||||
|
||||
// Matcher token stub for docs, or
|
||||
// Named matcher: @name, or
|
||||
// Path matcher: /foo, or
|
||||
// Wildcard path matcher: *
|
||||
// nolint: gosec
|
||||
var caddyfileMatcherTokenRegexp = `(\[\<matcher\>\]|@[^\s]+|/[^\s]+|\*)`
|
||||
|
||||
// Comment at start of line, or
|
||||
// Comment preceded by whitespace
|
||||
var caddyfileCommentRegexp = `(^|\s+)#.*\n`
|
||||
|
||||
// caddyfileCommon are the rules common to both of the lexer variants
|
||||
func caddyfileCommonRules() Rules {
|
||||
return Rules{
|
||||
"site_block_common": {
|
||||
Include("site_body"),
|
||||
// Any other directive
|
||||
{`[^\s#]+`, Keyword, Push("directive")},
|
||||
Include("base"),
|
||||
},
|
||||
"site_body": {
|
||||
// Import keyword
|
||||
{`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil},
|
||||
{`\b(import|invoke)\b( [^\s#]+)`, ByGroups(Keyword, Text), Push("subdirective")},
|
||||
// Matcher definition
|
||||
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
|
||||
// Matcher token stub for docs
|
||||
{`\[\<matcher\>\]`, NameDecorator, Push("matcher")},
|
||||
// These cannot have matchers but may have things that look like
|
||||
// matchers in their arguments, so we just parse as a subdirective.
|
||||
{`try_files`, Keyword, Push("subdirective")},
|
||||
{`\b(try_files|tls|log|bind)\b`, Keyword, Push("subdirective")},
|
||||
// These are special, they can nest more directives
|
||||
{`handle_errors|handle|route|handle_path|not`, Keyword, Push("nested_directive")},
|
||||
// Any other directive
|
||||
{`[^\s#]+`, Keyword, Push("directive")},
|
||||
Include("base"),
|
||||
{`\b(handle_errors|handle_path|handle_response|replace_status|handle|route)\b`, Keyword, Push("nested_directive")},
|
||||
// uri directive has special syntax
|
||||
{`\b(uri)\b`, Keyword, Push("uri_directive")},
|
||||
},
|
||||
"matcher": {
|
||||
{`\{`, Punctuation, Push("block")},
|
||||
// Not can be one-liner
|
||||
{`not`, Keyword, Push("deep_not_matcher")},
|
||||
// Heredoc for CEL expression
|
||||
Include("heredoc"),
|
||||
// Backtick for CEL expression
|
||||
{"`", StringBacktick, Push("backticks")},
|
||||
// Any other same-line matcher
|
||||
{`[^\s#]+`, Keyword, Push("arguments")},
|
||||
// Terminators
|
||||
{`\n`, Text, Pop(1)},
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
{`\}`, Punctuation, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"block": {
|
||||
{`\}`, Punctuation, Pop(2)},
|
||||
// Using double quotes doesn't stop at spaces
|
||||
{`"`, StringDouble, Push("double_quotes")},
|
||||
// Using backticks doesn't stop at spaces
|
||||
{"`", StringBacktick, Push("backticks")},
|
||||
// Not can be one-liner
|
||||
{`not`, Keyword, Push("not_matcher")},
|
||||
// Any other subdirective
|
||||
// Directives & matcher definitions
|
||||
Include("site_body"),
|
||||
// Any directive
|
||||
{`[^\s#]+`, Keyword, Push("subdirective")},
|
||||
Include("base"),
|
||||
},
|
||||
"nested_block": {
|
||||
{`\}`, Punctuation, Pop(2)},
|
||||
// Matcher definition
|
||||
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
|
||||
// Something that starts with literally < is probably a docs stub
|
||||
{`\<[^#]+\>`, Keyword, Push("nested_directive")},
|
||||
// Any other directive
|
||||
{`[^\s#]+`, Keyword, Push("nested_directive")},
|
||||
// Using double quotes doesn't stop at spaces
|
||||
{`"`, StringDouble, Push("double_quotes")},
|
||||
// Using backticks doesn't stop at spaces
|
||||
{"`", StringBacktick, Push("backticks")},
|
||||
// Not can be one-liner
|
||||
{`not`, Keyword, Push("not_matcher")},
|
||||
// Directives & matcher definitions
|
||||
Include("site_body"),
|
||||
// Any other subdirective
|
||||
{`[^\s#]+`, Keyword, Push("directive")},
|
||||
Include("base"),
|
||||
},
|
||||
"not_matcher": {
|
||||
@@ -66,69 +96,97 @@ func caddyfileCommonRules() Rules {
|
||||
},
|
||||
"directive": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
Include("matcher_token"),
|
||||
Include("comments_pop_1"),
|
||||
{`\n`, Text, Pop(1)},
|
||||
{caddyfileMatcherTokenRegexp, NameDecorator, Push("arguments")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"nested_directive": {
|
||||
{`\{(?=\s)`, Punctuation, Push("nested_block")},
|
||||
Include("matcher_token"),
|
||||
Include("comments_pop_1"),
|
||||
{`\n`, Text, Pop(1)},
|
||||
{caddyfileMatcherTokenRegexp, NameDecorator, Push("nested_arguments")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"subdirective": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
Include("comments_pop_1"),
|
||||
{`\n`, Text, Pop(1)},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"arguments": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
Include("comments_pop_2"),
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(2)},
|
||||
{`\\\n`, Text, nil}, // Skip escaped newlines
|
||||
{`\n`, Text, Pop(2)},
|
||||
{`\s*\n`, Text, Pop(2)},
|
||||
Include("base"),
|
||||
},
|
||||
"nested_arguments": {
|
||||
{`\{(?=\s)`, Punctuation, Push("nested_block")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(2)},
|
||||
{`\\\n`, Text, nil}, // Skip escaped newlines
|
||||
{`\s*\n`, Text, Pop(2)},
|
||||
Include("base"),
|
||||
},
|
||||
"deep_subdirective": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
Include("comments_pop_3"),
|
||||
{`\n`, Text, Pop(3)},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(3)},
|
||||
{`\s*\n`, Text, Pop(3)},
|
||||
Include("base"),
|
||||
},
|
||||
"matcher_token": {
|
||||
{`@[^\s]+`, NameDecorator, Push("arguments")}, // Named matcher
|
||||
{`/[^\s]+`, NameDecorator, Push("arguments")}, // Path matcher
|
||||
{`\*`, NameDecorator, Push("arguments")}, // Wildcard path matcher
|
||||
{`\[\<matcher\>\]`, NameDecorator, Push("arguments")}, // Matcher token stub for docs
|
||||
"uri_directive": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
{caddyfileMatcherTokenRegexp, NameDecorator, nil},
|
||||
{`(strip_prefix|strip_suffix|replace|path_regexp)`, NameConstant, Push("arguments")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"comments": {
|
||||
{`^#.*\n`, CommentSingle, nil}, // Comment at start of line
|
||||
{`\s+#.*\n`, CommentSingle, nil}, // Comment preceded by whitespace
|
||||
"double_quotes": {
|
||||
Include("placeholder"),
|
||||
{`\\"`, StringDouble, nil},
|
||||
{`[^"]`, StringDouble, nil},
|
||||
{`"`, StringDouble, Pop(1)},
|
||||
},
|
||||
"comments_pop_1": {
|
||||
{`^#.*\n`, CommentSingle, Pop(1)}, // Comment at start of line
|
||||
{`\s+#.*\n`, CommentSingle, Pop(1)}, // Comment preceded by whitespace
|
||||
"backticks": {
|
||||
Include("placeholder"),
|
||||
{"\\\\`", StringBacktick, nil},
|
||||
{"[^`]", StringBacktick, nil},
|
||||
{"`", StringBacktick, Pop(1)},
|
||||
},
|
||||
"comments_pop_2": {
|
||||
{`^#.*\n`, CommentSingle, Pop(2)}, // Comment at start of line
|
||||
{`\s+#.*\n`, CommentSingle, Pop(2)}, // Comment preceded by whitespace
|
||||
"optional": {
|
||||
// Docs syntax for showing optional parts with [ ]
|
||||
{`\[`, Punctuation, Push("optional")},
|
||||
Include("name_constants"),
|
||||
{`\|`, Punctuation, nil},
|
||||
{`[^\[\]\|]+`, String, nil},
|
||||
{`\]`, Punctuation, Pop(1)},
|
||||
},
|
||||
"comments_pop_3": {
|
||||
{`^#.*\n`, CommentSingle, Pop(3)}, // Comment at start of line
|
||||
{`\s+#.*\n`, CommentSingle, Pop(3)}, // Comment preceded by whitespace
|
||||
"heredoc": {
|
||||
{`(<<([a-zA-Z0-9_-]+))(\n(.*|\n)*)(\s*)(\2)`, ByGroups(StringHeredoc, nil, String, String, String, StringHeredoc), nil},
|
||||
},
|
||||
"name_constants": {
|
||||
{`\b(most_recently_modified|largest_size|smallest_size|first_exist|internal|disable_redirects|ignore_loaded_certs|disable_certs|private_ranges|first|last|before|after|on|off)\b(\||(?=\]|\s|$))`, ByGroups(NameConstant, Punctuation), nil},
|
||||
},
|
||||
"placeholder": {
|
||||
// Placeholder with dots, colon for default value, brackets for args[0:]
|
||||
{`\{[\w+.\[\]\:\$-]+\}`, StringEscape, nil},
|
||||
// Handle opening brackets with no matching closing one
|
||||
{`\{[^\}\s]*\b`, String, nil},
|
||||
},
|
||||
"base": {
|
||||
Include("comments"),
|
||||
{`(on|off|first|last|before|after|internal|strip_prefix|strip_suffix|replace)\b`, NameConstant, nil},
|
||||
{`(https?://)?([a-z0-9.-]+)(:)([0-9]+)`, ByGroups(Name, Name, Punctuation, LiteralNumberInteger), nil},
|
||||
{`[a-z-]+/[a-z-+]+`, LiteralString, nil},
|
||||
{`[0-9]+[km]?\b`, LiteralNumberInteger, nil},
|
||||
{`\{[\w+.\$-]+\}`, LiteralStringEscape, nil}, // Placeholder
|
||||
{`\[(?=[^#{}$]+\])`, Punctuation, nil},
|
||||
{`\]|\|`, Punctuation, nil},
|
||||
{`[^\s#{}$\]]+`, LiteralString, nil},
|
||||
{caddyfileCommentRegexp, CommentSingle, nil},
|
||||
{`\[\<matcher\>\]`, NameDecorator, nil},
|
||||
Include("name_constants"),
|
||||
Include("heredoc"),
|
||||
{`(https?://)?([a-z0-9.-]+)(:)([0-9]+)([^\s]*)`, ByGroups(Name, Name, Punctuation, NumberInteger, Name), nil},
|
||||
{`\[`, Punctuation, Push("optional")},
|
||||
{"`", StringBacktick, Push("backticks")},
|
||||
{`"`, StringDouble, Push("double_quotes")},
|
||||
Include("placeholder"),
|
||||
{`[a-z-]+/[a-z-+]+`, String, nil},
|
||||
{`[0-9]+([smhdk]|ns|us|µs|ms)?\b`, NumberInteger, nil},
|
||||
{`[^\s\n#\{]+`, String, nil},
|
||||
{`/[^\s#]*`, Name, nil},
|
||||
{`\s+`, Text, nil},
|
||||
},
|
||||
@@ -149,27 +207,29 @@ var Caddyfile = Register(MustNewLexer(
|
||||
func caddyfileRules() Rules {
|
||||
return Rules{
|
||||
"root": {
|
||||
Include("comments"),
|
||||
{caddyfileCommentRegexp, CommentSingle, nil},
|
||||
// Global options block
|
||||
{`^\s*(\{)\s*$`, ByGroups(Punctuation), Push("globals")},
|
||||
// Top level import
|
||||
{`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil},
|
||||
// Snippets
|
||||
{`(\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")},
|
||||
{`(&?\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")},
|
||||
// Site label
|
||||
{`[^#{(\s,]+`, GenericHeading, Push("label")},
|
||||
// Site label with placeholder
|
||||
{`\{[\w+.\$-]+\}`, LiteralStringEscape, Push("label")},
|
||||
{`\{[\w+.\[\]\:\$-]+\}`, StringEscape, Push("label")},
|
||||
{`\s+`, Text, nil},
|
||||
},
|
||||
"globals": {
|
||||
{`\}`, Punctuation, Pop(1)},
|
||||
{`[^\s#]+`, Keyword, Push("directive")},
|
||||
// Global options are parsed as subdirectives (no matcher)
|
||||
{`[^\s#]+`, Keyword, Push("subdirective")},
|
||||
Include("base"),
|
||||
},
|
||||
"snippet": {
|
||||
{`\}`, Punctuation, Pop(1)},
|
||||
// Matcher definition
|
||||
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
|
||||
// Any directive
|
||||
Include("site_body"),
|
||||
// Any other directive
|
||||
{`[^\s#]+`, Keyword, Push("directive")},
|
||||
Include("base"),
|
||||
},
|
||||
@@ -179,7 +239,7 @@ func caddyfileRules() Rules {
|
||||
{`,\s*\n?`, Text, nil},
|
||||
{` `, Text, nil},
|
||||
// Site label with placeholder
|
||||
{`\{[\w+.\$-]+\}`, LiteralStringEscape, nil},
|
||||
Include("placeholder"),
|
||||
// Site label
|
||||
{`[^#{(\s,]+`, GenericHeading, nil},
|
||||
// Comment after non-block label (hack because comments end in \n)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user