Compare commits

...

4 commits

Author SHA1 Message Date
Victor Dyotte 96181f23c9
Merge c8fb4c17f1 into 2a437685fc 2024-10-17 17:23:06 +08:00
Sqx. Flann van der Eik 2a437685fc
[docs/bugfix] Fix access to /dev and /tmp in AppArmor profile (#3444) 2024-10-16 14:34:08 +02:00
Markus Unterwaditzer a48cce82b9
[chore] Upgrade golangci-lint, ignore existing int overflow warnings (#3420)
* [chore] Bump tooling versions, bump go -> v1.23.0

* undo silly change

* sign

* bump go version in go.mod

* allow overflow in imaging

* goreleaser deprecation notices

* [chore] Upgrade golangci-lint, ignore existing int overflow warnings

There is a new lint for unchecked int casts. Integer overflows are bad,
but the old code that triggers this lint seems to be perfectly fine.
Instead of disabling the lint entirely for new code as well, grandfather
in existing code.

* fix golangci-lint documentation link

* revert unrelated changes

* revert another unrelated change

* get rid of remaining nolint:gosec

* swagger updates

* apply review feedback

* fix wrong formatting specifier thing

* fix the linter for real

---------

Co-authored-by: tobi <tobi.smethurst@protonmail.com>
2024-10-16 14:13:58 +02:00
vdyotte c8fb4c17f1
Feat: Add global instance CSS customization setting
Allow instance admins to add custom CSS that will affect
every page of their instance.

This is done with a new CustomCSS instance setting that
works pretty much exactly like the Users CustomCSS property.
This custom CSS is then requested for every page load.
User styles/themes take precedence over this CSS.
2024-09-25 00:26:56 -04:00
46 changed files with 249 additions and 105 deletions

View file

@ -385,7 +385,7 @@ We use [golangci-lint](https://golangci-lint.run/) for linting, which allows us
If you make a PR that doesn't pass the linter, it will be rejected. As such, it's good practice to run the linter locally before pushing or opening a PR. If you make a PR that doesn't pass the linter, it will be rejected. As such, it's good practice to run the linter locally before pushing or opening a PR.
To do this, first install the linter following the instructions [here](https://golangci-lint.run/usage/install/#local-installation). To do this, first install the linter following the instructions [here](https://golangci-lint.run/welcome/install/).
Then, you can run the linter with: Then, you can run the linter with:

View file

@ -167,3 +167,11 @@ Links to the set contact account and/or email address will appear on the footer
The selected **contact user** must be an active (not suspended) admin and/or moderator on the instance. The selected **contact user** must be an active (not suspended) admin and/or moderator on the instance.
If you're on a single-user instance and you give admin privileges to your main account, you can just fill in your own username here; you don't need to make a separate admin account just for this. If you're on a single-user instance and you give admin privileges to your main account, you can just fill in your own username here; you don't need to make a separate admin account just for this.
### Instance Custom CSS
custom CSS allows you to further customize the way your instance looks when visited through a browser.
This custom CSS will be applied to all pages of your instance. Users themes and CSS still take precedence over this customization.
See the [Custom CSS](./custom_css.md) page for some tips on writing custom CSS for your instance.

View file

@ -1550,6 +1550,10 @@ definitions:
$ref: '#/definitions/instanceV1Configuration' $ref: '#/definitions/instanceV1Configuration'
contact_account: contact_account:
$ref: '#/definitions/account' $ref: '#/definitions/account'
custom_css:
description: Custom CSS for the instance.
type: string
x-go-name: CustomCSS
debug: debug:
description: Whether or not instance is running in DEBUG mode. Omitted if false. description: Whether or not instance is running in DEBUG mode. Omitted if false.
type: boolean type: boolean
@ -1730,6 +1734,10 @@ definitions:
$ref: '#/definitions/instanceV2Configuration' $ref: '#/definitions/instanceV2Configuration'
contact: contact:
$ref: '#/definitions/instanceV2Contact' $ref: '#/definitions/instanceV2Contact'
custom_css:
description: Instance Custom Css
type: string
x-go-name: CustomCSS
debug: debug:
description: Whether or not instance is running in DEBUG mode. Omitted if false. description: Whether or not instance is running in DEBUG mode. Omitted if false.
type: boolean type: boolean
@ -2114,7 +2122,7 @@ definitions:
bitrate: bitrate:
description: Bitrate of the media in bits per second. description: Bitrate of the media in bits per second.
example: 1000000 example: 1000000
format: int64 format: uint64
type: integer type: integer
x-go-name: Bitrate x-go-name: Bitrate
duration: duration:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 229 KiB

View file

@ -24,12 +24,12 @@ profile gotosocial flags=(attach_disconnected, mediate_deleted) {
# Embedded ffmpeg needs read # Embedded ffmpeg needs read
# permission on /dev/urandom. # permission on /dev/urandom.
owner /dev/ r, /dev/ r,
owner /dev/urandom r, /dev/urandom r,
# Temp dir access is needed for storing # Temp dir access is needed for storing
# files briefly during media processing. # files briefly during media processing.
owner /tmp/ r, /tmp/ r,
owner /tmp/* rwk, owner /tmp/* rwk,
# If running with GTS_WAZERO_COMPILATION_CACHE set, # If running with GTS_WAZERO_COMPILATION_CACHE set,
@ -39,7 +39,7 @@ profile gotosocial flags=(attach_disconnected, mediate_deleted) {
# If you've enabled logging to syslog, allow GoToSocial # If you've enabled logging to syslog, allow GoToSocial
# to write logs by uncommenting the following line: # to write logs by uncommenting the following line:
# owner /var/log/syslog w, # /var/log/syslog w,
# These directories are not currently used by any of # These directories are not currently used by any of
# the recommended GoToSocial installation methods, but # the recommended GoToSocial installation methods, but
@ -65,6 +65,7 @@ profile gotosocial flags=(attach_disconnected, mediate_deleted) {
/etc/services r, /etc/services r,
/proc/sys/net/core/somaxconn r, /proc/sys/net/core/somaxconn r,
/sys/fs/cgroup/system.slice/gotosocial.service/{,*} r, /sys/fs/cgroup/system.slice/gotosocial.service/{,*} r,
/sys/kernel/mm/hugepages/ r,
/sys/kernel/mm/transparent_hugepage/hpage_pmd_size r, /sys/kernel/mm/transparent_hugepage/hpage_pmd_size r,
owner /proc/*/cgroup r, owner /proc/*/cgroup r,
owner /proc/*/cpuset r, owner /proc/*/cpuset r,

View file

@ -145,8 +145,8 @@ func validateCreateEmoji(form *apimodel.EmojiCreateRequest) error {
return errors.New("no emoji given") return errors.New("no emoji given")
} }
maxSize := config.GetMediaEmojiLocalMaxSize() maxSize := int64(config.GetMediaEmojiLocalMaxSize()) // #nosec G115 -- Already validated.
if form.Image.Size > int64(maxSize) { if form.Image.Size > maxSize {
return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024) return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024)
} }

View file

@ -208,8 +208,8 @@ func validateUpdateEmoji(form *apimodel.EmojiUpdateRequest) error {
} }
if hasImage { if hasImage {
maxSize := config.GetMediaEmojiLocalMaxSize() maxSize := int64(config.GetMediaEmojiLocalMaxSize()) // #nosec G115 -- Already validated.
if form.Image.Size > int64(maxSize) { if form.Image.Size > maxSize {
return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024) return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024)
} }
} }

View file

@ -175,6 +175,7 @@ func validateInstanceUpdate(form *apimodel.InstanceSettingsUpdateRequest) error
form.ContactEmail == nil && form.ContactEmail == nil &&
form.ShortDescription == nil && form.ShortDescription == nil &&
form.Description == nil && form.Description == nil &&
form.CustomCSS == nil &&
form.Terms == nil && form.Terms == nil &&
form.Avatar == nil && form.Avatar == nil &&
form.AvatarDescription == nil && form.AvatarDescription == nil &&

View file

@ -160,7 +160,7 @@ type MediaDimensions struct {
Duration float32 `json:"duration,omitempty"` Duration float32 `json:"duration,omitempty"`
// Bitrate of the media in bits per second. // Bitrate of the media in bits per second.
// example: 1000000 // example: 1000000
Bitrate int `json:"bitrate,omitempty"` Bitrate uint64 `json:"bitrate,omitempty"`
// Size of the media, in the format `[width]x[height]`. // Size of the media, in the format `[width]x[height]`.
// Not set for audio. // Not set for audio.
// example: 1920x1080 // example: 1920x1080

View file

@ -33,6 +33,8 @@ type InstanceSettingsUpdateRequest struct {
ShortDescription *string `form:"short_description" json:"short_description" xml:"short_description"` ShortDescription *string `form:"short_description" json:"short_description" xml:"short_description"`
// Longer description of the instance, max 5,000 chars. HTML formatting accepted. // Longer description of the instance, max 5,000 chars. HTML formatting accepted.
Description *string `form:"description" json:"description" xml:"description"` Description *string `form:"description" json:"description" xml:"description"`
// Custom CSS for the instance.
CustomCSS *string `form:"custom_css" json:"custom_css,omitempty" xml:"custom_css"`
// Terms and conditions of the instance, max 5,000 chars. HTML formatting accepted. // Terms and conditions of the instance, max 5,000 chars. HTML formatting accepted.
Terms *string `form:"terms" json:"terms" xml:"terms"` Terms *string `form:"terms" json:"terms" xml:"terms"`
// Image to use as the instance thumbnail. // Image to use as the instance thumbnail.

View file

@ -38,6 +38,8 @@ type InstanceV1 struct {
// //
// This should be displayed on the 'about' page for an instance. // This should be displayed on the 'about' page for an instance.
Description string `json:"description"` Description string `json:"description"`
// Custom CSS for the instance.
CustomCSS string `json:"custom_css,omitempty"`
// Raw (unparsed) version of description. // Raw (unparsed) version of description.
DescriptionText string `json:"description_text,omitempty"` DescriptionText string `json:"description_text,omitempty"`
// A shorter description of the instance. // A shorter description of the instance.

View file

@ -53,6 +53,8 @@ type InstanceV2 struct {
Description string `json:"description"` Description string `json:"description"`
// Raw (unparsed) version of description. // Raw (unparsed) version of description.
DescriptionText string `json:"description_text,omitempty"` DescriptionText string `json:"description_text,omitempty"`
// Instance Custom Css
CustomCSS string `json:"custom_css,omitempty"`
// Basic anonymous usage data for this instance. // Basic anonymous usage data for this instance.
Usage InstanceV2Usage `json:"usage"` Usage InstanceV2Usage `json:"usage"`
// An image used to represent this instance. // An image used to represent this instance.

View file

@ -220,7 +220,7 @@ func (n *node) getChild(part string) *node {
for i < j { for i < j {
// avoid overflow when computing h // avoid overflow when computing h
h := int(uint(i+j) >> 1) h := int(uint(i+j) >> 1) // #nosec G115
// i ≤ h < j // i ≤ h < j
if n.child[h].part < part { if n.child[h].part < part {

View file

@ -25,6 +25,7 @@
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"math"
"net/url" "net/url"
"os" "os"
"runtime" "runtime"
@ -489,7 +490,10 @@ func deriveBunDBPGOptions() (*pgx.ConnConfig, error) {
cfg.Host = address cfg.Host = address
} }
if port := config.GetDbPort(); port > 0 { if port := config.GetDbPort(); port > 0 {
cfg.Port = uint16(port) if port > math.MaxUint16 {
return nil, errors.New("invalid port, must be in range 1-65535")
}
cfg.Port = uint16(port) // #nosec G115 -- Just validated above.
} }
if u := config.GetDbUser(); u != "" { if u := config.GetDbUser(); u != "" {
cfg.User = u cfg.User = u

View file

@ -0,0 +1,44 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package migrations
import (
"context"
"strings"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? TEXT", bun.Ident("instances"), bun.Ident("custom_css"))
if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
return err
}
return nil
}
down := func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "ALTER TABLE ? DROP COLUMN ?", bun.Ident("instances"), bun.Ident("custom_css"))
return err
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -97,11 +97,11 @@ func() (*media.ProcessingEmoji, error) {
} }
// Get maximum supported remote emoji size. // Get maximum supported remote emoji size.
maxsz := config.GetMediaEmojiRemoteMaxSize() maxsz := int64(config.GetMediaEmojiRemoteMaxSize()) // #nosec G115 -- Already validated.
// Prepare data function to dereference remote emoji media. // Prepare data function to dereference remote emoji media.
data := func(context.Context) (io.ReadCloser, error) { data := func(context.Context) (io.ReadCloser, error) {
return tsport.DereferenceMedia(ctx, url, int64(maxsz)) return tsport.DereferenceMedia(ctx, url, maxsz)
} }
// Create new emoji with prepared info. // Create new emoji with prepared info.
@ -189,11 +189,11 @@ func() (*media.ProcessingEmoji, error) {
} }
// Get maximum supported remote emoji size. // Get maximum supported remote emoji size.
maxsz := config.GetMediaEmojiRemoteMaxSize() maxsz := int64(config.GetMediaEmojiRemoteMaxSize()) // #nosec G115 -- Already validated.
// Prepare data function to dereference remote emoji media. // Prepare data function to dereference remote emoji media.
data := func(context.Context) (io.ReadCloser, error) { data := func(context.Context) (io.ReadCloser, error) {
return tsport.DereferenceMedia(ctx, url, int64(maxsz)) return tsport.DereferenceMedia(ctx, url, maxsz)
} }
// Update emoji with prepared info. // Update emoji with prepared info.
@ -255,11 +255,11 @@ func() (*media.ProcessingEmoji, error) {
} }
// Get maximum supported remote emoji size. // Get maximum supported remote emoji size.
maxsz := config.GetMediaEmojiRemoteMaxSize() maxsz := int64(config.GetMediaEmojiRemoteMaxSize()) // #nosec G115 -- Already validated.
// Prepare data function to dereference remote emoji media. // Prepare data function to dereference remote emoji media.
data := func(context.Context) (io.ReadCloser, error) { data := func(context.Context) (io.ReadCloser, error) {
return tsport.DereferenceMedia(ctx, url, int64(maxsz)) return tsport.DereferenceMedia(ctx, url, maxsz)
} }
// Recache emoji with prepared info. // Recache emoji with prepared info.

View file

@ -77,14 +77,14 @@ func() (*media.ProcessingMedia, error) {
} }
// Get maximum supported remote media size. // Get maximum supported remote media size.
maxsz := config.GetMediaRemoteMaxSize() maxsz := int64(config.GetMediaRemoteMaxSize()) // #nosec G115 -- Already validated.
// Create media with prepared info. // Create media with prepared info.
return d.mediaManager.CreateMedia( return d.mediaManager.CreateMedia(
ctx, ctx,
accountID, accountID,
func(ctx context.Context) (io.ReadCloser, error) { func(ctx context.Context) (io.ReadCloser, error) {
return tsport.DereferenceMedia(ctx, url, int64(maxsz)) return tsport.DereferenceMedia(ctx, url, maxsz)
}, },
info, info,
) )
@ -168,14 +168,14 @@ func() (*media.ProcessingMedia, error) {
} }
// Get maximum supported remote media size. // Get maximum supported remote media size.
maxsz := config.GetMediaRemoteMaxSize() maxsz := int64(config.GetMediaRemoteMaxSize()) // #nosec G115 -- Already validated.
// Recache media with prepared info, // Recache media with prepared info,
// this will also update media in db. // this will also update media in db.
return d.mediaManager.CacheMedia( return d.mediaManager.CacheMedia(
attach, attach,
func(ctx context.Context) (io.ReadCloser, error) { func(ctx context.Context) (io.ReadCloser, error) {
return tsport.DereferenceMedia(ctx, url, int64(maxsz)) return tsport.DereferenceMedia(ctx, url, maxsz)
}, },
), nil ), nil
}, },

View file

@ -34,6 +34,7 @@ type Instance struct {
ShortDescriptionText string `bun:""` // Raw text version of short description (before parsing). ShortDescriptionText string `bun:""` // Raw text version of short description (before parsing).
Description string `bun:""` // Longer description of this instance. Description string `bun:""` // Longer description of this instance.
DescriptionText string `bun:""` // Raw text version of long description (before parsing). DescriptionText string `bun:""` // Raw text version of long description (before parsing).
CustomCSS string `bun:",nullzero"` // Custom CSS for the instance.
Terms string `bun:""` // Terms and conditions of this instance. Terms string `bun:""` // Terms and conditions of this instance.
TermsText string `bun:""` // Raw text version of terms (before parsing). TermsText string `bun:""` // Raw text version of terms (before parsing).
ContactEmail string `bun:""` // Contact email address for this instance ContactEmail string `bun:""` // Contact email address for this instance

View file

@ -340,14 +340,14 @@ func (c *Client) do(r *Request) (rsp *http.Response, retry bool, err error) {
if u, _ := strconv.ParseUint(after, 10, 32); u != 0 { if u, _ := strconv.ParseUint(after, 10, 32); u != 0 {
// An integer no. of backoff seconds was provided. // An integer no. of backoff seconds was provided.
r.backoff = time.Duration(u) * time.Second r.backoff = time.Duration(u) * time.Second // #nosec G115 -- We clamp backoff below.
} else if at, _ := http.ParseTime(after); !at.Before(now) { } else if at, _ := http.ParseTime(after); !at.Before(now) {
// An HTTP formatted future date-time was provided. // An HTTP formatted future date-time was provided.
r.backoff = at.Sub(now) r.backoff = at.Sub(now)
} }
// Don't let their provided backoff exceed our max. // Don't let their provided backoff exceed our max.
if max := baseBackoff * time.Duration(c.retries); // if max := baseBackoff * time.Duration(c.retries); // #nosec G115 -- We control c.retries.
r.backoff > max { r.backoff > max {
r.backoff = max r.backoff = max
} }

View file

@ -556,10 +556,10 @@ func (res *ffprobeResult) Process() (*result, error) {
if p := strings.SplitN(str, "/", 2); len(p) == 2 { if p := strings.SplitN(str, "/", 2); len(p) == 2 {
n, _ := strconv.ParseUint(p[0], 10, 32) n, _ := strconv.ParseUint(p[0], 10, 32)
d, _ := strconv.ParseUint(p[1], 10, 32) d, _ := strconv.ParseUint(p[1], 10, 32)
num, den = uint32(n), uint32(d) num, den = uint32(n), uint32(d) // #nosec G115 -- ParseUint is configured to check
} else { } else {
n, _ := strconv.ParseUint(p[0], 10, 32) n, _ := strconv.ParseUint(p[0], 10, 32)
num = uint32(n) num = uint32(n) // #nosec G115 -- ParseUint is configured to check
} }
// Set final divised framerate. // Set final divised framerate.

View file

@ -399,9 +399,9 @@ func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) {
g16 := uint16(s[1]) g16 := uint16(s[1])
b16 := uint16(s[2]) b16 := uint16(s[2])
a16 := uint16(a) a16 := uint16(a)
d[0] = uint8(r16 * 0xff / a16) d[0] = uint8(r16 * 0xff / a16) // #nosec G115 -- Overflow desired.
d[1] = uint8(g16 * 0xff / a16) d[1] = uint8(g16 * 0xff / a16) // #nosec G115 -- Overflow desired.
d[2] = uint8(b16 * 0xff / a16) d[2] = uint8(b16 * 0xff / a16) // #nosec G115 -- Overflow desired.
d[3] = a d[3] = a
} }
j += 4 j += 4
@ -431,9 +431,9 @@ func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) {
g32 := uint32(s[2])<<8 | uint32(s[3]) g32 := uint32(s[2])<<8 | uint32(s[3])
b32 := uint32(s[4])<<8 | uint32(s[5]) b32 := uint32(s[4])<<8 | uint32(s[5])
a32 := uint32(s[6])<<8 | uint32(s[7]) a32 := uint32(s[6])<<8 | uint32(s[7])
d[0] = uint8((r32 * 0xffff / a32) >> 8) d[0] = uint8((r32 * 0xffff / a32) >> 8) // #nosec G115 -- Overflow desired.
d[1] = uint8((g32 * 0xffff / a32) >> 8) d[1] = uint8((g32 * 0xffff / a32) >> 8) // #nosec G115 -- Overflow desired.
d[2] = uint8((b32 * 0xffff / a32) >> 8) d[2] = uint8((b32 * 0xffff / a32) >> 8) // #nosec G115 -- Overflow desired.
} }
d[3] = a d[3] = a
j += 4 j += 4
@ -509,30 +509,30 @@ func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) {
cr1 := int32(img.Cr[ic]) - 128 cr1 := int32(img.Cr[ic]) - 128
r := yy1 + 91881*cr1 r := yy1 + 91881*cr1
if uint32(r)&0xff000000 == 0 { if uint32(r)&0xff000000 == 0 { //nolint:gosec
r >>= 16 r >>= 16
} else { } else {
r = ^(r >> 31) r = ^(r >> 31)
} }
g := yy1 - 22554*cb1 - 46802*cr1 g := yy1 - 22554*cb1 - 46802*cr1
if uint32(g)&0xff000000 == 0 { if uint32(g)&0xff000000 == 0 { //nolint:gosec
g >>= 16 g >>= 16
} else { } else {
g = ^(g >> 31) g = ^(g >> 31)
} }
b := yy1 + 116130*cb1 b := yy1 + 116130*cb1
if uint32(b)&0xff000000 == 0 { if uint32(b)&0xff000000 == 0 { //nolint:gosec
b >>= 16 b >>= 16
} else { } else {
b = ^(b >> 31) b = ^(b >> 31)
} }
d := dst[j : j+4 : j+4] d := dst[j : j+4 : j+4]
d[0] = uint8(r) d[0] = uint8(r) // #nosec G115 -- Overflow desired.
d[1] = uint8(g) d[1] = uint8(g) // #nosec G115 -- Overflow desired.
d[2] = uint8(b) d[2] = uint8(b) // #nosec G115 -- Overflow desired.
d[3] = 0xff d[3] = 0xff
iy++ iy++
@ -569,9 +569,9 @@ func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) {
d := dst[j : j+4 : j+4] d := dst[j : j+4 : j+4]
switch a16 { switch a16 {
case 0xffff: case 0xffff:
d[0] = uint8(r16 >> 8) d[0] = uint8(r16 >> 8) // #nosec G115 -- Overflow desired.
d[1] = uint8(g16 >> 8) d[1] = uint8(g16 >> 8) // #nosec G115 -- Overflow desired.
d[2] = uint8(b16 >> 8) d[2] = uint8(b16 >> 8) // #nosec G115 -- Overflow desired.
d[3] = 0xff d[3] = 0xff
case 0: case 0:
d[0] = 0 d[0] = 0
@ -579,10 +579,10 @@ func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) {
d[2] = 0 d[2] = 0
d[3] = 0 d[3] = 0
default: default:
d[0] = uint8(((r16 * 0xffff) / a16) >> 8) d[0] = uint8(((r16 * 0xffff) / a16) >> 8) // #nosec G115 -- Overflow desired.
d[1] = uint8(((g16 * 0xffff) / a16) >> 8) d[1] = uint8(((g16 * 0xffff) / a16) >> 8) // #nosec G115 -- Overflow desired.
d[2] = uint8(((b16 * 0xffff) / a16) >> 8) d[2] = uint8(((b16 * 0xffff) / a16) >> 8) // #nosec G115 -- Overflow desired.
d[3] = uint8(a16 >> 8) d[3] = uint8(a16 >> 8) // #nosec G115 -- Overflow desired.
} }
j += 4 j += 4
} }
@ -617,7 +617,7 @@ func clampFloat(x float64) uint8 {
return 255 return 255
} }
if v > 0 { if v > 0 {
return uint8(v) return uint8(v) // #nosec G115 -- Just checked.
} }
return 0 return 0
} }

View file

@ -49,9 +49,6 @@ func (m *Manager) RefetchEmojis(ctx context.Context, domain string, dereferenceM
refetchIDs []string refetchIDs []string
) )
// Get max supported remote emoji media size.
maxsz := config.GetMediaEmojiRemoteMaxSize()
// page through emojis 20 at a time, looking for those with missing images // page through emojis 20 at a time, looking for those with missing images
for { for {
// Fetch next block of emojis from database // Fetch next block of emojis from database
@ -111,8 +108,10 @@ func (m *Manager) RefetchEmojis(ctx context.Context, domain string, dereferenceM
continue continue
} }
// Get max supported remote emoji media size.
maxsz := int64(config.GetMediaEmojiRemoteMaxSize()) // #nosec G115 -- Already validated.
dataFunc := func(ctx context.Context) (reader io.ReadCloser, err error) { dataFunc := func(ctx context.Context) (reader io.ReadCloser, err error) {
return dereferenceMedia(ctx, emojiImageIRI, int64(maxsz)) return dereferenceMedia(ctx, emojiImageIRI, maxsz)
} }
processingEmoji, err := m.UpdateEmoji(ctx, emoji, dataFunc, AdditionalEmojiInfo{ processingEmoji, err := m.UpdateEmoji(ctx, emoji, dataFunc, AdditionalEmojiInfo{

View file

@ -145,7 +145,7 @@ func drainToTmp(rc io.ReadCloser) (string, error) {
// Check to see if limit was reached, // Check to see if limit was reached,
// (produces more useful error messages). // (produces more useful error messages).
if lr != nil && lr.N <= 0 { if lr != nil && lr.N <= 0 {
err := fmt.Errorf("reached read limit %s", bytesize.Size(limit)) err := fmt.Errorf("reached read limit %s", bytesize.Size(limit)) // #nosec G115 -- Just logging
return path, gtserror.SetLimitReached(err) return path, gtserror.SetLimitReached(err)
} }

View file

@ -123,7 +123,7 @@ func Logger(logClientIP bool) gin.HandlerFunc {
} }
// Generate a nicer looking bytecount // Generate a nicer looking bytecount
size := bytesize.Size(c.Writer.Size()) size := bytesize.Size(c.Writer.Size()) // #nosec G115 -- Just logging
// Finally, write log entry with status text + body size. // Finally, write log entry with status text + body size.
l.Logf(lvl, "%s: wrote %s", statusText, size) l.Logf(lvl, "%s: wrote %s", statusText, size)

View file

@ -48,7 +48,7 @@ func NewRequestID() string {
b := make([]byte, 12) b := make([]byte, 12)
// Get current time in milliseconds. // Get current time in milliseconds.
ms := uint64(time.Now().UnixMilli()) ms := uint64(time.Now().UnixMilli()) // #nosec G115 -- Pre-1970 clock?
// Store binary time data in byte buffer. // Store binary time data in byte buffer.
binary.LittleEndian.PutUint64(b[0:8], ms) binary.LittleEndian.PutUint64(b[0:8], ms)

View file

@ -82,12 +82,16 @@ func Throttle(cpuMultiplier int, retryAfter time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {} return func(c *gin.Context) {}
} }
if retryAfter < 0 {
retryAfter = 0
}
var ( var (
limit = runtime.GOMAXPROCS(0) * cpuMultiplier limit = runtime.GOMAXPROCS(0) * cpuMultiplier
queueLimit = limit * cpuMultiplier queueLimit = limit * cpuMultiplier
tokens = make(chan token, limit) tokens = make(chan token, limit)
requestCount = atomic.Int64{} requestCount = atomic.Int64{}
retryAfterStr = strconv.FormatUint(uint64(retryAfter/time.Second), 10) retryAfterStr = strconv.FormatUint(uint64(retryAfter/time.Second), 10) // #nosec G115 -- Checked right above
) )
// prefill token channel // prefill token channel

View file

@ -463,9 +463,10 @@ func (p *Processor) UpdateAvatar(
) { ) {
// Get maximum supported local media size. // Get maximum supported local media size.
maxsz := config.GetMediaLocalMaxSize() maxsz := config.GetMediaLocalMaxSize()
maxszInt64 := int64(maxsz) // #nosec G115 -- Already validated.
// Ensure media within size bounds. // Ensure media within size bounds.
if avatar.Size > int64(maxsz) { if avatar.Size > maxszInt64 {
text := fmt.Sprintf("media exceeds configured max size: %s", maxsz) text := fmt.Sprintf("media exceeds configured max size: %s", maxsz)
return nil, gtserror.NewErrorBadRequest(errors.New(text), text) return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
} }
@ -478,7 +479,7 @@ func (p *Processor) UpdateAvatar(
} }
// Wrap the multipart file reader to ensure is limited to max. // Wrap the multipart file reader to ensure is limited to max.
rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, int64(maxsz)) rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, maxszInt64)
// Write to instance storage. // Write to instance storage.
return p.c.StoreLocalMedia(ctx, return p.c.StoreLocalMedia(ctx,
@ -508,9 +509,10 @@ func (p *Processor) UpdateHeader(
) { ) {
// Get maximum supported local media size. // Get maximum supported local media size.
maxsz := config.GetMediaLocalMaxSize() maxsz := config.GetMediaLocalMaxSize()
maxszInt64 := int64(maxsz) // #nosec G115 -- Already validated.
// Ensure media within size bounds. // Ensure media within size bounds.
if header.Size > int64(maxsz) { if header.Size > maxszInt64 {
text := fmt.Sprintf("media exceeds configured max size: %s", maxsz) text := fmt.Sprintf("media exceeds configured max size: %s", maxsz)
return nil, gtserror.NewErrorBadRequest(errors.New(text), text) return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
} }
@ -523,7 +525,7 @@ func (p *Processor) UpdateHeader(
} }
// Wrap the multipart file reader to ensure is limited to max. // Wrap the multipart file reader to ensure is limited to max.
rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, int64(maxsz)) rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, maxszInt64)
// Write to instance storage. // Write to instance storage.
return p.c.StoreLocalMedia(ctx, return p.c.StoreLocalMedia(ctx,

View file

@ -25,7 +25,6 @@
"mime/multipart" "mime/multipart"
"strings" "strings"
"codeberg.org/gruf/go-bytesize"
"codeberg.org/gruf/go-iotools" "codeberg.org/gruf/go-iotools"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
@ -46,9 +45,10 @@ func (p *Processor) EmojiCreate(
// Get maximum supported local emoji size. // Get maximum supported local emoji size.
maxsz := config.GetMediaEmojiLocalMaxSize() maxsz := config.GetMediaEmojiLocalMaxSize()
maxszInt64 := int64(maxsz) // #nosec G115 -- Already validated.
// Ensure media within size bounds. // Ensure media within size bounds.
if form.Image.Size > int64(maxsz) { if form.Image.Size > maxszInt64 {
text := fmt.Sprintf("emoji exceeds configured max size: %s", maxsz) text := fmt.Sprintf("emoji exceeds configured max size: %s", maxsz)
return nil, gtserror.NewErrorBadRequest(errors.New(text), text) return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
} }
@ -61,7 +61,7 @@ func (p *Processor) EmojiCreate(
} }
// Wrap the multipart file reader to ensure is limited to max. // Wrap the multipart file reader to ensure is limited to max.
rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, int64(maxsz)) rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, maxszInt64)
data := func(context.Context) (io.ReadCloser, error) { data := func(context.Context) (io.ReadCloser, error) {
return rc, nil return rc, nil
} }
@ -301,9 +301,10 @@ func (p *Processor) emojiUpdateCopy(
// Get maximum supported local emoji size. // Get maximum supported local emoji size.
maxsz := config.GetMediaEmojiLocalMaxSize() maxsz := config.GetMediaEmojiLocalMaxSize()
maxszInt := int(maxsz) // #nosec G115 -- Already validated.
// Ensure target emoji image within size bounds. // Ensure target emoji image within size bounds.
if bytesize.Size(target.ImageFileSize) > maxsz { if target.ImageFileSize > maxszInt {
text := fmt.Sprintf("emoji exceeds configured max size: %s", maxsz) text := fmt.Sprintf("emoji exceeds configured max size: %s", maxsz)
return nil, gtserror.NewErrorBadRequest(errors.New(text), text) return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
} }
@ -442,9 +443,10 @@ func (p *Processor) emojiUpdateModify(
// Get maximum supported local emoji size. // Get maximum supported local emoji size.
maxsz := config.GetMediaEmojiLocalMaxSize() maxsz := config.GetMediaEmojiLocalMaxSize()
maxszInt64 := int64(maxsz) // #nosec G115 -- Already validated.
// Ensure media within size bounds. // Ensure media within size bounds.
if image.Size > int64(maxsz) { if image.Size > maxszInt64 {
text := fmt.Sprintf("emoji exceeds configured max size: %s", maxsz) text := fmt.Sprintf("emoji exceeds configured max size: %s", maxsz)
return nil, gtserror.NewErrorBadRequest(errors.New(text), text) return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
} }
@ -457,7 +459,7 @@ func (p *Processor) emojiUpdateModify(
} }
// Wrap the multipart file reader to ensure is limited to max. // Wrap the multipart file reader to ensure is limited to max.
rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, int64(maxsz)) rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, int64(maxsz)) // #nosec G115 -- Already validated.
data := func(context.Context) (io.ReadCloser, error) { data := func(context.Context) (io.ReadCloser, error) {
return rc, nil return rc, nil
} }

View file

@ -227,6 +227,17 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
columns = append(columns, []string{"description", "description_text"}...) columns = append(columns, []string{"description", "description_text"}...)
} }
// validate & update site custom css if it's set on the form
if form.CustomCSS != nil {
customCSS := *form.CustomCSS
if err := validate.InstanceCustomCSS(customCSS); err != nil {
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
instance.CustomCSS = text.SanitizeToPlaintext(customCSS)
columns = append(columns, []string{"custom_css"}...)
}
// Validate & update site // Validate & update site
// terms if set on the form. // terms if set on the form.
if form.Terms != nil { if form.Terms != nil {

View file

@ -36,9 +36,10 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
// Get maximum supported local media size. // Get maximum supported local media size.
maxsz := config.GetMediaLocalMaxSize() maxsz := config.GetMediaLocalMaxSize()
maxszInt64 := int64(maxsz) // #nosec G115 -- Already validated.
// Ensure media within size bounds. // Ensure media within size bounds.
if form.File.Size > int64(maxsz) { if form.File.Size > maxszInt64 {
text := fmt.Sprintf("media exceeds configured max size: %s", maxsz) text := fmt.Sprintf("media exceeds configured max size: %s", maxsz)
return nil, gtserror.NewErrorBadRequest(errors.New(text), text) return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
} }
@ -58,7 +59,7 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
} }
// Wrap the multipart file reader to ensure is limited to max. // Wrap the multipart file reader to ensure is limited to max.
rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, int64(maxsz)) rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, maxszInt64)
// Create local media and write to instance storage. // Create local media and write to instance storage.
attachment, errWithCode := p.c.StoreLocalMedia(ctx, attachment, errWithCode := p.c.StoreLocalMedia(ctx,

View file

@ -53,7 +53,7 @@ func (t *transport) DereferenceMedia(ctx context.Context, iri *url.URL, maxsz in
// Check media within size limit. // Check media within size limit.
if rsp.ContentLength > maxsz { if rsp.ContentLength > maxsz {
_ = rsp.Body.Close() // close early. _ = rsp.Body.Close() // close early.
sz := bytesize.Size(maxsz) // nicer log format sz := bytesize.Size(maxsz) //nolint:gosec
return nil, gtserror.Newf("media body exceeds max size %s", sz) return nil, gtserror.Newf("media body exceeds max size %s", sz)
} }

View file

@ -647,7 +647,7 @@ func (c *Converter) AttachmentToAPIAttachment(ctx context.Context, media *gtsmod
Size: toAPISize(media.FileMeta.Original.Width, media.FileMeta.Original.Height), Size: toAPISize(media.FileMeta.Original.Width, media.FileMeta.Original.Height),
FrameRate: toAPIFrameRate(media.FileMeta.Original.Framerate), FrameRate: toAPIFrameRate(media.FileMeta.Original.Framerate),
Duration: util.PtrOrZero(media.FileMeta.Original.Duration), Duration: util.PtrOrZero(media.FileMeta.Original.Duration),
Bitrate: int(util.PtrOrZero(media.FileMeta.Original.Bitrate)), Bitrate: util.PtrOrZero(media.FileMeta.Original.Bitrate),
} }
// Copy over local file URL. // Copy over local file URL.
@ -1523,15 +1523,16 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
Title: i.Title, Title: i.Title,
Description: i.Description, Description: i.Description,
DescriptionText: i.DescriptionText, DescriptionText: i.DescriptionText,
CustomCSS: i.CustomCSS,
ShortDescription: i.ShortDescription, ShortDescription: i.ShortDescription,
ShortDescriptionText: i.ShortDescriptionText, ShortDescriptionText: i.ShortDescriptionText,
Email: i.ContactEmail, Email: i.ContactEmail,
Version: config.GetSoftwareVersion(), Version: config.GetSoftwareVersion(),
Languages: config.GetInstanceLanguages().TagStrs(), Languages: config.GetInstanceLanguages().TagStrs(),
Registrations: config.GetAccountsRegistrationOpen(), Registrations: config.GetAccountsRegistrationOpen(),
ApprovalRequired: true, // approval always required ApprovalRequired: true, // approval always required
InvitesEnabled: false, // todo: not supported yet InvitesEnabled: false, // todo: not supported yet
MaxTootChars: uint(config.GetStatusesMaxChars()), MaxTootChars: uint(config.GetStatusesMaxChars()), // #nosec G115 -- Already validated.
Rules: c.InstanceRulesToAPIRules(i.Rules), Rules: c.InstanceRulesToAPIRules(i.Rules),
Terms: i.Terms, Terms: i.Terms,
TermsRaw: i.TermsText, TermsRaw: i.TermsText,
@ -1551,9 +1552,9 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL
instance.Configuration.Statuses.SupportedMimeTypes = instanceStatusesSupportedMimeTypes instance.Configuration.Statuses.SupportedMimeTypes = instanceStatusesSupportedMimeTypes
instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes
instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaRemoteMaxSize()) instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaRemoteMaxSize()) // #nosec G115 -- Already validated.
instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit
instance.Configuration.MediaAttachments.VideoSizeLimit = int(config.GetMediaRemoteMaxSize()) instance.Configuration.MediaAttachments.VideoSizeLimit = int(config.GetMediaRemoteMaxSize()) // #nosec G115 -- Already validated.
instance.Configuration.MediaAttachments.VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit instance.Configuration.MediaAttachments.VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit
instance.Configuration.MediaAttachments.VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit instance.Configuration.MediaAttachments.VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit
instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions() instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions()
@ -1563,7 +1564,7 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS() instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS()
instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags
instance.Configuration.Accounts.MaxProfileFields = instanceAccountsMaxProfileFields instance.Configuration.Accounts.MaxProfileFields = instanceAccountsMaxProfileFields
instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize()) instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize()) // #nosec G115 -- Already validated.
instance.Configuration.OIDCEnabled = config.GetOIDCEnabled() instance.Configuration.OIDCEnabled = config.GetOIDCEnabled()
// URLs // URLs
@ -1644,6 +1645,7 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
SourceURL: instanceSourceURL, SourceURL: instanceSourceURL,
Description: i.Description, Description: i.Description,
DescriptionText: i.DescriptionText, DescriptionText: i.DescriptionText,
CustomCSS: i.CustomCSS,
Usage: apimodel.InstanceV2Usage{}, // todo: not implemented Usage: apimodel.InstanceV2Usage{}, // todo: not implemented
Languages: config.GetInstanceLanguages().TagStrs(), Languages: config.GetInstanceLanguages().TagStrs(),
Rules: c.InstanceRulesToAPIRules(i.Rules), Rules: c.InstanceRulesToAPIRules(i.Rules),
@ -1695,9 +1697,9 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL
instance.Configuration.Statuses.SupportedMimeTypes = instanceStatusesSupportedMimeTypes instance.Configuration.Statuses.SupportedMimeTypes = instanceStatusesSupportedMimeTypes
instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes
instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaRemoteMaxSize()) instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaRemoteMaxSize()) // #nosec G115 -- Already validated.
instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit
instance.Configuration.MediaAttachments.VideoSizeLimit = int(config.GetMediaRemoteMaxSize()) instance.Configuration.MediaAttachments.VideoSizeLimit = int(config.GetMediaRemoteMaxSize()) // #nosec G115 -- Already validated.
instance.Configuration.MediaAttachments.VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit instance.Configuration.MediaAttachments.VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit
instance.Configuration.MediaAttachments.VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit instance.Configuration.MediaAttachments.VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit
instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions() instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions()
@ -1707,7 +1709,7 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS() instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS()
instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags
instance.Configuration.Accounts.MaxProfileFields = instanceAccountsMaxProfileFields instance.Configuration.Accounts.MaxProfileFields = instanceAccountsMaxProfileFields
instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize()) instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize()) // #nosec G115 -- Already validated.
instance.Configuration.OIDCEnabled = config.GetOIDCEnabled() instance.Configuration.OIDCEnabled = config.GetOIDCEnabled()
// registrations // registrations

View file

@ -189,6 +189,16 @@ func CustomCSS(customCSS string) error {
return nil return nil
} }
func InstanceCustomCSS(customCSS string) error {
maximumCustomCSSLength := config.GetAccountsCustomCSSLength()
if length := len([]rune(customCSS)); length > maximumCustomCSSLength {
return fmt.Errorf("custom_css must be less than %d characters, but submitted custom_css was %d characters", maximumCustomCSSLength, length)
}
return nil
}
// EmojiShortcode just runs the given shortcode through the regular expression // EmojiShortcode just runs the given shortcode through the regular expression
// for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 2-30 characters, // for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 2-30 characters,
// a-zA-Z, numbers, and underscores. // a-zA-Z, numbers, and underscores.

View file

@ -54,7 +54,7 @@ func (m *Module) aboutGETHandler(c *gin.Context) {
Template: "about.tmpl", Template: "about.tmpl",
Instance: instance, Instance: instance,
OGMeta: apiutil.OGBase(instance), OGMeta: apiutil.OGBase(instance),
Stylesheets: []string{cssAbout}, Stylesheets: []string{cssAbout, instanceCustomCSSPath},
Extra: map[string]any{ Extra: map[string]any{
"showStrap": true, "showStrap": true,
"blocklistExposed": config.GetInstanceExposeSuspendedWeb(), "blocklistExposed": config.GetInstanceExposeSuspendedWeb(),

View file

@ -127,8 +127,9 @@ func (m *Module) confirmEmailPOSTHandler(c *gin.Context) {
// Serve page informing user that their // Serve page informing user that their
// email address is now confirmed. // email address is now confirmed.
page := apiutil.WebPage{ page := apiutil.WebPage{
Template: "confirmed_email.tmpl", Template: "confirmed_email.tmpl",
Instance: instance, Instance: instance,
Stylesheets: []string{instanceCustomCSSPath},
Extra: map[string]any{ Extra: map[string]any{
"email": user.Email, "email": user.Email,
"username": user.Account.Username, "username": user.Account.Username,

View file

@ -55,3 +55,22 @@ func (m *Module) customCSSGETHandler(c *gin.Context) {
c.Header(cacheControlHeader, cacheControlNoCache) c.Header(cacheControlHeader, cacheControlNoCache)
c.Data(http.StatusOK, textCSSUTF8, []byte(customCSS)) c.Data(http.StatusOK, textCSSUTF8, []byte(customCSS))
} }
func (m *Module) instanceCustomCSSGETHandler(c *gin.Context) {
if _, err := apiutil.NegotiateAccept(c, apiutil.TextCSS); err != nil {
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
instanceV1, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
instanceCustomCSS := instanceV1.CustomCSS
c.Header(cacheControlHeader, cacheControlNoCache)
c.Data(http.StatusOK, textCSSUTF8, []byte(instanceCustomCSS))
}

View file

@ -67,7 +67,7 @@ func (m *Module) domainBlockListGETHandler(c *gin.Context) {
Template: "domain-blocklist.tmpl", Template: "domain-blocklist.tmpl",
Instance: instance, Instance: instance,
OGMeta: apiutil.OGBase(instance), OGMeta: apiutil.OGBase(instance),
Stylesheets: []string{cssFA}, Stylesheets: []string{cssFA, instanceCustomCSSPath},
Javascript: []string{jsFrontend}, Javascript: []string{jsFrontend},
Extra: map[string]any{"blocklist": domainBlocks}, Extra: map[string]any{"blocklist": domainBlocks},
} }

View file

@ -59,7 +59,7 @@ func (m *Module) indexHandler(c *gin.Context) {
Template: "index.tmpl", Template: "index.tmpl",
Instance: instance, Instance: instance,
OGMeta: apiutil.OGBase(instance), OGMeta: apiutil.OGBase(instance),
Stylesheets: []string{cssAbout, cssIndex}, Stylesheets: []string{cssAbout, cssIndex, instanceCustomCSSPath},
Extra: map[string]any{"showStrap": true}, Extra: map[string]any{"showStrap": true},
} }

View file

@ -132,7 +132,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
} }
// Prepare stylesheets for profile. // Prepare stylesheets for profile.
stylesheets := make([]string, 0, 6) stylesheets := make([]string, 0, 7)
// Basic profile stylesheets. // Basic profile stylesheets.
stylesheets = append( stylesheets = append(
@ -142,6 +142,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
cssStatus, cssStatus,
cssThread, cssThread,
cssProfile, cssProfile,
instanceCustomCSSPath,
}..., }...,
) )

View file

@ -53,6 +53,7 @@ func (m *Module) SettingsPanelHandler(c *gin.Context) {
cssProfile, // Used for rendering stub/fake profiles. cssProfile, // Used for rendering stub/fake profiles.
cssStatus, // Used for rendering stub/fake statuses. cssStatus, // Used for rendering stub/fake statuses.
cssSettings, cssSettings,
instanceCustomCSSPath,
}, },
Javascript: []string{jsSettings}, Javascript: []string{jsSettings},
} }

View file

@ -126,9 +126,10 @@ func (m *Module) signupPOSTHandler(c *gin.Context) {
// Serve a page informing the // Serve a page informing the
// user that they've signed up. // user that they've signed up.
page := apiutil.WebPage{ page := apiutil.WebPage{
Template: "signed-up.tmpl", Template: "signed-up.tmpl",
Instance: instance, Instance: instance,
OGMeta: apiutil.OGBase(instance), Stylesheets: []string{instanceCustomCSSPath},
OGMeta: apiutil.OGBase(instance),
Extra: map[string]any{ Extra: map[string]any{
"email": user.UnconfirmedEmail, "email": user.UnconfirmedEmail,
"username": user.Account.Username, "username": user.Account.Username,

View file

@ -59,7 +59,7 @@ func (m *Module) tagGETHandler(c *gin.Context) {
Template: "tag.tmpl", Template: "tag.tmpl",
Instance: instance, Instance: instance,
OGMeta: apiutil.OGBase(instance), OGMeta: apiutil.OGBase(instance),
Stylesheets: []string{cssFA, cssThread, cssTag}, Stylesheets: []string{cssFA, cssThread, cssTag, instanceCustomCSSPath},
Extra: map[string]any{"tagName": tagName}, Extra: map[string]any{"tagName": tagName},
} }

View file

@ -115,7 +115,7 @@ func (m *Module) threadGETHandler(c *gin.Context) {
} }
// Prepare stylesheets for thread. // Prepare stylesheets for thread.
stylesheets := make([]string, 0, 5) stylesheets := make([]string, 0, 6)
// Basic thread stylesheets. // Basic thread stylesheets.
stylesheets = append( stylesheets = append(
@ -131,6 +131,7 @@ func (m *Module) threadGETHandler(c *gin.Context) {
if theme := targetAccount.Theme; theme != "" { if theme := targetAccount.Theme; theme != "" {
stylesheets = append( stylesheets = append(
stylesheets, stylesheets,
instanceCustomCSSPath,
themesPathPrefix+"/"+theme, themesPathPrefix+"/"+theme,
) )
} }

View file

@ -36,20 +36,21 @@
) )
const ( const (
confirmEmailPath = "/" + uris.ConfirmEmailPath confirmEmailPath = "/" + uris.ConfirmEmailPath
profileGroupPath = "/@:username" profileGroupPath = "/@:username"
statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group
tagsPath = "/tags/:" + apiutil.TagNameKey tagsPath = "/tags/:" + apiutil.TagNameKey
customCSSPath = profileGroupPath + "/custom.css" customCSSPath = profileGroupPath + "/custom.css"
rssFeedPath = profileGroupPath + "/feed.rss" instanceCustomCSSPath = "/custom.css"
assetsPathPrefix = "/assets" rssFeedPath = profileGroupPath + "/feed.rss"
distPathPrefix = assetsPathPrefix + "/dist" assetsPathPrefix = "/assets"
themesPathPrefix = assetsPathPrefix + "/themes" distPathPrefix = assetsPathPrefix + "/dist"
settingsPathPrefix = "/settings" themesPathPrefix = assetsPathPrefix + "/themes"
settingsPanelGlob = settingsPathPrefix + "/*panel" settingsPathPrefix = "/settings"
userPanelPath = settingsPathPrefix + "/user" settingsPanelGlob = settingsPathPrefix + "/*panel"
adminPanelPath = settingsPathPrefix + "/admin" userPanelPath = settingsPathPrefix + "/user"
signupPath = "/signup" adminPanelPath = settingsPathPrefix + "/admin"
signupPath = "/signup"
cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
cacheControlNoCache = "no-cache" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives cacheControlNoCache = "no-cache" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives
@ -114,6 +115,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler)
r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler)
r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler) r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler)
r.AttachHandler(http.MethodGet, instanceCustomCSSPath, m.instanceCustomCSSGETHandler)
r.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler) r.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler)
r.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler) r.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler)
r.AttachHandler(http.MethodPost, confirmEmailPath, m.confirmEmailPOSTHandler) r.AttachHandler(http.MethodPost, confirmEmailPath, m.confirmEmailPOSTHandler)

View file

@ -25,6 +25,7 @@ export interface InstanceV1 {
description_text?: string; description_text?: string;
short_description: string; short_description: string;
short_description_text?: string; short_description_text?: string;
custom_css: string;
email: string; email: string;
version: string; version: string;
debug?: boolean; debug?: boolean;

View file

@ -66,6 +66,10 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) {
valueSelector: (s: InstanceV1) => s.description_text, valueSelector: (s: InstanceV1) => s.description_text,
validator: (val: string) => val.length <= descLimit ? "" : `Instance description is ${val.length} characters; must be ${descLimit} characters or less` validator: (val: string) => val.length <= descLimit ? "" : `Instance description is ${val.length} characters; must be ${descLimit} characters or less`
}), }),
customCSS: useTextInput("custom_css", {
source: instance,
valueSelector: (s: InstanceV1) => s.custom_css
}),
terms: useTextInput("terms", { terms: useTextInput("terms", {
source: instance, source: instance,
// Select "raw" text version of parsed field for editing. // Select "raw" text version of parsed field for editing.
@ -191,6 +195,15 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) {
type="email" type="email"
/> />
<TextArea
field={form.customCSS}
label={"Custom CSS"}
className="monospace"
rows={8}
autoCapitalize="none"
spellCheck="false"
/>
<MutationButton label="Save" result={result} disabled={false} /> <MutationButton label="Save" result={result} disabled={false} />
</form> </form>
); );