Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions backend/app/http/endpoints/api/gallery/galleryimport.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ func ImportHandler(ctx *gin.Context) {
return
}

// The bot token can post anywhere the bot is, so both IDs must be proven to belong to this guild
// before anything is sent or stored, otherwise a panel can be planted in another server.
if body.ChannelId != nil || body.CategoryId != nil {
chCtx, cancel := app.DefaultContext()
channels, err := botContext.GetGuildChannels(chCtx, guildId)
cancel()
if err != nil {
_ = ctx.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to fetch guild channels from Discord"))
return
}

if msg := validateImportTargets(channels, body.ChannelId, body.CategoryId); msg != "" {
ctx.JSON(http.StatusBadRequest, utils.ErrorStr("%s", msg))
return
}
}

// Check panel quota
premiumTier, err := rpc.PremiumClient.GetTierByGuildId(ctx, guildId, false, botContext.Token, botContext.RateLimiter)
if err != nil {
Expand Down
38 changes: 38 additions & 0 deletions backend/app/http/endpoints/api/gallery/importvalidation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,47 @@ import (
"errors"

"github.com/TicketsBot-cloud/database"
"github.com/TicketsBot-cloud/gdl/objects/channel"
"github.com/ticketsbot-cloud/dashboard/backend/utils/types"
)

// validateImportTargets returns a user-facing error message, or "" if both targets are valid.
func validateImportTargets(channels []channel.Channel, channelId, categoryId *uint64) string {
find := func(id uint64) (channel.Channel, bool) {
for _, ch := range channels {
if ch.Id == id {
return ch, true
}
}
return channel.Channel{}, false
}

if channelId != nil {
ch, ok := find(*channelId)
if !ok {
return "Panel channel not found"
}
if ch.IsObfuscated() {
return "Panel channel is not visible to the bot - grant it the View Channel permission"
}
if ch.Type != channel.ChannelTypeGuildText && ch.Type != channel.ChannelTypeGuildNews {
return "Panel channel must be a text or announcement channel"
}
}

if categoryId != nil {
ch, ok := find(*categoryId)
if !ok || ch.Type != channel.ChannelTypeGuildCategory {
return "Invalid ticket category"
}
if ch.IsObfuscated() {
return "Ticket category is not visible to the bot - grant it the View Channel permission"
}
}

return ""
}

// Snapshots may predate URL validation, and the importer cannot edit someone else's listing.
func validateListingUrls(e *database.CustomEmbed, panelUrls ...*string) error {
for _, url := range panelUrls {
Expand Down
59 changes: 59 additions & 0 deletions backend/app/http/endpoints/api/gallery/importvalidation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package gallery

import (
"testing"

"github.com/TicketsBot-cloud/gdl/objects/channel"
"github.com/stretchr/testify/require"
)

func TestValidateImportTargets(t *testing.T) {
const (
textId uint64 = 1
newsId uint64 = 2
voiceId uint64 = 3
categoryId uint64 = 4
foreignId uint64 = 999
)

channels := []channel.Channel{
{Id: textId, Type: channel.ChannelTypeGuildText},
{Id: newsId, Type: channel.ChannelTypeGuildNews},
{Id: voiceId, Type: channel.ChannelTypeGuildVoice},
{Id: categoryId, Type: channel.ChannelTypeGuildCategory},
}

ptr := func(v uint64) *uint64 { return &v }

tests := []struct {
name string
channels []channel.Channel
channel *uint64
category *uint64
wantErr bool
}{
{name: "neither supplied", channels: channels},
{name: "text channel", channels: channels, channel: ptr(textId)},
{name: "announcement channel", channels: channels, channel: ptr(newsId)},
{name: "channel and category", channels: channels, channel: ptr(textId), category: ptr(categoryId)},
{name: "channel from another guild", channels: channels, channel: ptr(foreignId), wantErr: true},
{name: "category from another guild", channels: channels, category: ptr(foreignId), wantErr: true},
{name: "valid channel, foreign category", channels: channels, channel: ptr(textId), category: ptr(foreignId), wantErr: true},
{name: "voice channel as panel channel", channels: channels, channel: ptr(voiceId), wantErr: true},
{name: "category as panel channel", channels: channels, channel: ptr(categoryId), wantErr: true},
{name: "text channel as category", channels: channels, category: ptr(textId), wantErr: true},
{name: "zero channel id", channels: channels, channel: ptr(0), wantErr: true},
{name: "empty guild channel list", channels: nil, channel: ptr(textId), wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := validateImportTargets(tt.channels, tt.channel, tt.category)
if tt.wantErr {
require.NotEmpty(t, msg)
} else {
require.Empty(t, msg)
}
})
}
}
Loading