mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-28 00:16:48 +09:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ebc47a8f0 | ||
|
|
962c48af13 | ||
|
|
0920ceab6b | ||
|
|
12bb943af8 | ||
|
|
e4f88b425a | ||
|
|
bfa9f66633 | ||
|
|
8d3c2bbd2b | ||
|
|
03fa883f98 | ||
|
|
8e0aa7d636 | ||
|
|
efaae642b0 | ||
|
|
28525bec5d | ||
|
|
52ccd24efa | ||
|
|
c189ddea41 | ||
|
|
7ffb043cf3 | ||
|
|
17f81d4fa0 | ||
|
|
905165b7e2 | ||
|
|
ce62fd0814 | ||
|
|
47cdbb2a0f | ||
|
|
6ec04cbc28 | ||
|
|
e74d192aec | ||
|
|
67ca61da1f | ||
|
|
0febc9f415 | ||
|
|
4a75954080 | ||
|
|
69c94b6594 | ||
|
|
ba11a0a67f | ||
|
|
8c7f49e003 | ||
|
|
3fada3d997 | ||
|
|
dde94b06ed | ||
|
|
1852adb06c | ||
|
|
e983e3f79f | ||
|
|
53e36a16ea | ||
|
|
a05292f548 | ||
|
|
71ffeeea78 | ||
|
|
77bcad65b5 | ||
|
|
238f73c26f | ||
|
|
1feb2d94b9 | ||
|
|
49c0ce496c | ||
|
|
88f3f942f7 | ||
|
|
6f256f5f2d | ||
|
|
089a8b35c0 | ||
|
|
b52c318518 | ||
|
|
de5493d8a9 | ||
|
|
74346342df | ||
|
|
e4a49d52b2 | ||
|
|
b8919161a8 | ||
|
|
80585a9585 | ||
|
|
b3d97ecb0e | ||
|
|
94692026d3 | ||
|
|
0672673916 | ||
|
|
5667262d1c | ||
|
|
1babf3f9ed | ||
|
|
4d04f9beee | ||
|
|
bebe518c74 |
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
<% if (session_token = `op signin --account basecamp --raw`.strip) != "" %># Generated by mrsk envify
|
<% if (session_token = `op signin --account 23QPQDKZC5BKBIIG7UGT5GR5RM --raw`.strip) != "" %># Generated by mrsk envify
|
||||||
MRSK_REGISTRY_PASSWORD=<%= `op read "op://Deploy/Docker Hub MRSK/password" -n --session #{session_token}` %>
|
MRSK_REGISTRY_PASSWORD=<%= `op read "op://Deploy/Docker Hub MRSK/password" -n --session #{session_token}` %>
|
||||||
RAILS_MASTER_KEY=<%= `op read "op://Development/Chat-in-a-box/RAILS_MASTER_KEY" -n --session #{session_token}` %>
|
RAILS_MASTER_KEY=<%= `op read "op://Development/Chat-in-a-box/RAILS_MASTER_KEY" -n --session #{session_token}` %>
|
||||||
<% else raise ArgumentError, "Session token missing" end %>
|
<% else raise ArgumentError, "Session token missing" end %>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
cooldown:
|
||||||
|
default-days: 7
|
||||||
|
|
||||||
|
- package-ecosystem: bundler
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
cooldown:
|
||||||
|
semver-major-days: 7
|
||||||
|
semver-minor-days: 3
|
||||||
|
semver-patch-days: 2
|
||||||
|
default-days: 7
|
||||||
|
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
cooldown:
|
||||||
|
semver-major-days: 7
|
||||||
|
semver-minor-days: 3
|
||||||
|
semver-patch-days: 2
|
||||||
|
default-days: 7
|
||||||
+47
-10
@@ -6,16 +6,22 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
scan:
|
scan:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||||
with:
|
with:
|
||||||
ruby-version: .ruby-version
|
ruby-version: .ruby-version
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
@@ -25,12 +31,16 @@ jobs:
|
|||||||
|
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||||
with:
|
with:
|
||||||
ruby-version: .ruby-version
|
ruby-version: .ruby-version
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
@@ -38,11 +48,32 @@ jobs:
|
|||||||
- name: Lint code for consistent style
|
- name: Lint code for consistent style
|
||||||
run: bin/rubocop
|
run: bin/rubocop
|
||||||
|
|
||||||
|
lint-actions:
|
||||||
|
name: GitHub Actions audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run actionlint
|
||||||
|
uses: rhysd/actionlint@393031adb9afb225ee52ae2ccd7a5af5525e03e8 # v1.7.11
|
||||||
|
|
||||||
|
- name: Run zizmor
|
||||||
|
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||||
|
with:
|
||||||
|
advanced-security: false
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis # zizmor: ignore[unpinned-images] -- version tag is fine for service containers
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
@@ -51,10 +82,12 @@ jobs:
|
|||||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
|
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||||
env:
|
env:
|
||||||
REDIS_URL: redis://localhost:6379/0
|
REDIS_URL: redis://localhost:6379/0
|
||||||
with:
|
with:
|
||||||
@@ -66,9 +99,11 @@ jobs:
|
|||||||
|
|
||||||
test_system:
|
test_system:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis # zizmor: ignore[unpinned-images] -- version tag is fine for service containers
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
@@ -77,10 +112,12 @@ jobs:
|
|||||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
|
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||||
env:
|
env:
|
||||||
REDIS_URL: redis://localhost:6379/0
|
REDIS_URL: redis://localhost:6379/0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ concurrency:
|
|||||||
group: publish-${{ github.workflow }}-${{ github.ref }}
|
group: publish-${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_DESCRIPTION: Campfire is a web-based chat application with multiple rooms, direct messages, file attachments with previews, search, web push notifications, @mentions, and bot integrations. Single-tenant; production-ready image with web app, background jobs, caching, file serving, and SSL.
|
IMAGE_DESCRIPTION: Campfire is a web-based chat application with multiple rooms, direct messages, file attachments with previews, search, web push notifications, @mentions, and bot integrations. Single-tenant; production-ready image with web app, background jobs, caching, file serving, and SSL.
|
||||||
@@ -27,6 +23,11 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build and push image (${{ matrix.arch }})
|
name: Build and push image (${{ matrix.arch }})
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -43,13 +44,15 @@ jobs:
|
|||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -66,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract Docker metadata (tags, labels) with arch suffix
|
- name: Extract Docker metadata (tags, labels) with arch suffix
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
with:
|
with:
|
||||||
images: ${{ steps.vars.outputs.canonical }}
|
images: ${{ steps.vars.outputs.canonical }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -84,7 +87,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push (${{ matrix.platform }})
|
- name: Build and push (${{ matrix.platform }})
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6.18.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
@@ -102,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Attest image provenance (per-arch)
|
- name: Attest image provenance (per-arch)
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: actions/attest-build-provenance@v3.0.0
|
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ steps.vars.outputs.canonical }}
|
subject-name: ${{ steps.vars.outputs.canonical }}
|
||||||
subject-digest: ${{ steps.build.outputs.digest }}
|
subject-digest: ${{ steps.build.outputs.digest }}
|
||||||
@@ -113,16 +116,20 @@ jobs:
|
|||||||
needs: build
|
needs: build
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Docker Buildx (for imagetools)
|
- name: Set up Docker Buildx (for imagetools)
|
||||||
uses: docker/setup-buildx-action@v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -139,7 +146,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Compute base tags (no suffix)
|
- name: Compute base tags (no suffix)
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
with:
|
with:
|
||||||
images: ${{ steps.vars.outputs.canonical }}
|
images: ${{ steps.vars.outputs.canonical }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -157,9 +164,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Create multi-arch manifests
|
- name: Create multi-arch manifests
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
tags="${{ steps.meta.outputs.tags }}"
|
tags="$TAGS"
|
||||||
echo "Creating manifests for tags:"
|
echo "Creating manifests for tags:"
|
||||||
printf '%s\n' "$tags"
|
printf '%s\n' "$tags"
|
||||||
while IFS= read -r tag; do
|
while IFS= read -r tag; do
|
||||||
@@ -185,13 +194,15 @@ jobs:
|
|||||||
done <<< "$tags"
|
done <<< "$tags"
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.9.2
|
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||||
|
|
||||||
- name: Cosign sign all tags (keyless OIDC)
|
- name: Cosign sign all tags (keyless OIDC)
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
tags="${{ steps.meta.outputs.tags }}"
|
tags="$TAGS"
|
||||||
printf '%s\n' "$tags"
|
printf '%s\n' "$tags"
|
||||||
while IFS= read -r tag; do
|
while IFS= read -r tag; do
|
||||||
[ -z "$tag" ] && continue
|
[ -z "$tag" ] && continue
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ FROM base AS build
|
|||||||
|
|
||||||
# Install packages need to build gems
|
# Install packages need to build gems
|
||||||
RUN apt-get update -qq && \
|
RUN apt-get update -qq && \
|
||||||
apt-get install -y build-essential git pkg-config libyaml-dev && \
|
apt-get install -y build-essential git pkg-config libyaml-dev libssl-dev && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
# Install application gems
|
# Install application gems
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ gem "sqlite3"
|
|||||||
gem "redis", "~> 5.4"
|
gem "redis", "~> 5.4"
|
||||||
|
|
||||||
# Deployment
|
# Deployment
|
||||||
gem "puma", "~> 6.6"
|
gem "puma", "~> 7.2", ">= 7.2.1"
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
gem "resque", "~> 2.7.0"
|
gem "resque", "~> 2.7.0"
|
||||||
@@ -48,6 +48,7 @@ gem "thruster"
|
|||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem "debug"
|
gem "debug"
|
||||||
gem "rubocop-rails-omakase", require: false
|
gem "rubocop-rails-omakase", require: false
|
||||||
|
gem "bundler-audit", require: false
|
||||||
gem "faker", require: false
|
gem "faker", require: false
|
||||||
gem "brakeman", require: false
|
gem "brakeman", require: false
|
||||||
end
|
end
|
||||||
|
|||||||
+36
-32
@@ -128,18 +128,21 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
action_text-trix (2.1.15)
|
action_text-trix (2.1.19)
|
||||||
railties
|
railties
|
||||||
addressable (2.8.7)
|
addressable (2.9.0)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.22)
|
||||||
benchmark (0.5.0)
|
benchmark (0.5.0)
|
||||||
bigdecimal (3.3.1)
|
bigdecimal (3.3.1)
|
||||||
brakeman (7.1.1)
|
brakeman (8.0.5)
|
||||||
racc
|
racc
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
|
bundler-audit (0.9.3)
|
||||||
|
bundler (>= 1.2.0)
|
||||||
|
thor (~> 1.0)
|
||||||
capybara (3.40.0)
|
capybara (3.40.0)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
@@ -150,18 +153,18 @@ GEM
|
|||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.7)
|
||||||
connection_pool (2.5.5)
|
connection_pool (2.5.5)
|
||||||
crack (1.0.0)
|
crack (1.0.0)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.7)
|
||||||
date (3.5.0)
|
date (3.5.1)
|
||||||
debug (1.11.0)
|
debug (1.11.0)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
erb (6.0.0)
|
erb (6.0.4)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
faker (3.5.2)
|
faker (3.5.2)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
@@ -188,8 +191,8 @@ GEM
|
|||||||
jbuilder (2.14.1)
|
jbuilder (2.14.1)
|
||||||
actionview (>= 7.0.0)
|
actionview (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
json (2.13.2)
|
json (2.20.0)
|
||||||
jwt (3.1.2)
|
jwt (3.2.0)
|
||||||
base64
|
base64
|
||||||
kredis (1.8.0)
|
kredis (1.8.0)
|
||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
@@ -221,7 +224,7 @@ GEM
|
|||||||
ruby2_keywords (~> 0.0.1)
|
ruby2_keywords (~> 0.0.1)
|
||||||
net-http-persistent (4.0.6)
|
net-http-persistent (4.0.6)
|
||||||
connection_pool (~> 2.2, >= 2.2.4)
|
connection_pool (~> 2.2, >= 2.2.4)
|
||||||
net-imap (0.5.12)
|
net-imap (0.6.4.1)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -231,13 +234,13 @@ GEM
|
|||||||
net-smtp (0.5.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.5)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.18.10-aarch64-linux-gnu)
|
nokogiri (1.19.4-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-arm64-darwin)
|
nokogiri (1.19.4-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-x86_64-darwin)
|
nokogiri (1.19.4-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-x86_64-linux-gnu)
|
nokogiri (1.19.4-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
openssl (3.3.0)
|
openssl (3.3.0)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.3)
|
||||||
@@ -256,15 +259,15 @@ GEM
|
|||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.2)
|
public_suffix (6.0.2)
|
||||||
puma (6.6.1)
|
puma (7.2.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.4)
|
rack (3.2.6)
|
||||||
rack-protection (4.1.1)
|
rack-protection (4.2.1)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
logger (>= 1.6.0)
|
logger (>= 1.6.0)
|
||||||
rack (>= 3.0.0, < 4)
|
rack (>= 3.0.0, < 4)
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.2)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.2.0)
|
rack-test (2.2.0)
|
||||||
@@ -305,12 +308,12 @@ GEM
|
|||||||
resque-pool (0.7.1)
|
resque-pool (0.7.1)
|
||||||
rake (>= 10.0, < 14.0)
|
rake (>= 10.0, < 14.0)
|
||||||
resque (>= 1.22, < 3)
|
resque (>= 1.22, < 3)
|
||||||
rexml (3.4.1)
|
rexml (3.4.4)
|
||||||
rqrcode (3.1.0)
|
rqrcode (3.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 2.0)
|
rqrcode_core (~> 2.0)
|
||||||
rqrcode_core (2.0.0)
|
rqrcode_core (2.1.0)
|
||||||
rubocop (1.80.0)
|
rubocop (1.80.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -357,11 +360,11 @@ GEM
|
|||||||
sentry-ruby (5.26.0)
|
sentry-ruby (5.26.0)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sinatra (4.1.1)
|
sinatra (4.2.1)
|
||||||
logger (>= 1.6.0)
|
logger (>= 1.6.0)
|
||||||
mustermann (~> 3.0)
|
mustermann (~> 3.0)
|
||||||
rack (>= 3.0.0, < 4)
|
rack (>= 3.0.0, < 4)
|
||||||
rack-protection (= 4.1.1)
|
rack-protection (= 4.2.1)
|
||||||
rack-session (>= 2.0.0, < 3)
|
rack-session (>= 2.0.0, < 3)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
sqlite3 (2.7.3-aarch64-linux-gnu)
|
sqlite3 (2.7.3-aarch64-linux-gnu)
|
||||||
@@ -377,13 +380,13 @@ GEM
|
|||||||
thruster (0.1.15-x86_64-darwin)
|
thruster (0.1.15-x86_64-darwin)
|
||||||
thruster (0.1.15-x86_64-linux)
|
thruster (0.1.15-x86_64-linux)
|
||||||
tilt (2.6.1)
|
tilt (2.6.1)
|
||||||
timeout (0.4.4)
|
timeout (0.6.1)
|
||||||
tsort (0.2.0)
|
tsort (0.2.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unicode-display_width (3.1.5)
|
unicode-display_width (3.2.0)
|
||||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.0.4)
|
unicode-emoji (4.2.0)
|
||||||
uri (1.1.1)
|
uri (1.1.1)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
web-push (3.0.2)
|
web-push (3.0.2)
|
||||||
@@ -412,6 +415,7 @@ DEPENDENCIES
|
|||||||
bcrypt
|
bcrypt
|
||||||
benchmark
|
benchmark
|
||||||
brakeman
|
brakeman
|
||||||
|
bundler-audit
|
||||||
capybara
|
capybara
|
||||||
debug
|
debug
|
||||||
faker
|
faker
|
||||||
@@ -425,7 +429,7 @@ DEPENDENCIES
|
|||||||
ostruct
|
ostruct
|
||||||
platform_agent
|
platform_agent
|
||||||
propshaft!
|
propshaft!
|
||||||
puma (~> 6.6)
|
puma (~> 7.2, >= 7.2.1)
|
||||||
rails!
|
rails!
|
||||||
rails_autolink
|
rails_autolink
|
||||||
redis (~> 5.4)
|
redis (~> 5.4)
|
||||||
@@ -444,4 +448,4 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.9
|
4.0.13
|
||||||
@@ -127,6 +127,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:has(input[type=checkbox][disabled]) {
|
||||||
|
cursor: not-allowed;
|
||||||
|
filter: grayscale(100%) contrast(0.5);
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
[disabled] &[type=submit],
|
[disabled] &[type=submit],
|
||||||
&[type=submit]:disabled {
|
&[type=submit]:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ class AccountsController < ApplicationController
|
|||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
set_page_and_extract_portion_from account_users.ordered, per_page: 500
|
users = account_users.ordered.without_bots
|
||||||
|
@administrators, @members = users.partition(&:administrator?)
|
||||||
|
set_page_and_extract_portion_from users, per_page: 500
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -67,17 +67,27 @@ module Authentication
|
|||||||
authenticated_as session
|
authenticated_as session
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def terminate_current_session
|
||||||
|
Current.session&.destroy!
|
||||||
|
reset_session
|
||||||
|
remove_authentication_cookie
|
||||||
|
end
|
||||||
|
|
||||||
def authenticated_as(session)
|
def authenticated_as(session)
|
||||||
Current.user = session.user
|
Current.session = session
|
||||||
set_authenticated_by(:session)
|
set_authenticated_by(:session)
|
||||||
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
|
set_authentication_cookie(session)
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_authenticating_url
|
def post_authenticating_url
|
||||||
session.delete(:return_to_after_authenticating) || root_url
|
session.delete(:return_to_after_authenticating) || root_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_authentication
|
def set_authentication_cookie(session)
|
||||||
|
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_authentication_cookie
|
||||||
cookies.delete(:session_token)
|
cookies.delete(:session_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class FirstRunsController < ApplicationController
|
|||||||
user = FirstRun.create!(user_params)
|
user = FirstRun.create!(user_params)
|
||||||
start_new_session_for user
|
start_new_session_for user
|
||||||
|
|
||||||
|
redirect_to root_url
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
redirect_to root_url
|
redirect_to root_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
class Rooms::InvolvementsController < ApplicationController
|
class Rooms::InvolvementsController < ApplicationController
|
||||||
include RoomScoped
|
include RoomScoped
|
||||||
|
|
||||||
before_action :ensure_can_administer, only: %i[ update ]
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@involvement = @membership.involvement
|
@involvement = @membership.involvement
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class SessionsController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
remove_push_subscription
|
remove_push_subscription
|
||||||
reset_authentication
|
terminate_current_session
|
||||||
redirect_to root_url
|
redirect_to root_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Users::SidebarsController < ApplicationController
|
|||||||
|
|
||||||
def find_direct_placeholder_users
|
def find_direct_placeholder_users
|
||||||
exclude_user_ids = user_ids_already_in_direct_rooms_with_current_user.including(Current.user.id)
|
exclude_user_ids = user_ids_already_in_direct_rooms_with_current_user.including(Current.user.id)
|
||||||
User.active.where.not(id: exclude_user_ids).order(:created_at).limit(DIRECT_PLACEHOLDERS - exclude_user_ids.count)
|
User.active.where.not(id: exclude_user_ids).order(:created_at).limit([ DIRECT_PLACEHOLDERS - exclude_user_ids.count, 0 ].max)
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_ids_already_in_direct_rooms_with_current_user
|
def user_ids_already_in_direct_rooms_with_current_user
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
class Current < ActiveSupport::CurrentAttributes
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :user, :request
|
attribute :session, :user, :request
|
||||||
|
|
||||||
delegate :host, :protocol, to: :request, prefix: true, allow_nil: true
|
delegate :host, :protocol, to: :request, prefix: true, allow_nil: true
|
||||||
|
|
||||||
|
def session=(value)
|
||||||
|
super(value)
|
||||||
|
|
||||||
|
if value.present?
|
||||||
|
self.user = session.user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def account
|
def account
|
||||||
Account.first
|
Account.first
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -100,7 +100,13 @@
|
|||||||
|
|
||||||
<menu class="flex flex-column gap margin-none pad">
|
<menu class="flex flex-column gap margin-none pad">
|
||||||
<turbo-frame id="account_users">
|
<turbo-frame id="account_users">
|
||||||
<%= render partial: "accounts/users/user", collection: @page.records, as: :user %>
|
<%= render partial: "accounts/users/user", collection: @administrators, as: :user %>
|
||||||
|
|
||||||
|
<% if @administrators.any? && @members.any? %>
|
||||||
|
<hr class="separator full-width" style="--border-style: solid">
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= render partial: "accounts/users/user", collection: @members, as: :user %>
|
||||||
<%= render "accounts/users/next_page_container", page: @page.next_param unless @page.last? %>
|
<%= render "accounts/users/next_page_container", page: @page.next_param unless @page.last? %>
|
||||||
</turbo-frame>
|
</turbo-frame>
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -9,23 +9,26 @@
|
|||||||
|
|
||||||
<hr class="separator" aria-hidden="true">
|
<hr class="separator" aria-hidden="true">
|
||||||
|
|
||||||
<% if Current.user.can_administer? && user != Current.user && user.active? %>
|
<% if Current.user.can_administer? && user.active? %>
|
||||||
<% unless user.bot? %>
|
<% unless user.bot? %>
|
||||||
<%= form_with model: user, url: account_user_path(user), data: { controller: "form" }, method: :patch do | form | %>
|
<%= form_with model: user, url: account_user_path(user), data: { controller: "form" }, method: :patch do | form | %>
|
||||||
<label class="btn txt-small flex-item-no-shrink" for="<%= dom_id(user, :role) %>">
|
<label class="btn txt-small flex-item-no-shrink" for="<%= dom_id(user, :role) %>">
|
||||||
<span class="for-screen-reader">Role: <%= user.administrator? ? "Administrator" : "Member" %></span>
|
<span class="for-screen-reader">Role: <%= user.administrator? ? "Administrator" : "Member" %></span>
|
||||||
<%= image_tag "crown.svg", size: 20, aria: { hidden: "true" } %>
|
<%= image_tag "crown.svg", size: 20, aria: { hidden: "true" } %>
|
||||||
<%= form.check_box :role, { data: { action: "form#submit" }, hidden: true, id: dom_id(user, :role) }, "administrator", "member" %>
|
<%= form.check_box :role, { data: { action: "form#submit" }, hidden: true, id: dom_id(user, :role), disabled: user == Current.user }, "administrator", "member" %>
|
||||||
</label>
|
</label>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= button_to account_user_path(user), method: :delete, class: "btn txt-small flex-item-no-shrink btn--negative", data: {
|
<% unless user == Current.user %>
|
||||||
turbo_confirm: "Are you sure you want to permanently remove this person from the account? This can’t be undone."} do %>
|
<%= button_to account_user_path(user), method: :delete, class: "btn txt-small flex-item-no-shrink btn--negative", data: {
|
||||||
<%= image_tag "minus.svg", size: 20, aria: { hidden: "true" } %>
|
turbo_confirm: "Are you sure you want to permanently remove this person from the account? This can’t be undone."} do %>
|
||||||
<span class="for-screen-reader">Delete <%= user.name %></span>
|
<%= image_tag "minus.svg", size: 20, aria: { hidden: "true" } %>
|
||||||
|
<span class="for-screen-reader">Delete <%= user.name %></span>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% elsif user == Current.user %>
|
<% end %>
|
||||||
|
<% if user == Current.user %>
|
||||||
<%= link_to user_profile_path, class: "btn txt-small flex-item-no-shrink", target: "_top" do %>
|
<%= link_to user_profile_path, class: "btn txt-small flex-item-no-shrink", target: "_top" do %>
|
||||||
<%= image_tag "pencil.svg", size: 20, aria: { hidden: "true" } %>
|
<%= image_tag "pencil.svg", size: 20, aria: { hidden: "true" } %>
|
||||||
<span class="for-screen-reader">My settings</span>
|
<span class="for-screen-reader">My settings</span>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<%= button_to user_ban_path(user), method: :delete,
|
<%= button_to user_ban_path(user), method: :delete,
|
||||||
class: "btn btn--negative full-width",
|
class: "btn btn--negative full-width",
|
||||||
data: { turbo_confirm: "Are you sure you want to remove the ban on this user?" } do %>
|
data: { turbo_confirm: "Are you sure you want to remove the ban on this user?" } do %>
|
||||||
<%= image_tag "cancel.svg", aria: { hidden: "true", label: "Ban #{@user.name}" } %>
|
<%= image_tag "cancel.svg", aria: { hidden: "true", label: "Remove Ban #{@user.name}" } %>
|
||||||
<span>Remove ban</span>
|
<span>Remove ban</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
+11
@@ -30,6 +30,17 @@ EXPORT_DEPLOY_LOCATION = "/local/app/product_releases/"
|
|||||||
|
|
||||||
abort "You must specify a version, e.g. `bin/release 1.0.0`" unless VERSION
|
abort "You must specify a version, e.g. `bin/release 1.0.0`" unless VERSION
|
||||||
|
|
||||||
|
current_branch = `git rev-parse --abbrev-ref HEAD`.strip
|
||||||
|
if current_branch != "main"
|
||||||
|
print "\e[1;33mWarning: You are on branch '#{current_branch}', not 'main'. Continue? [y/N] \e[0m"
|
||||||
|
abort "Release aborted." unless $stdin.gets.strip.downcase.chars.first == "y"
|
||||||
|
end
|
||||||
|
|
||||||
|
unless `git status --porcelain`.strip.empty?
|
||||||
|
print "\e[1;33mWarning: You have uncommitted changes. Continue? [y/N] \e[0m"
|
||||||
|
abort "Release aborted." unless $stdin.gets.strip.downcase.chars.first == "y"
|
||||||
|
end
|
||||||
|
|
||||||
def announcing(message)
|
def announcing(message)
|
||||||
puts "\n\e[1;36m#{message}\e[0m"
|
puts "\n\e[1;36m#{message}\e[0m"
|
||||||
yield
|
yield
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ echo
|
|||||||
if command -v brew &>/dev/null; then
|
if command -v brew &>/dev/null; then
|
||||||
step "Installing packages" brew install sqlite ffmpeg mise
|
step "Installing packages" brew install sqlite ffmpeg mise
|
||||||
elif command -v pacman &>/dev/null; then
|
elif command -v pacman &>/dev/null; then
|
||||||
step "Installing packages" sudo pacman -S --noconfirm --needed sqlite ffmpeg mise
|
packages=(sqlite ffmpeg mise)
|
||||||
|
if ! pacman -Q "${packages[@]}" >/dev/null 2>&1; then
|
||||||
|
step "Installing packages" sudo pacman -S --noconfirm --needed "${packages[@]}"
|
||||||
|
fi
|
||||||
elif command -v apt &>/dev/null; then
|
elif command -v apt &>/dev/null; then
|
||||||
step "Installing packages" sudo apt-get install --no-install-recommends -y libsqlite3-0 ffmpeg
|
step "Installing packages" sudo apt-get install --no-install-recommends -y libsqlite3-0 ffmpeg
|
||||||
fi
|
fi
|
||||||
@@ -100,6 +103,20 @@ if ! redis_running; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Install GitHub Actions linting tools
|
||||||
|
for tool in actionlint shellcheck zizmor; do
|
||||||
|
if ! command -v "$tool" &> /dev/null; then
|
||||||
|
if command -v brew &> /dev/null; then
|
||||||
|
step "Installing $tool" brew install "$tool"
|
||||||
|
elif command -v pacman &> /dev/null; then
|
||||||
|
step "Installing $tool" sudo pacman -S --noconfirm "$tool"
|
||||||
|
else
|
||||||
|
echo "Error: install $tool manually" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
step "Cleaning up logs and tempfiles" rails log:clear tmp:clear
|
step "Cleaning up logs and tempfiles" rails log:clear tmp:clear
|
||||||
|
|
||||||
step "Restarting services" rails restart
|
step "Restarting services" rails restart
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ CI.run do
|
|||||||
step "Setup", "bin/setup --skip-server"
|
step "Setup", "bin/setup --skip-server"
|
||||||
|
|
||||||
step "Style: Ruby", "bin/rubocop"
|
step "Style: Ruby", "bin/rubocop"
|
||||||
|
step "Style: GitHub Actions (actionlint)", "actionlint"
|
||||||
|
step "Style: GitHub Actions (zizmor)", "zizmor ."
|
||||||
|
|
||||||
step "Security: Gem audit", "bin/bundler-audit"
|
step "Security: Gem audit", "bin/bundler-audit"
|
||||||
step "Security: Importmap vulnerability audit", "bin/importmap audit"
|
step "Security: Importmap vulnerability audit", "bin/importmap audit"
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
|||||||
pin "@hotwired/turbo-rails", to: "turbo.js"
|
pin "@hotwired/turbo-rails", to: "turbo.js"
|
||||||
pin "@rails/actioncable", to: "actioncable.esm.js"
|
pin "@rails/actioncable", to: "actioncable.esm.js"
|
||||||
pin "@rails/request.js", to: "@rails--request.js" # @0.0.8
|
pin "@rails/request.js", to: "@rails--request.js" # @0.0.8
|
||||||
pin "trix", to: "trix.esm.min.js" # @2.0.10
|
pin "trix", to: "trix.esm.min.js" # @2.1.19
|
||||||
pin "@rails/actiontext", to: "actiontext.js"
|
pin "@rails/actiontext", to: "actiontext.js"
|
||||||
pin "highlight.js", to: "highlight.js/core.js"
|
pin "highlight.js", to: "highlight.js/core.js"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
class AddSingletonConstraintToAccounts < ActiveRecord::Migration[8.2]
|
||||||
|
def change
|
||||||
|
add_column :accounts, :singleton_guard, :integer, default: 0, null: false
|
||||||
|
add_index :accounts, :singleton_guard, unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+3
-1
@@ -10,14 +10,16 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.2].define(version: 2025_11_26_130131) do
|
ActiveRecord::Schema[8.2].define(version: 2025_12_12_154340) do
|
||||||
create_table "accounts", force: :cascade do |t|
|
create_table "accounts", force: :cascade do |t|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.text "custom_styles"
|
t.text "custom_styles"
|
||||||
t.string "join_code", null: false
|
t.string "join_code", null: false
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.json "settings"
|
t.json "settings"
|
||||||
|
t.integer "singleton_guard", default: 0, null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["singleton_guard"], name: "index_accounts_on_singleton_guard", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "action_text_rich_texts", force: :cascade do |t|
|
create_table "action_text_rich_texts", force: :cascade do |t|
|
||||||
|
|||||||
@@ -12,21 +12,32 @@ ActiveSupport.on_load(:action_text_content) do
|
|||||||
|
|
||||||
def attachable_from_possibly_expired_sgid(sgid)
|
def attachable_from_possibly_expired_sgid(sgid)
|
||||||
if message = sgid&.split("--")&.first
|
if message = sgid&.split("--")&.first
|
||||||
encoded_message = JSON.parse Base64.strict_decode64(message)
|
encoded_message = JSON.parse(decode_base64(message))
|
||||||
|
|
||||||
decoded_gid = if data = encoded_message.dig("_rails", "data")
|
decoded_gid = if data = encoded_message.dig("_rails", "data")
|
||||||
data
|
data
|
||||||
|
elsif data = encoded_message.dig("_rails", "message")
|
||||||
|
# Rails 7 used an older format of GID that serialized the payload using Marshall
|
||||||
|
# Since we intentionally skip signature verification, we can't safely unmarshal the data
|
||||||
|
# To work around this, we manually extract the GID from the marshaled data
|
||||||
|
decode_base64(data).match(%r{(gid://campfire/[^/]+/\d+)})&.to_s
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
model = GlobalID.find(decoded_gid)
|
if model = GlobalID.find(decoded_gid)
|
||||||
|
model.model_name.to_s.in?(ATTACHABLES_PERMITTED_WITH_INVALID_SIGNATURES) ? model : nil
|
||||||
model.model_name.to_s.in?(ATTACHABLES_PERMITTED_WITH_INVALID_SIGNATURES) ? model : nil
|
end
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def decode_base64(message)
|
||||||
|
Base64.strict_decode64(message)
|
||||||
|
rescue => _e
|
||||||
|
Base64.urlsafe_decode64(message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -16,7 +16,7 @@ module RestrictedHTTP
|
|||||||
|
|
||||||
def private_ip?(ip)
|
def private_ip?(ip)
|
||||||
IPAddr.new(ip).then do |ipaddr|
|
IPAddr.new(ip).then do |ipaddr|
|
||||||
ipaddr.private? || ipaddr.loopback? || LOCAL_IP.include?(ipaddr)
|
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? || ipaddr.ipv4_mapped? || ipaddr.ipv4_compat? || LOCAL_IP.include?(ipaddr)
|
||||||
end
|
end
|
||||||
rescue IPAddr::InvalidAddressError
|
rescue IPAddr::InvalidAddressError
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -10,6 +10,38 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_response :ok
|
assert_response :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "edit groups administrators separately from members with a divider" do
|
||||||
|
get edit_account_url
|
||||||
|
|
||||||
|
assert_response :ok
|
||||||
|
|
||||||
|
# Verify the divider exists between administrator and member sections
|
||||||
|
assert_select "turbo-frame#account_users hr.separator.full-width"
|
||||||
|
|
||||||
|
# Verify administrators appear before the divider and members appear after
|
||||||
|
# by checking the order of user names in the response body
|
||||||
|
administrators = users(:david, :jason).map(&:name)
|
||||||
|
members = users(:jz, :kevin).map(&:name)
|
||||||
|
|
||||||
|
response_body = response.body
|
||||||
|
|
||||||
|
# Find positions of divider and user names
|
||||||
|
divider_position = response_body.index('hr class="separator full-width"')
|
||||||
|
assert divider_position, "Divider should exist in the response"
|
||||||
|
|
||||||
|
administrators.each do |name|
|
||||||
|
name_position = response_body.index("<strong>#{name}</strong>")
|
||||||
|
assert name_position, "Administrator #{name} should appear in the response"
|
||||||
|
assert name_position < divider_position, "Administrator #{name} should appear before the divider"
|
||||||
|
end
|
||||||
|
|
||||||
|
members.each do |name|
|
||||||
|
name_position = response_body.index("<strong>#{name}</strong>")
|
||||||
|
assert name_position, "Member #{name} should appear in the response"
|
||||||
|
assert name_position > divider_position, "Member #{name} should appear after the divider"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "update" do
|
test "update" do
|
||||||
assert users(:david).administrator?
|
assert users(:david).administrator?
|
||||||
|
|
||||||
|
|||||||
@@ -30,4 +30,28 @@ class FirstRunsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
assert parsed_cookies.signed[:session_token]
|
assert parsed_cookies.signed[:session_token]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "create is not vulnerable to race conditions" do
|
||||||
|
num_attackers = 5
|
||||||
|
url = first_run_url
|
||||||
|
barrier = Concurrent::CyclicBarrier.new(num_attackers)
|
||||||
|
|
||||||
|
num_attackers.times.map do |i|
|
||||||
|
Thread.new do
|
||||||
|
session = ActionDispatch::Integration::Session.new(Rails.application)
|
||||||
|
barrier.wait # All threads wait here, then fire simultaneously
|
||||||
|
|
||||||
|
session.post url, params: {
|
||||||
|
user: {
|
||||||
|
name: "Attacker#{i}",
|
||||||
|
email_address: "attacker#{i}@example.com",
|
||||||
|
password: "password123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end.each(&:join)
|
||||||
|
|
||||||
|
assert_equal 1, Account.count, "Race condition allowed #{Account.count} accounts to be created!"
|
||||||
|
assert_equal 1, User.where(role: :administrator).count, "Race condition allowed multiple admin users!"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -43,4 +43,13 @@ class Rooms::InvolvementsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "a non-admin can update their room involvement" do
|
||||||
|
sign_in :jz
|
||||||
|
|
||||||
|
assert_changes -> { memberships(:jz_designers).reload.involvement }, from: "everything", to: "mentions" do
|
||||||
|
put room_involvement_url(rooms(:designers)), params: { involvement: "mentions" }
|
||||||
|
assert_redirected_to room_involvement_url(rooms(:designers))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -28,7 +28,9 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "create with valid credentials" do
|
test "create with valid credentials" do
|
||||||
post session_url, params: { email_address: "david@37signals.com", password: "secret123456" }
|
assert_difference -> { Session.count }, +1 do
|
||||||
|
post session_url, params: { email_address: "david@37signals.com", password: "secret123456" }
|
||||||
|
end
|
||||||
|
|
||||||
assert_redirected_to root_url
|
assert_redirected_to root_url
|
||||||
assert parsed_cookies.signed[:session_token]
|
assert parsed_cookies.signed[:session_token]
|
||||||
@@ -43,11 +45,15 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
test "destroy" do
|
test "destroy" do
|
||||||
sign_in :david
|
sign_in :david
|
||||||
|
session = users(:david).sessions.last
|
||||||
|
|
||||||
delete session_url
|
assert_difference -> { Session.count }, -1 do
|
||||||
|
delete session_url
|
||||||
|
end
|
||||||
|
|
||||||
assert_redirected_to root_url
|
assert_redirected_to root_url
|
||||||
assert_not cookies[:session_token].present?
|
assert_not cookies[:session_token].present?
|
||||||
|
assert_nil Session.find_by(id: session.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "destroy removes the push subscription for the device" do
|
test "destroy removes the push subscription for the device" do
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ActionText::AttachmentTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@user = users(:david)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "from_node" do
|
||||||
|
html = %Q(<action-text-attachment sgid="#{@user.attachable_sgid}"></action-text-attachment>)
|
||||||
|
node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
|
||||||
|
|
||||||
|
attachment = ActionText::Attachment.from_node(node)
|
||||||
|
assert_equal @user, attachment.attachable
|
||||||
|
end
|
||||||
|
|
||||||
|
test "from_node with a Rails 7 SGID" do
|
||||||
|
gid = @user.to_gid.to_s
|
||||||
|
marshaled_gid = Base64.urlsafe_encode64(Marshal.dump(gid))
|
||||||
|
rails7_payload = { "_rails" => { "message" => marshaled_gid, "exp" => nil, "pur" => "attachable" } }
|
||||||
|
rails7_message = Base64.strict_encode64(JSON.generate(rails7_payload))
|
||||||
|
rails7_sgid = "#{rails7_message}--invalidsignature"
|
||||||
|
|
||||||
|
html = %Q(<action-text-attachment sgid="#{rails7_sgid}"></action-text-attachment>)
|
||||||
|
node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
|
||||||
|
|
||||||
|
attachment = ActionText::Attachment.from_node(node)
|
||||||
|
assert_equal @user, attachment.attachable
|
||||||
|
end
|
||||||
|
|
||||||
|
test "from_node with an invalid SGID" do
|
||||||
|
room = rooms(:pets).tap { |r| r.extend ActionText::Attachable }
|
||||||
|
|
||||||
|
html = %Q(<action-text-attachment sgid="#{room.attachable_sgid}invalid"></action-text-attachment>)
|
||||||
|
node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
|
||||||
|
|
||||||
|
attachment = ActionText::Attachment.from_node(node)
|
||||||
|
assert_kind_of ActionText::Attachables::MissingAttachable, attachment.attachable
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
require "test_helper"
|
||||||
|
require "restricted_http/private_network_guard"
|
||||||
|
|
||||||
|
class RestrictedHTTP::PrivateNetworkGuardTest < ActiveSupport::TestCase
|
||||||
|
test "private_ip? returns true for 'This' network (RFC1700)" do
|
||||||
|
assert_private_ip "0.0.0.0"
|
||||||
|
assert_private_ip "0.255.255.255"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for loopback addresses" do
|
||||||
|
assert_private_ip "127.0.0.0"
|
||||||
|
assert_private_ip "127.0.0.1"
|
||||||
|
assert_private_ip "127.255.255.255"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for RFC1918 private addresses" do
|
||||||
|
assert_private_ip "10.0.0.0"
|
||||||
|
assert_private_ip "10.255.255.255"
|
||||||
|
assert_private_ip "172.16.0.0"
|
||||||
|
assert_private_ip "172.31.255.255"
|
||||||
|
assert_private_ip "192.168.0.0"
|
||||||
|
assert_private_ip "192.168.255.255"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for link-local addresses" do
|
||||||
|
assert_private_ip "169.254.0.1"
|
||||||
|
assert_private_ip "169.254.169.254" # AWS IMDS
|
||||||
|
assert_private_ip "169.254.255.255"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns false for public addresses" do
|
||||||
|
assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("93.184.216.34")
|
||||||
|
assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("8.8.8.8")
|
||||||
|
end
|
||||||
|
|
||||||
|
# IPv6 address format tests (SSRF bypass prevention)
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-mapped IPv6 addresses with private IPs" do
|
||||||
|
assert_private_ip "::ffff:192.168.1.1"
|
||||||
|
assert_private_ip "::ffff:10.0.0.1"
|
||||||
|
assert_private_ip "::ffff:172.16.0.1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-mapped IPv6 addresses with link-local IPs" do
|
||||||
|
assert_private_ip "::ffff:169.254.169.254" # AWS metadata via mapped format
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-mapped IPv6 addresses even with public IPs" do
|
||||||
|
# Block all ipv4_mapped? since DNS never returns this format legitimately
|
||||||
|
assert_private_ip "::ffff:93.184.216.34"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-compatible IPv6 addresses with private IPs" do
|
||||||
|
assert_private_ip "::192.168.1.1"
|
||||||
|
assert_private_ip "::10.0.0.1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-compatible IPv6 addresses with link-local IPs" do
|
||||||
|
assert_private_ip "::169.254.169.254" # AWS metadata via compat format - the reported bypass
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-compatible IPv6 addresses even with public IPs" do
|
||||||
|
# Block all ipv4_compat? since DNS never returns this format legitimately
|
||||||
|
assert_private_ip "::93.184.216.34"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for invalid addresses" do
|
||||||
|
assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("not-an-ip")
|
||||||
|
assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "resolve raises Violation for private hostname" do
|
||||||
|
Resolv.stubs(:getaddress).returns("192.168.1.1")
|
||||||
|
assert_raises RestrictedHTTP::Violation do
|
||||||
|
RestrictedHTTP::PrivateNetworkGuard.resolve("private.example.com")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "resolve returns IP for public hostname" do
|
||||||
|
Resolv.stubs(:getaddress).returns("93.184.216.34")
|
||||||
|
assert_equal "93.184.216.34", RestrictedHTTP::PrivateNetworkGuard.resolve("example.com")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def assert_private_ip(address)
|
||||||
|
assert RestrictedHTTP::PrivateNetworkGuard.private_ip?(address),
|
||||||
|
"Expected #{address} to be classified as private"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -21,6 +21,28 @@ class Opengraph::LocationTest < ActiveSupport::TestCase
|
|||||||
assert_equal [ "is not public" ], location.errors[:url]
|
assert_equal [ "is not public" ], location.errors[:url]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "link-local addresses are blocked" do
|
||||||
|
Resolv.stubs(:getaddress).with("metadata.internal").returns("169.254.169.254")
|
||||||
|
|
||||||
|
location = Opengraph::Location.new("https://metadata.internal")
|
||||||
|
assert_not location.valid?
|
||||||
|
assert_equal [ "is not public" ], location.errors[:url]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "IPv6 addresses mapped to IPv4 addresses are blocked" do
|
||||||
|
Resolv.stubs(:getaddress).with("metadata.internal").returns("::ffff:192.168.1.1")
|
||||||
|
|
||||||
|
location = Opengraph::Location.new("https://metadata.internal")
|
||||||
|
assert_not location.valid?
|
||||||
|
assert_equal [ "is not public" ], location.errors[:url]
|
||||||
|
|
||||||
|
Resolv.stubs(:getaddress).with("metadata.internal").returns("::ffff:c0a8:0101")
|
||||||
|
|
||||||
|
location = Opengraph::Location.new("https://metadata.internal")
|
||||||
|
assert_not location.valid?
|
||||||
|
assert_equal [ "is not public" ], location.errors[:url]
|
||||||
|
end
|
||||||
|
|
||||||
test "avoid reading file urls when expecting HTML" do
|
test "avoid reading file urls when expecting HTML" do
|
||||||
large_file = Opengraph::Location.new("https://www.example.com/100gb.zip")
|
large_file = Opengraph::Location.new("https://www.example.com/100gb.zip")
|
||||||
|
|
||||||
|
|||||||
Vendored
+4
-3
@@ -1,6 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Trix 2.0.10
|
Trix 2.1.19
|
||||||
Copyright © 2024 37signals, LLC
|
Copyright © 2026 37signals, LLC
|
||||||
*/
|
*/
|
||||||
var t="2.0.10";const e="[data-trix-attachment]",i={preview:{presentation:"gallery",caption:{name:!0,size:!0}},file:{caption:{size:!0}}},n={default:{tagName:"div",parse:!1},quote:{tagName:"blockquote",nestable:!0},heading1:{tagName:"h1",terminal:!0,breakOnReturn:!0,group:!1},code:{tagName:"pre",terminal:!0,text:{plaintext:!0}},bulletList:{tagName:"ul",parse:!1},bullet:{tagName:"li",listAttribute:"bulletList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},numberList:{tagName:"ol",parse:!1},number:{tagName:"li",listAttribute:"numberList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},attachmentGallery:{tagName:"div",exclusive:!0,terminal:!0,parse:!1,group:!1}},r=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},o=navigator.userAgent.match(/android\s([0-9]+.*Chrome)/i),s=o&&parseInt(o[1]);var a={composesExistingText:/Android.*Chrome/.test(navigator.userAgent),recentAndroid:s&&s>12,samsungAndroid:s&&navigator.userAgent.match(/Android.*SM-/),forcesObjectResizing:/Trident.*rv:11/.test(navigator.userAgent),supportsInputEvents:"undefined"!=typeof InputEvent&&["data","getTargetRanges","inputType"].every((t=>t in InputEvent.prototype))},l={attachFiles:"Attach Files",bold:"Bold",bullets:"Bullets",byte:"Byte",bytes:"Bytes",captionPlaceholder:"Add a caption…",code:"Code",heading1:"Heading",indent:"Increase Level",italic:"Italic",link:"Link",numbers:"Numbers",outdent:"Decrease Level",quote:"Quote",redo:"Redo",remove:"Remove",strike:"Strikethrough",undo:"Undo",unlink:"Unlink",url:"URL",urlPlaceholder:"Enter a URL…",GB:"GB",KB:"KB",MB:"MB",PB:"PB",TB:"TB"};const c=[l.bytes,l.KB,l.MB,l.GB,l.TB,l.PB];var h={prefix:"IEC",precision:2,formatter(t){switch(t){case 0:return"0 ".concat(l.bytes);case 1:return"1 ".concat(l.byte);default:let e;"SI"===this.prefix?e=1e3:"IEC"===this.prefix&&(e=1024);const i=Math.floor(Math.log(t)/Math.log(e)),n=(t/Math.pow(e,i)).toFixed(this.precision).replace(/0*$/,"").replace(/\.$/,"");return"".concat(n," ").concat(c[i])}}};const u="\ufeff",d=" ",g=function(t){for(const e in t){const i=t[e];this[e]=i}return this},m=document.documentElement,p=m.matches,f=function(t){let{onElement:e,matchingSelector:i,withCallback:n,inPhase:r,preventDefault:o,times:s}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const a=e||m,l=i,c="capturing"===r,h=function(t){null!=s&&0==--s&&h.destroy();const e=A(t.target,{matchingSelector:l});null!=e&&(null==n||n.call(e,t,e),o&&t.preventDefault())};return h.destroy=()=>a.removeEventListener(t,h,c),a.addEventListener(t,h,c),h},b=function(t){let{onElement:e,bubbles:i,cancelable:n,attributes:r}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const o=null!=e?e:m;i=!1!==i,n=!1!==n;const s=document.createEvent("Events");return s.initEvent(t,i,n),null!=r&&g.call(s,r),o.dispatchEvent(s)},v=function(t,e){if(1===(null==t?void 0:t.nodeType))return p.call(t,e)},A=function(t){let{matchingSelector:e,untilNode:i}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};for(;t&&t.nodeType!==Node.ELEMENT_NODE;)t=t.parentNode;if(null!=t){if(null==e)return t;if(t.closest&&null==i)return t.closest(e);for(;t&&t!==i;){if(v(t,e))return t;t=t.parentNode}}},x=t=>document.activeElement!==t&&y(t,document.activeElement),y=function(t,e){if(t&&e)for(;e;){if(e===t)return!0;e=e.parentNode}},C=function(t){var e;if(null===(e=t)||void 0===e||!e.parentNode)return;let i=0;for(t=t.previousSibling;t;)i++,t=t.previousSibling;return i},R=t=>{var e;return null==t||null===(e=t.parentNode)||void 0===e?void 0:e.removeChild(t)},S=function(t){let{onlyNodesOfType:e,usingFilter:i,expandEntityReferences:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const r=(()=>{switch(e){case"element":return NodeFilter.SHOW_ELEMENT;case"text":return NodeFilter.SHOW_TEXT;case"comment":return NodeFilter.SHOW_COMMENT;default:return NodeFilter.SHOW_ALL}})();return document.createTreeWalker(t,r,null!=i?i:null,!0===n)},E=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},k=function(t){let e,i,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};"object"==typeof t?(n=t,t=n.tagName):n={attributes:n};const r=document.createElement(t);if(null!=n.editable&&(null==n.attributes&&(n.attributes={}),n.attributes.contenteditable=n.editable),n.attributes)for(e in n.attributes)i=n.attributes[e],r.setAttribute(e,i);if(n.style)for(e in n.style)i=n.style[e],r.style[e]=i;if(n.data)for(e in n.data)i=n.data[e],r.dataset[e]=i;return n.className&&n.className.split(" ").forEach((t=>{r.classList.add(t)})),n.textContent&&(r.textContent=n.textContent),n.childNodes&&[].concat(n.childNodes).forEach((t=>{r.appendChild(t)})),r};let L;const D=function(){if(null!=L)return L;L=[];for(const t in n){const e=n[t];e.tagName&&L.push(e.tagName)}return L},w=t=>B(null==t?void 0:t.firstChild),T=function(t){let{strict:e}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{strict:!0};return e?B(t):B(t)Line truncated
|
var t="2.1.19";const e="[data-trix-attachment]",i={preview:{presentation:"gallery",caption:{name:!0,size:!0}},file:{caption:{size:!0}}},n={default:{tagName:"div",parse:!1},quote:{tagName:"blockquote",nestable:!0},heading1:{tagName:"h1",terminal:!0,breakOnReturn:!0,group:!1},code:{tagName:"pre",terminal:!0,htmlAttributes:["language"],text:{plaintext:!0}},bulletList:{tagName:"ul",parse:!1},bullet:{tagName:"li",listAttribute:"bulletList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},numberList:{tagName:"ol",parse:!1},number:{tagName:"li",listAttribute:"numberList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},attachmentGallery:{tagName:"div",exclusive:!0,terminal:!0,parse:!1,group:!1}},r=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},o=navigator.userAgent.match(/android\s([0-9]+.*Chrome)/i),s=o&&parseInt(o[1]);var a={composesExistingText:/Android.*Chrome/.test(navigator.userAgent),recentAndroid:s&&s>12,samsungAndroid:s&&navigator.userAgent.match(/Android.*SM-/),forcesObjectResizing:/Trident.*rv:11/.test(navigator.userAgent),supportsInputEvents:"undefined"!=typeof InputEvent&&["data","getTargetRanges","inputType"].every((t=>t in InputEvent.prototype))},l={ADD_ATTR:["language"],SAFE_FOR_XML:!1,RETURN_DOM:!0},c={attachFiles:"Attach Files",bold:"Bold",bullets:"Bullets",byte:"Byte",bytes:"Bytes",captionPlaceholder:"Add a caption…",code:"Code",heading1:"Heading",indent:"Increase Level",italic:"Italic",link:"Link",numbers:"Numbers",outdent:"Decrease Level",quote:"Quote",redo:"Redo",remove:"Remove",strike:"Strikethrough",undo:"Undo",unlink:"Unlink",url:"URL",urlPlaceholder:"Enter a URL…",GB:"GB",KB:"KB",MB:"MB",PB:"PB",TB:"TB"};const u=[c.bytes,c.KB,c.MB,c.GB,c.TB,c.PB];var h={prefix:"IEC",precision:2,formatter(t){switch(t){case 0:return"0 ".concat(c.bytes);case 1:return"1 ".concat(c.byte);default:let e;"SI"===this.prefix?e=1e3:"IEC"===this.prefix&&(e=1024);const i=Math.floor(Math.log(t)/Math.log(e)),n=(t/Math.pow(e,i)).toFixed(this.precision).replace(/0*$/,"").replace(/\.$/,"");return"".concat(n," ").concat(u[i])}}};const d="\ufeff",g=" ",m=function(t){for(const e in t){const i=t[e];this[e]=i}return this},p=document.documentElement,f=p.matches,b=function(t){let{onElement:e,matchingSelector:i,withCallback:n,inPhase:r,preventDefault:o,times:s}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const a=e||p,l=i,c="capturing"===r,u=function(t){null!=s&&0==--s&&u.destroy();const e=x(t.target,{matchingSelector:l});null!=e&&(null==n||n.call(e,t,e),o&&t.preventDefault())};return u.destroy=()=>a.removeEventListener(t,u,c),a.addEventListener(t,u,c),u},v=function(t){let{bubbles:e,cancelable:i,attributes:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};e=!1!==e,i=!1!==i;const r=document.createEvent("Events");return r.initEvent(t,e,i),null!=n&&m.call(r,n),r},A=function(t){let{onElement:e,bubbles:i,cancelable:n,attributes:r}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const o=null!=e?e:p,s=v(t,{bubbles:i,cancelable:n,attributes:r});return o.dispatchEvent(s)},y=function(t,e){if(1===(null==t?void 0:t.nodeType))return f.call(t,e)},x=function(t){let{matchingSelector:e,untilNode:i}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};for(;t&&t.nodeType!==Node.ELEMENT_NODE;)t=t.parentNode;if(null!=t){if(null==e)return t;if(t.closest&&null==i)return t.closest(e);for(;t&&t!==i;){if(y(t,e))return t;t=t.parentNode}}},C=t=>document.activeElement!==t&&E(t,document.activeElement),E=function(t,e){if(t&&e)for(;e;){if(e===t)return!0;e=e.parentNode}},S=function(t){var e;if(null===(e=t)||void 0===e||!e.parentNode)return;let i=0;for(t=t.previousSibling;t;)i++,t=t.previousSibling;return i},R=t=>{var e;return null==t||null===(e=t.parentNode)||void 0===e?void 0:e.removeChild(t)},T=function(t){let{onlyNodesOfType:e,usingFilter:i,expandEntityReferences:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const r=(()=>{switch(e){case"element":return NodeFilter.SHOW_ELEMENT;case"text":return NodeFilter.SHOW_TEXT;case"comment":return NodeFilter.SHOW_COMMENT;default:return NodeFilter.SHOW_ALL}})();return document.createTreeWalker(t,r,null!=i?i:null,!0===n)},k=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},w=function(t){let e,i,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};"object"==typeof t?(n=t,t=n.tagName):n={attributes:n};const r=document.createElement(t);if(null!=n.editable&&(null==n.attributes&&(n.attributes={}),n.attributes.contenteditable=n.editable),n.attributes)for(e in n.attributes)i=n.attributes[e],r.setAttribute(e,i);if(n.style)for(e in n.style)i=n.style[e],r.style[e]=i;if(n.data)for(e in n.data)i=n.data[e],r.dataset[e]=i;return n.className&&n.className.split(" ").forEach((t=>{r.classList.add(t)})),n.textContent&&(r.textContent=n.textContent),n.childNodes&&[].concat(n.childNodes).forEach((t=>{r.appendChild(t)})),r};let D;const L=funLine truncated
|
||||||
|
/*! @license DOMPurify 3.4.2 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.2/LICENSE */const{entries:oe,setPrototypeOf:se,isFrozen:ae,getPrototypeOf:le,getOwnPropertyDescriptor:ce}=Object;let{freeze:ue,seal:he,create:de}=Object,{apply:ge,construct:me}="undefined"!=typeof Reflect&&Reflect;ue||(ue=function(t){return t}),he||(he=function(t){return t}),ge||(ge=function(t,e){for(var i=arguments.length,n=new Array(i>2?i-2:0),r=2;r<i;r++)n[r-2]=arguments[r];return t.apply(e,n)}),me||(me=function(t){for(var e=arguments.length,i=new Array(e>1?e-1:0),n=1;n<e;n++)i[n-1]=arguments[n];return new t(...i)});const pe=Me(Array.prototype.forEach),fe=Me(Array.prototype.lastIndexOf),be=Me(Array.prototype.pop),ve=Me(Array.prototype.push),Ae=Me(Array.prototype.splice),ye=Array.isArray,xe=Me(String.prototype.toLowerCase),Ce=Me(String.prototype.toString),Ee=Me(String.prototype.match),Se=Me(String.prototype.replace),Re=Me(String.prototype.indexOf),Te=Me(String.prototype.trim),ke=Me(Number.prototype.toString),we=Me(Boolean.prototype.toString),De="undefined"==typeof BigInt?null:Me(BigInt.prototype.toString),Le="undefined"==typeof Symbol?null:Me(Symbol.prototype.toString),Ne=Me(Object.prototype.hasOwnProperty),Ie=Me(Object.prototype.toString),Oe=Me(RegExp.prototype.test),Fe=(Pe=TypeError,function(){for(var t=arguments.length,e=new Array(t),i=0;i<t;i++)e[i]=arguments[i];return me(Pe,e)});var Pe;function Me(t){return function(e){e instanceof RegExp&&(e.lastIndex=0);for(var i=arguments.length,n=new Array(i>1?i-1:0),r=1;r<i;r++)n[r-1]=arguments[r];return ge(t,e,n)}}function Be(t,e){let i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:xe;if(se&&se(t,null),!ye(e))return t;let n=e.length;for(;n--;){let r=e[n];if("string"==typeof r){const t=i(r);t!==r&&(ae(e)||(e[n]=t),r=t)}t[r]=!0}return t}function _e(t){for(let e=0;e<t.length;e++){Ne(t,e)||(t[e]=null)}return t}function je(t){const e=de(null);for(const[i,n]of oe(t)){Ne(t,i)&&(ye(n)?e[i]=_e(n):n&&"object"==typeof n&&n.constructor===Object?e[i]=je(n):e[i]=n)}return e}function We(t,e){for(;null!==t;){const i=ce(t,e);if(i){if(i.get)return Me(i.get);if("function"==typeof i.value)return Me(i.value)}t=le(t)}return function(){return null}}const Ue=ue(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),Ve=ue(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),ze=ue(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),He=ue(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),qe=ue(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),Je=ue(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),Ke=ue(["#text"]),Ge=ue(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","contLine truncated
|
||||||
//# sourceMappingURL=trix.esm.min.js.map
|
//# sourceMappingURL=trix.esm.min.js.map
|
||||||
Reference in new issue
Block a user