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 |
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}` %>
|
||||
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 %>
|
||||
@@ -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:
|
||||
branches: [ main ]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||
with:
|
||||
ruby-version: .ruby-version
|
||||
bundler-cache: true
|
||||
@@ -25,12 +31,16 @@ jobs:
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||
with:
|
||||
ruby-version: .ruby-version
|
||||
bundler-cache: true
|
||||
@@ -38,11 +48,32 @@ jobs:
|
||||
- name: Lint code for consistent style
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
image: redis # zizmor: ignore[unpinned-images] -- version tag is fine for service containers
|
||||
ports:
|
||||
- 6379:6379
|
||||
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
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||
env:
|
||||
REDIS_URL: redis://localhost:6379/0
|
||||
with:
|
||||
@@ -66,9 +99,11 @@ jobs:
|
||||
|
||||
test_system:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
image: redis # zizmor: ignore[unpinned-images] -- version tag is fine for service containers
|
||||
ports:
|
||||
- 6379:6379
|
||||
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
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||
env:
|
||||
REDIS_URL: redis://localhost:6379/0
|
||||
with:
|
||||
|
||||
@@ -13,11 +13,7 @@ concurrency:
|
||||
group: publish-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
permissions: {}
|
||||
|
||||
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.
|
||||
@@ -27,6 +23,11 @@ jobs:
|
||||
build:
|
||||
name: Build and push image (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -43,13 +44,15 @@ jobs:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- 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
|
||||
uses: docker/login-action@v3.5.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -66,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Extract Docker metadata (tags, labels) with arch suffix
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.8.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
with:
|
||||
images: ${{ steps.vars.outputs.canonical }}
|
||||
tags: |
|
||||
@@ -84,7 +87,7 @@ jobs:
|
||||
|
||||
- name: Build and push (${{ matrix.platform }})
|
||||
id: build
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
@@ -102,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Attest image provenance (per-arch)
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/attest-build-provenance@v3.0.0
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
with:
|
||||
subject-name: ${{ steps.vars.outputs.canonical }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
@@ -113,16 +116,20 @@ jobs:
|
||||
needs: build
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
steps:
|
||||
- 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
|
||||
uses: docker/login-action@v3.5.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -139,7 +146,7 @@ jobs:
|
||||
|
||||
- name: Compute base tags (no suffix)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.8.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
with:
|
||||
images: ${{ steps.vars.outputs.canonical }}
|
||||
tags: |
|
||||
@@ -157,9 +164,11 @@ jobs:
|
||||
|
||||
- name: Create multi-arch manifests
|
||||
shell: bash
|
||||
env:
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
set -eu
|
||||
tags="${{ steps.meta.outputs.tags }}"
|
||||
tags="$TAGS"
|
||||
echo "Creating manifests for tags:"
|
||||
printf '%s\n' "$tags"
|
||||
while IFS= read -r tag; do
|
||||
@@ -185,13 +194,15 @@ jobs:
|
||||
done <<< "$tags"
|
||||
|
||||
- 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)
|
||||
shell: bash
|
||||
env:
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
set -eu
|
||||
tags="${{ steps.meta.outputs.tags }}"
|
||||
tags="$TAGS"
|
||||
printf '%s\n' "$tags"
|
||||
while IFS= read -r tag; do
|
||||
[ -z "$tag" ] && continue
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ FROM base AS build
|
||||
|
||||
# Install packages need to build gems
|
||||
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
|
||||
|
||||
# Install application gems
|
||||
|
||||
@@ -12,7 +12,7 @@ gem "sqlite3"
|
||||
gem "redis", "~> 5.4"
|
||||
|
||||
# Deployment
|
||||
gem "puma", "~> 6.6"
|
||||
gem "puma", "~> 7.2", ">= 7.2.1"
|
||||
|
||||
# Jobs
|
||||
gem "resque", "~> 2.7.0"
|
||||
@@ -48,6 +48,7 @@ gem "thruster"
|
||||
group :development, :test do
|
||||
gem "debug"
|
||||
gem "rubocop-rails-omakase", require: false
|
||||
gem "bundler-audit", require: false
|
||||
gem "faker", require: false
|
||||
gem "brakeman", require: false
|
||||
end
|
||||
|
||||
+36
-32
@@ -128,18 +128,21 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
action_text-trix (2.1.15)
|
||||
action_text-trix (2.1.19)
|
||||
railties
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
ast (2.4.3)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt (3.1.22)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (3.3.1)
|
||||
brakeman (7.1.1)
|
||||
brakeman (8.0.5)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bundler-audit (0.9.3)
|
||||
bundler (>= 1.2.0)
|
||||
thor (~> 1.0)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
@@ -150,18 +153,18 @@ GEM
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
chunky_png (1.4.0)
|
||||
concurrent-ruby (1.3.5)
|
||||
concurrent-ruby (1.3.7)
|
||||
connection_pool (2.5.5)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
date (3.5.0)
|
||||
crass (1.0.7)
|
||||
date (3.5.1)
|
||||
debug (1.11.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
drb (2.2.3)
|
||||
erb (6.0.0)
|
||||
erb (6.0.4)
|
||||
erubi (1.13.1)
|
||||
faker (3.5.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
@@ -188,8 +191,8 @@ GEM
|
||||
jbuilder (2.14.1)
|
||||
actionview (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
json (2.13.2)
|
||||
jwt (3.1.2)
|
||||
json (2.20.0)
|
||||
jwt (3.2.0)
|
||||
base64
|
||||
kredis (1.8.0)
|
||||
activemodel (>= 6.0.0)
|
||||
@@ -221,7 +224,7 @@ GEM
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
net-http-persistent (4.0.6)
|
||||
connection_pool (~> 2.2, >= 2.2.4)
|
||||
net-imap (0.5.12)
|
||||
net-imap (0.6.4.1)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -231,13 +234,13 @@ GEM
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.18.10-aarch64-linux-gnu)
|
||||
nokogiri (1.19.4-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-arm64-darwin)
|
||||
nokogiri (1.19.4-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-darwin)
|
||||
nokogiri (1.19.4-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-linux-gnu)
|
||||
nokogiri (1.19.4-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
openssl (3.3.0)
|
||||
ostruct (0.6.3)
|
||||
@@ -256,15 +259,15 @@ GEM
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
puma (6.6.1)
|
||||
puma (7.2.1)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.4)
|
||||
rack-protection (4.1.1)
|
||||
rack (3.2.6)
|
||||
rack-protection (4.2.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-session (2.1.1)
|
||||
rack-session (2.1.2)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
@@ -305,12 +308,12 @@ GEM
|
||||
resque-pool (0.7.1)
|
||||
rake (>= 10.0, < 14.0)
|
||||
resque (>= 1.22, < 3)
|
||||
rexml (3.4.1)
|
||||
rqrcode (3.1.0)
|
||||
rexml (3.4.4)
|
||||
rqrcode (3.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.0)
|
||||
rubocop (1.80.0)
|
||||
rqrcode_core (2.1.0)
|
||||
rubocop (1.80.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -357,11 +360,11 @@ GEM
|
||||
sentry-ruby (5.26.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sinatra (4.1.1)
|
||||
sinatra (4.2.1)
|
||||
logger (>= 1.6.0)
|
||||
mustermann (~> 3.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-protection (= 4.1.1)
|
||||
rack-protection (= 4.2.1)
|
||||
rack-session (>= 2.0.0, < 3)
|
||||
tilt (~> 2.0)
|
||||
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-linux)
|
||||
tilt (2.6.1)
|
||||
timeout (0.4.4)
|
||||
timeout (0.6.1)
|
||||
tsort (0.2.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (3.1.5)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
web-push (3.0.2)
|
||||
@@ -412,6 +415,7 @@ DEPENDENCIES
|
||||
bcrypt
|
||||
benchmark
|
||||
brakeman
|
||||
bundler-audit
|
||||
capybara
|
||||
debug
|
||||
faker
|
||||
@@ -425,7 +429,7 @@ DEPENDENCIES
|
||||
ostruct
|
||||
platform_agent
|
||||
propshaft!
|
||||
puma (~> 6.6)
|
||||
puma (~> 7.2, >= 7.2.1)
|
||||
rails!
|
||||
rails_autolink
|
||||
redis (~> 5.4)
|
||||
@@ -444,4 +448,4 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
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],
|
||||
&[type=submit]:disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
@@ -3,7 +3,9 @@ class AccountsController < ApplicationController
|
||||
before_action :set_account
|
||||
|
||||
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
|
||||
|
||||
def update
|
||||
|
||||
@@ -67,17 +67,27 @@ module Authentication
|
||||
authenticated_as session
|
||||
end
|
||||
|
||||
def terminate_current_session
|
||||
Current.session&.destroy!
|
||||
reset_session
|
||||
remove_authentication_cookie
|
||||
end
|
||||
|
||||
def authenticated_as(session)
|
||||
Current.user = session.user
|
||||
Current.session = session
|
||||
set_authenticated_by(:session)
|
||||
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
|
||||
set_authentication_cookie(session)
|
||||
end
|
||||
|
||||
def post_authenticating_url
|
||||
session.delete(:return_to_after_authenticating) || root_url
|
||||
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)
|
||||
end
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ class FirstRunsController < ApplicationController
|
||||
user = FirstRun.create!(user_params)
|
||||
start_new_session_for user
|
||||
|
||||
redirect_to root_url
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
redirect_to root_url
|
||||
end
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
class Rooms::InvolvementsController < ApplicationController
|
||||
include RoomScoped
|
||||
|
||||
before_action :ensure_can_administer, only: %i[ update ]
|
||||
|
||||
def show
|
||||
@involvement = @membership.involvement
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ class SessionsController < ApplicationController
|
||||
|
||||
def destroy
|
||||
remove_push_subscription
|
||||
reset_authentication
|
||||
terminate_current_session
|
||||
redirect_to root_url
|
||||
end
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class Users::SidebarsController < ApplicationController
|
||||
|
||||
def find_direct_placeholder_users
|
||||
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
|
||||
|
||||
def user_ids_already_in_direct_rooms_with_current_user
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :user, :request
|
||||
attribute :session, :user, :request
|
||||
|
||||
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
|
||||
Account.first
|
||||
end
|
||||
|
||||
@@ -100,7 +100,13 @@
|
||||
|
||||
<menu class="flex flex-column gap margin-none pad">
|
||||
<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? %>
|
||||
</turbo-frame>
|
||||
</menu>
|
||||
|
||||
@@ -9,23 +9,26 @@
|
||||
|
||||
<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? %>
|
||||
<%= 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) %>">
|
||||
<span class="for-screen-reader">Role: <%= user.administrator? ? "Administrator" : "Member" %></span>
|
||||
<%= 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>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= button_to account_user_path(user), method: :delete, class: "btn txt-small flex-item-no-shrink btn--negative", data: {
|
||||
turbo_confirm: "Are you sure you want to permanently remove this person from the account? This can’t be undone."} do %>
|
||||
<%= image_tag "minus.svg", size: 20, aria: { hidden: "true" } %>
|
||||
<span class="for-screen-reader">Delete <%= user.name %></span>
|
||||
<% unless user == Current.user %>
|
||||
<%= button_to account_user_path(user), method: :delete, class: "btn txt-small flex-item-no-shrink btn--negative", data: {
|
||||
turbo_confirm: "Are you sure you want to permanently remove this person from the account? This can’t be undone."} do %>
|
||||
<%= image_tag "minus.svg", size: 20, aria: { hidden: "true" } %>
|
||||
<span class="for-screen-reader">Delete <%= user.name %></span>
|
||||
<% 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 %>
|
||||
<%= image_tag "pencil.svg", size: 20, aria: { hidden: "true" } %>
|
||||
<span class="for-screen-reader">My settings</span>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<%= button_to user_ban_path(user), method: :delete,
|
||||
class: "btn btn--negative full-width",
|
||||
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>
|
||||
<% 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
|
||||
|
||||
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)
|
||||
puts "\n\e[1;36m#{message}\e[0m"
|
||||
yield
|
||||
|
||||
@@ -60,7 +60,10 @@ echo
|
||||
if command -v brew &>/dev/null; then
|
||||
step "Installing packages" brew install sqlite ffmpeg mise
|
||||
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
|
||||
step "Installing packages" sudo apt-get install --no-install-recommends -y libsqlite3-0 ffmpeg
|
||||
fi
|
||||
@@ -100,6 +103,20 @@ if ! redis_running; then
|
||||
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 "Restarting services" rails restart
|
||||
|
||||
@@ -4,6 +4,8 @@ CI.run do
|
||||
step "Setup", "bin/setup --skip-server"
|
||||
|
||||
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: 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 "@rails/actioncable", to: "actioncable.esm.js"
|
||||
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 "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.
|
||||
|
||||
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|
|
||||
t.datetime "created_at", null: false
|
||||
t.text "custom_styles"
|
||||
t.string "join_code", null: false
|
||||
t.string "name", null: false
|
||||
t.json "settings"
|
||||
t.integer "singleton_guard", default: 0, null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["singleton_guard"], name: "index_accounts_on_singleton_guard", unique: true
|
||||
end
|
||||
|
||||
create_table "action_text_rich_texts", force: :cascade do |t|
|
||||
|
||||
@@ -12,7 +12,7 @@ ActiveSupport.on_load(:action_text_content) do
|
||||
|
||||
def attachable_from_possibly_expired_sgid(sgid)
|
||||
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")
|
||||
data
|
||||
@@ -20,7 +20,7 @@ ActiveSupport.on_load(:action_text_content) do
|
||||
# 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
|
||||
Base64.urlsafe_decode64(data).match(%r{(gid://campfire/[^/]+/\d+)})&.to_s
|
||||
decode_base64(data).match(%r{(gid://campfire/[^/]+/\d+)})&.to_s
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -32,6 +32,12 @@ ActiveSupport.on_load(:action_text_content) do
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
nil
|
||||
end
|
||||
|
||||
def decode_base64(message)
|
||||
Base64.strict_decode64(message)
|
||||
rescue => _e
|
||||
Base64.urlsafe_decode64(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -16,7 +16,7 @@ module RestrictedHTTP
|
||||
|
||||
def private_ip?(ip)
|
||||
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
|
||||
rescue IPAddr::InvalidAddressError
|
||||
true
|
||||
|
||||
@@ -10,6 +10,38 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :ok
|
||||
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
|
||||
assert users(:david).administrator?
|
||||
|
||||
|
||||
@@ -30,4 +30,28 @@ class FirstRunsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert parsed_cookies.signed[:session_token]
|
||||
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
|
||||
@@ -43,4 +43,13 @@ class Rooms::InvolvementsControllerTest < ActionDispatch::IntegrationTest
|
||||
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
|
||||
@@ -28,7 +28,9 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
|
||||
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 parsed_cookies.signed[:session_token]
|
||||
@@ -43,11 +45,15 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
test "destroy" do
|
||||
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_not cookies[:session_token].present?
|
||||
assert_nil Session.find_by(id: session.id)
|
||||
end
|
||||
|
||||
test "destroy removes the push subscription for the device" do
|
||||
|
||||
@@ -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]
|
||||
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
|
||||
large_file = Opengraph::Location.new("https://www.example.com/100gb.zip")
|
||||
|
||||
|
||||
Vendored
+4
-3
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Trix 2.0.10
|
||||
Copyright © 2024 37signals, LLC
|
||||
Trix 2.1.19
|
||||
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
|
||||
Reference in new issue
Block a user