Skip to content
quarry Artifact [fed77049e5]
Login

Artifact [fed77049e5]

Artifact fed77049e56e1422873f8709a90284cb589ec1eafc3757ecf9f036aedb5f04ec:


package oltp

import (
	"fmt"
	"strings"
)

// Refusal is a save the guard would not make, with the reason in the
// words the page shows. It is the error type callers match on.
type Refusal struct{ Reason string }

func (r Refusal) Error() string { return r.Reason }

// GuardCaps judges a change to one account's string on a repository
// before it is written (RFC 0008, bulletproofing): a save that would
// remove the actor's own admin here is refused; a save that would
// leave the repository with no admin is refused unless the actor is a
// site admin, who is told instead. Site admins hold s through the
// account flag, so nothing they do to a string locks them out.
func (d *DB) GuardCaps(actor, account, repo, caps string) (warning string, err error) {
	me, _ := d.AccountByName(actor)
	if !me.Admin && actor == account {
		if d.EffectiveCaps(actor, repo).HasAny("a") && !d.effective(actor, repo, caps).HasAny("a") {
			return "", Refusal{fmt.Sprintf("that would remove your own admin on %s; have another admin make the change", repo)}
		}
	}
	before, after, err := d.adminCount(repo, account, caps)
	if err != nil {
		return "", err
	}
	if before > 0 && after == 0 {
		if me.Admin {
			return fmt.Sprintf("%s has no admin of its own now; only site admins can change its settings", repo), nil
		}
		return "", Refusal{fmt.Sprintf("that would leave %s with no admin; make someone else an Admin first", repo)}
	}
	return "", nil
}

// adminCount counts the repository's admins — accounts whose effective
// set holds a on their own account, site admins aside — before and
// after one account's string becomes caps.
func (d *DB) adminCount(repo, account, caps string) (before, after int, err error) {
	holders, err := d.CapHolders(repo)
	if err != nil {
		return 0, 0, err
	}
	for _, h := range holders {
		if acct, _ := d.AccountByName(h.Account); acct.Admin {
			continue
		}
		if d.effective(h.Account, repo, h.Caps).HasAny("a") {
			before++
		}
		own := h.Caps
		if h.Account == account {
			own = caps
		}
		if d.effective(h.Account, repo, own).HasAny("a") {
			after++
		}
	}
	return before, after, nil
}

// IsRefusal reports whether err is the guard saying no.
func IsRefusal(err error) bool {
	_, ok := err.(Refusal)
	return ok
}

// Minimal drops every letter the others already imply, so a string
// assembled from checkboxes is stored the way a person would type it.
func Minimal(caps string) string {
	// In the order given: the presets are hand-ordered strings, and a
	// string that round-trips through the editor should come back as
	// it went in.
	full := Closure(caps)
	var out []rune
	for _, r := range capUnion(caps) {
		rest := strings.ReplaceAll(full, string(r), "")
		if !strings.ContainsRune(Closure(rest), r) {
			out = append(out, r)
		}
	}
	return string(out)
}

// Change says what a save did, in words: the role it moved between and
// the abilities it gained and lost. "" when nothing changed.
func Change(before, after string) string {
	if Closure(before) == Closure(after) {
		return ""
	}
	var parts []string
	b, a := Infer(before), Infer(after)
	switch {
	case before == "":
		parts = append(parts, "added as "+a.Role)
	case after == "":
		parts = append(parts, "removed from the repository")
	case b.Role != a.Role:
		parts = append(parts, b.Role+" → "+a.Role)
	}
	was := map[string]bool{}
	for _, s := range Decode(before) {
		was[s] = true
	}
	now := map[string]bool{}
	var gained, lost []string
	for _, s := range Decode(after) {
		now[s] = true
		if !was[s] {
			gained = append(gained, s)
		}
	}
	for _, s := range Decode(before) {
		if !now[s] {
			lost = append(lost, s)
		}
	}
	if len(gained) > 0 && after != "" {
		parts = append(parts, "gains: "+strings.Join(gained, ", "))
	}
	if len(lost) > 0 && before != "" && after != "" {
		parts = append(parts, "loses: "+strings.Join(lost, ", "))
	}
	return strings.Join(parts, "; ")
}