1package home23import (4 "chain"5 "chain/banker"6 "chain/runtime"7 "chain/runtime/unsafe"8 "strconv"9 "strings"10 "time"11 "unicode"12 "unicode/utf8"13)1415// Every write function calls runtime.AssertOriginCall() first, in its own16// body: from inside a helper it refuses every call.1718const maxSignBytes = 5601920func invalid(field string) string { return errInvalidInput + field }2122// checkLine enforces valid UTF-8, a rune count in [min, max], and no control23// characters (so no newline either): the text may end up inside an SVG,24// where they would break the XML.25func checkLine(field, s string, min, max int) {26 if !utf8.ValidString(s) {27 panic(invalid(field))28 }29 n := 030 for _, r := range s {31 if r < 0x20 || r == 0x7f || r == 0xfffe || r == 0xffff {32 panic(invalid(field))33 }34 n++35 }36 if n < min || n > max {37 panic(invalid(field))38 }39}4041// assertMasterKey refuses a transaction signed by a session key. Delegated42// keys may edit content, but not move ownership or hide an entry for good.43func assertMasterKey() {44 if _, _, _, isSession := runtime.GetSessionInfo(); isSession {45 panic(errMasterKey)46 }47}4849// checkBadgeURL wants a printable-ASCII https:// URL (non-ASCII must be50// percent-encoded) with no quotes, backslashes, backticks, parentheses,51// square brackets, < or >, so the markdown link can't end early or become52// javascript:.53func checkBadgeURL(u string) {54 if !strings.HasPrefix(u, "https://") {55 panic(invalid("url"))56 }57 for i := 0; i < len(u); i++ {58 c := u[i]59 if c <= 0x20 || c >= 0x7f || strings.IndexByte("\"'\\`()[]<>", c) >= 0 {60 panic(invalid("url"))61 }62 }63}6465// parseBadges reads one badge per line, "label | value | url". Blank lines66// between two badges start a new row; other blank lines are skipped.67func parseBadges(spec string) []badge {68 var out []badge69 row := false70 for _, line := range strings.Split(spec, "\n") {71 line = strings.TrimSpace(line)72 if line == "" {73 row = len(out) > 074 continue75 }76 parts := strings.Split(line, "|")77 if len(parts) != 3 || len(out) == maxBadges {78 panic(invalid("spec"))79 }80 b := badge{81 label: strings.TrimSpace(parts[0]),82 value: strings.TrimSpace(parts[1]),83 url: strings.TrimSpace(parts[2]),84 row: row,85 }86 row = false87 checkLine("label", b.label, 1, 24)88 checkLine("value", b.value, 1, 24)89 checkBadgeURL(b.url)90 out = append(out, b)91 }92 return out93}9495// hasVisible reports whether s has a character that draws something: not a96// space, a format character (zero-width, bidi), a combining mark, or one of97// the blank-looking fillers (Hangul fillers, braille blank).98func hasVisible(s string) bool {99 for _, r := range s {100 switch {101 case unicode.IsSpace(r), unicode.In(r, unicode.Cf, unicode.Mn, unicode.Me):102 case r == 0x115f, r == 0x1160, r == 0x3164, r == 0xffa0, r == 0x2800:103 default:104 return true105 }106 }107 return false108}109110// validName wants 1 to max characters from a-z, 0-9 and "-": safe in a111// link, a tab label, and a path gnoweb passes to Render.112func validName(s string, max int) bool {113 if len(s) < 1 || len(s) > max {114 return false115 }116 for _, c := range s {117 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') {118 return false119 }120 }121 return true122}123124// checkPage enforces the tab name, the page size, and the tab count.125func checkPage(tab, md string) {126 if !validName(tab, maxTabName) {127 panic(invalid("tab"))128 }129 if len(md) > maxPageBytes || !utf8.ValidString(md) {130 panic(invalid("md"))131 }132 if _, exists := pages[tab]; !exists && len(tabs) >= maxTabs {133 panic(invalid("tab"))134 }135}136137// checkPost enforces the slug, a one-line title, and the body size. Title138// and body must each show something.139func checkPost(slug, title, body string) {140 if !validName(slug, maxSlugBytes) {141 panic(invalid("slug"))142 }143 checkLine("title", title, 1, maxTitleRunes)144 if !hasVisible(title) {145 panic(invalid("title"))146 }147 if len(body) > maxPageBytes || !utf8.ValidString(body) || !hasVisible(body) {148 panic(invalid("body"))149 }150}151152// seqKey zero-pads n so that keys sort in number order.153func seqKey(n int) string {154 s := strconv.Itoa(n)155 return strings.Repeat("0", seqWidth-len(s)) + s156}157158func touch() { updated = runtime.ChainHeight() }159160// ----- owner only -----161162// SetNow sets the one-line status shown on Home; empty clears it.163func SetNow(cur realm, text string) {164 runtime.AssertOriginCall()165 assertOwner(cur)166 checkLine("text", text, 0, maxLineRunes)167 now = nowLine{text: text, height: runtime.ChainHeight()}168 touch()169 chain.Emit("NowSet", "text", text)170}171172// SetProfile sets the name and tagline drawn on the banner.173func SetProfile(cur realm, name, tagline string) {174 runtime.AssertOriginCall()175 assertOwner(cur)176 checkLine("name", name, 1, 24)177 checkLine("tagline", tagline, 0, 60)178 profile = profileData{name: name, tagline: tagline}179 touch()180 chain.Emit("ProfileSet", "name", name, "tagline", tagline)181}182183// SetBadges replaces every badge. One badge per line: "label | value | url";184// a blank line starts a new row.185func SetBadges(cur realm, spec string) {186 runtime.AssertOriginCall()187 assertOwner(cur)188 badges = parseBadges(spec)189 touch()190 chain.Emit("BadgesSet", "count", strconv.Itoa(len(badges)))191}192193// SetPage creates or replaces any tab's markdown, Blog and Guestbook194// included. A new tab goes just before Guestbook, which stays last.195func SetPage(cur realm, tab, md string) {196 runtime.AssertOriginCall()197 assertOwner(cur)198 checkPage(tab, md)199 if _, exists := pages[tab]; !exists {200 tabs = append(tabs[:len(tabs)-1], tab, guestbookTab) // guestbook stays last201 }202 pages[tab] = md203 touch()204 chain.Emit("PageSet", "tab", tab)205}206207// RemovePage removes any tab except home, blog and guestbook.208func RemovePage(cur realm, tab string) {209 runtime.AssertOriginCall()210 assertOwner(cur)211 if tab == homeTab || tab == blogTab || tab == guestbookTab {212 panic(errTabsRequired)213 }214 if _, ok := pages[tab]; !ok {215 panic(errNoTab)216 }217 delete(pages, tab)218 kept := make([]string, 0, len(tabs)-1)219 for _, t := range tabs {220 if t != tab {221 kept = append(kept, t)222 }223 }224 tabs = kept225 touch()226 chain.Emit("PageRemoved", "tab", tab)227}228229// SetPost publishes a post, or edits the post at slug. An edit keeps the230// post's place in the list and its publish date.231func SetPost(cur realm, slug, title, body string) {232 runtime.AssertOriginCall()233 assertOwner(cur)234 checkPost(slug, title, body)235 today := time.Now().UTC().Format("2006-01-02")236 touch()237 if p, ok := posts.Get(slug).(*post); ok {238 p.title, p.body, p.edited = title, body, today239 chain.Emit("PostEdited", "slug", slug)240 return241 }242 postSeq++243 seq := seqKey(postSeq)244 posts.Set(slug, &post{title: title, body: body, published: today, seq: seq})245 postOrder.Set(seq, slug)246 chain.Emit("PostPublished", "slug", slug)247}248249// RemovePost deletes a post for good; its slug can be used again.250func RemovePost(cur realm, slug string) {251 runtime.AssertOriginCall()252 assertOwner(cur)253 v, ok := posts.Remove(slug)254 if !ok {255 panic(errNoPost)256 }257 postOrder.Remove(v.(*post).seq)258 touch()259 chain.Emit("PostRemoved", "slug", slug)260}261262// Hide hides an author's guestbook entry for good. The entry stays stored,263// so the author can't sign again, and nobody's deposit is refunded. Hiding264// a hidden entry does nothing.265func Hide(cur realm, author address) {266 runtime.AssertOriginCall()267 assertOwner(cur)268 assertMasterKey()269 e, ok := entries.Get(author.String()).(*entry)270 if !ok {271 panic(errNoEntry)272 }273 if e.hidden {274 return275 }276 e.hidden = true277 touch()278 chain.Emit("EntryHidden", "author", author.String())279}280281// TransferOwnership records newOwner as pending; nothing changes until that282// address calls AcceptOwnership. Naming the current owner cancels.283func TransferOwnership(cur realm, newOwner address) {284 runtime.AssertOriginCall()285 assertOwner(cur)286 assertMasterKey()287 if !newOwner.IsValid() {288 panic(invalid("newOwner"))289 }290 touch()291 if newOwner == owner {292 pendingOwner = ""293 chain.Emit("OwnershipTransferCanceled", "owner", owner.String())294 return295 }296 pendingOwner = newOwner297 chain.Emit("OwnershipTransferStarted", "from", owner.String(), "to", newOwner.String())298}299300// AcceptOwnership completes a transfer. Only the pending owner may call it,301// and a realm address can't pass the account guard, so it never can.302func AcceptOwnership(cur realm) {303 runtime.AssertOriginCall()304 caller := callerOf(cur)305 if pendingOwner == "" || caller != pendingOwner {306 panic(errPendingOnly)307 }308 prev := owner309 owner = caller310 pendingOwner = ""311 touch()312 chain.Emit("OwnershipTransferred", "from", prev.String(), "to", caller.String())313}314315// ----- visitors -----316317// Wave adds one to the wave count. It's a plain counter: anyone can wave318// again, and a wave stores nothing but the count.319func Wave(cur realm) {320 runtime.AssertOriginCall()321 from := callerOf(cur).String()322 waves++323 chain.Emit("Waved", "from", from)324}325326// Sign leaves one guestbook entry per address. The message is stored trimmed327// and sanitized when rendered, and must contain a visible character.328func Sign(cur realm, msg string) {329 runtime.AssertOriginCall()330 caller := callerOf(cur)331 if len(msg) > maxSignBytes {332 panic(invalid("msg"))333 }334 msg = strings.TrimSpace(msg)335 checkLine("msg", msg, 1, maxLineRunes)336 if !hasVisible(msg) {337 panic(invalid("msg")) // the entry would show up blank338 }339 key := caller.String()340 if entries.Has(key) {341 panic(errSigned)342 }343 entries.Set(key, &entry{msg: msg, height: runtime.ChainHeight()})344 order.Set(seqKey(order.Size()+1), caller)345 chain.Emit("Signed", "from", key)346}347348// Tip forwards exactly the ugnot sent with this call to the owner. The349// account guard is inlined so the audit harness sees it next to OriginSend.350// It forwards what was sent, never the realm's balance: the OriginSend351// banker refuses to spend more than was sent.352func Tip(cur realm) {353 runtime.AssertOriginCall()354 if !cur.IsCurrent() || !cur.Previous().IsUserCall() {355 panic(errUnauthorized)356 }357 sent := unsafe.OriginSend()358 if len(sent) != 1 || sent[0].Denom != "ugnot" || sent[0].Amount <= 0 {359 panic(errUgnotOnly)360 }361 banker.NewBanker(banker.BankerTypeOriginSend, cur).SendCoins(cur.Address(), owner, sent)362 tips.count++363 tips.total += sent[0].Amount364 chain.Emit("Tipped", "from", cur.Previous().Address().String(), "amount", strconv.FormatInt(sent[0].Amount, 10))365}366AcceptOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Hide(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, author string)
Owner() string
RemovePage(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tab string)
RemovePost(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string)
Render(path string) string
SetBadges(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, spec string)
SetNow(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, text string)
SetPage(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tab string, md string)
SetPost(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string, title string, body string)
SetProfile(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, tagline string)
Sign(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, msg string)
Tip(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
TransferOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newOwner string)
Wave(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.