A game sold in the EU cannot ship what a licence excluding the EU made. On the profile page a person leaves out licences one by one, or by rule: every licence forbidding commercial use, every licence granting nothing in a region. The rules cover models added later. The choice reaches every principal through the resolved limits, so permits() leaves such models out of /v1/models, requests, jobs, fallbacks and the portal's pickers, and a request for one is refused 403 licence_excluded saying which choice did. Kept in user_excluded_licences (migration 0046), copied with the store and gone with the person. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
143 lines
4.4 KiB
Go
143 lines
4.4 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// LicenceChoice is one licence the catalog's models are under, as the
|
|
// profile page offers it.
|
|
type LicenceChoice struct {
|
|
// Key is what is stored when it is ticked: the licence's id, else its
|
|
// name, lowercased; "unspecified" for models whose entry names none.
|
|
Key string
|
|
Name, URL string
|
|
// Commercial is "yes" or "no" as the licence says, empty when it does
|
|
// not say.
|
|
Commercial string
|
|
ExcludedRegions []string
|
|
Models []string
|
|
// Excluded is whether the person has left it out.
|
|
Excluded bool
|
|
}
|
|
|
|
// LicenceChoices is the licences on offer, and what the rules can be about.
|
|
type LicenceChoices struct {
|
|
Licences []LicenceChoice
|
|
// Regions are those some licence in the catalog grants nothing in.
|
|
Regions []string
|
|
// AnyNonCommercial is whether some licence forbids commercial use.
|
|
AnyNonCommercial bool
|
|
// What the person has chosen, for the rules.
|
|
NonCommercial bool
|
|
ExcludedRegions []string
|
|
}
|
|
|
|
// LicencePrefs keeps what a person has said not to use.
|
|
type LicencePrefs interface {
|
|
UserExcludedLicences(ctx context.Context, userID string) ([]string, error)
|
|
SetUserExcludedLicences(ctx context.Context, userID string, licences []string) error
|
|
}
|
|
|
|
// Rule words, matched by the router.
|
|
const (
|
|
licenceRuleNonCommercial = "rule:non-commercial"
|
|
licenceRuleRegion = "rule:region:"
|
|
)
|
|
|
|
var regionCode = regexp.MustCompile(`^[A-Z]{2}$`)
|
|
|
|
// SetLicences offers the choice of licences on the profile page: what the
|
|
// catalog's models are under, and where the choices are kept.
|
|
func (p *AccountPages) SetLicences(choices func() LicenceChoices, prefs LicencePrefs) {
|
|
p.licenceChoices, p.licencePrefs = choices, prefs
|
|
}
|
|
|
|
// fillLicences puts the choice, and what the person has made of it, on the
|
|
// profile page; nothing when it is not offered.
|
|
func (p *AccountPages) fillLicences(ctx context.Context, userID string, d *profileData) {
|
|
if p.licenceChoices == nil || p.licencePrefs == nil {
|
|
return
|
|
}
|
|
c := p.licenceChoices()
|
|
ex, err := p.licencePrefs.UserExcludedLicences(ctx, userID)
|
|
if err != nil {
|
|
p.log.Error("could not read a person's licence choices", "err", err)
|
|
}
|
|
for i := range c.Licences {
|
|
c.Licences[i].Excluded = slices.ContainsFunc(ex, func(e string) bool { return strings.EqualFold(e, c.Licences[i].Key) })
|
|
}
|
|
for _, e := range ex {
|
|
switch {
|
|
case e == licenceRuleNonCommercial:
|
|
c.NonCommercial = true
|
|
case strings.HasPrefix(e, licenceRuleRegion):
|
|
c.ExcludedRegions = append(c.ExcludedRegions, strings.TrimPrefix(e, licenceRuleRegion))
|
|
}
|
|
}
|
|
// A region the person excluded that no licence in the catalog names yet
|
|
// is still offered, ticked: it is there for the model added next.
|
|
for _, reg := range c.ExcludedRegions {
|
|
if !slices.Contains(c.Regions, reg) {
|
|
c.Regions = append(c.Regions, reg)
|
|
}
|
|
}
|
|
slices.Sort(c.Regions)
|
|
d.Licences = &c
|
|
}
|
|
|
|
func (p *AccountPages) setLicences(w http.ResponseWriter, r *http.Request) {
|
|
noStore(w)
|
|
sess, user, ok := p.session(r)
|
|
if !ok {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
if p.licencePrefs == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var ex []string
|
|
for _, k := range r.Form["licence"] {
|
|
if k = strings.TrimSpace(k); k != "" && !strings.HasPrefix(k, "rule:") {
|
|
ex = append(ex, strings.ToLower(k))
|
|
}
|
|
}
|
|
if r.FormValue("non_commercial") != "" {
|
|
ex = append(ex, licenceRuleNonCommercial)
|
|
}
|
|
regions := r.Form["region"]
|
|
regions = append(regions, strings.FieldsFunc(r.FormValue("regions_other"), func(c rune) bool {
|
|
return c == ',' || c == ' '
|
|
})...)
|
|
l := langOf(r)
|
|
for _, reg := range regions {
|
|
reg = strings.ToUpper(strings.TrimSpace(reg))
|
|
if reg == "" {
|
|
continue
|
|
}
|
|
if !regionCode.MatchString(reg) {
|
|
p.render(w, r, sess, user, profileData{LicenceError: l.T("profile.licences.err_region", reg)})
|
|
return
|
|
}
|
|
ex = append(ex, licenceRuleRegion+reg)
|
|
}
|
|
if err := p.licencePrefs.SetUserExcludedLicences(r.Context(), user.ID, ex); err != nil {
|
|
p.log.Error("could not keep a person's licence choices", "err", err)
|
|
p.render(w, r, sess, user, profileData{LicenceError: l.T("profile.err.try_again")})
|
|
return
|
|
}
|
|
p.render(w, r, sess, user, profileData{LicencesSaved: true})
|
|
}
|