cerca

lean forum software (pmc local branch)
git clone http://git.permacomputing.net/repos/cerca.git # read-only access
Log | Files | Refs | README | LICENSE

util.go (6268B)


      1 package util
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/base64"
      6 	"encoding/hex"
      7 	"encoding/json"
      8 	"errors"
      9 	"fmt"
     10 	"html/template"
     11 	"log"
     12 	"net/http"
     13 	"net/url"
     14 	"os"
     15 	"path/filepath"
     16 	"regexp"
     17 	"strconv"
     18 	"strings"
     19 	"time"
     20 
     21 	"github.com/gomarkdown/markdown"
     22 	"github.com/gomarkdown/markdown/parser"
     23 	"github.com/komkom/toml"
     24 	"github.com/microcosm-cc/bluemonday"
     25 	"golang.org/x/exp/utf8string"
     26 
     27 	"cerca/defaults"
     28 	"cerca/types"
     29 )
     30 
     31 /* util.Eout example invocations
     32 if err != nil {
     33   return util.Eout(err, "reading data")
     34 }
     35 if err = util.Eout(err, "reading data"); err != nil {
     36   return nil, err
     37 }
     38 */
     39 
     40 type ErrorDescriber struct {
     41 	environ string // the basic context that is potentially generating errors (like a GetThread function, the environ would be "get thread")
     42 }
     43 
     44 // parametrize Eout/Check such that error messages contain a defined context/environ
     45 func Describe(environ string) ErrorDescriber {
     46 	return ErrorDescriber{environ}
     47 }
     48 
     49 func (ed ErrorDescriber) Eout(err error, msg string, args ...interface{}) error {
     50 	msg = fmt.Sprintf("%s: %s", ed.environ, msg)
     51 	return Eout(err, msg, args...)
     52 }
     53 
     54 func (ed ErrorDescriber) Check(err error, msg string, args ...interface{}) {
     55 	msg = fmt.Sprintf("%s: %s", ed.environ, msg)
     56 	Check(err, msg, args...)
     57 }
     58 
     59 // format all errors consistently, and provide context for the error using the string `msg`
     60 func Eout(err error, msg string, args ...interface{}) error {
     61 	if err != nil {
     62 		// received an invocation of e.g. format:
     63 		// Eout(err, "reading data for %s and %s", "database item", "weird user")
     64 		if len(args) > 0 {
     65 			return fmt.Errorf("%s (%w)", fmt.Sprintf(msg, args...), err)
     66 		}
     67 		return fmt.Errorf("%s (%w)", msg, err)
     68 	}
     69 	return nil
     70 }
     71 
     72 func Check(err error, msg string, args ...interface{}) {
     73 	if len(args) > 0 {
     74 		err = Eout(err, msg, args...)
     75 	} else {
     76 		err = Eout(err, msg)
     77 	}
     78 	if err != nil {
     79 		log.Fatalln(err)
     80 	}
     81 }
     82 
     83 func Contains(slice []string, s string) bool {
     84 	for _, item := range slice {
     85 		if item == s {
     86 			return true
     87 		}
     88 	}
     89 	return false
     90 }
     91 
     92 var contentGuardian = bluemonday.UGCPolicy()
     93 var strictContentGuardian = bluemonday.StrictPolicy()
     94 
     95 // Turns Markdown input into HTML
     96 func Markup(md string) template.HTML {
     97 	mdBytes := []byte(string(md))
     98 	// fix newlines
     99 	mdBytes = markdown.NormalizeNewlines(mdBytes)
    100 	mdParser := parser.NewWithExtensions(parser.CommonExtensions ^ parser.MathJax)
    101 	maybeUnsafeHTML := markdown.ToHTML(mdBytes, mdParser, nil)
    102 	// guard against malicious code being embedded
    103 	html := contentGuardian.SanitizeBytes(maybeUnsafeHTML)
    104 	// lazy load images
    105 	pattern := regexp.MustCompile("<img")
    106 	if pattern.Match(html) {
    107 		html = pattern.ReplaceAll(html, []byte(`<img loading="lazy"`))
    108 	}
    109 	return template.HTML(html)
    110 }
    111 
    112 func SanitizeStringStrict(s string) string {
    113 	return strictContentGuardian.Sanitize(s)
    114 }
    115 
    116 func VerificationPrefix(name string) string {
    117 	pattern := regexp.MustCompile("A|E|O|U|I|Y")
    118 	upper := strings.ToUpper(name)
    119 	replaced := string(pattern.ReplaceAll([]byte(upper), []byte("")))
    120 	if len(replaced) < 3 {
    121 		replaced += "XYZ"
    122 	}
    123 	return replaced[0:3]
    124 }
    125 
    126 func GetThreadSlug(threadid int, title string, threadLen int) string {
    127 	return fmt.Sprintf("/thread/%d/%s-%d/", threadid, SanitizeURL(title), threadLen)
    128 }
    129 
    130 func Hex2Base64(s string) (string, error) {
    131 	b, err := hex.DecodeString(s)
    132 	if err != nil {
    133 		return "", err
    134 	}
    135 	b64 := base64.StdEncoding.EncodeToString(b)
    136 	return b64, nil
    137 }
    138 
    139 // make a string be suitable for use as part of a url
    140 func SanitizeURL(input string) string {
    141 	input = strings.ReplaceAll(input, " ", "-")
    142 	input = url.PathEscape(input)
    143 	// TODO(2022-01-08): evaluate use of strict content guardian?
    144 	return strings.ToLower(input)
    145 }
    146 
    147 // returns an id from a url path, and a boolean. the boolean is true if we're returning what we expect; false if the
    148 // operation failed
    149 func GetURLPortion(req *http.Request, index int) (int, bool) {
    150 	var desiredID int
    151 	parts := strings.Split(strings.TrimSpace(req.URL.Path), "/")
    152 	if len(parts) < index || parts[index] == "" {
    153 		return -1, false
    154 	}
    155 	desiredID, err := strconv.Atoi(parts[index])
    156 	if err != nil {
    157 		return -1, false
    158 	}
    159 	return desiredID, true
    160 }
    161 
    162 func Capitalize(s string) string {
    163 	// utf8 safe capitalization
    164 	str := utf8string.NewString(s)
    165 	first := string(str.At(0))
    166 	rest := string(str.Slice(1, str.RuneCount()))
    167 	return strings.ToUpper(first) + rest
    168 }
    169 
    170 func CreateIfNotExist(docpath, content string) (bool, error) {
    171 	err := os.MkdirAll(filepath.Dir(docpath), 0750)
    172 	if err != nil {
    173 		return false, err
    174 	}
    175 	_, err = os.Stat(docpath)
    176 	if err != nil {
    177 		// if the file doesn't exist, create it
    178 		if errors.Is(err, os.ErrNotExist) {
    179 			err = os.WriteFile(docpath, []byte(content), 0777)
    180 			if err != nil {
    181 				return false, err
    182 			}
    183 			// created file successfully
    184 			return true, nil
    185 		} else {
    186 			return false, err
    187 		}
    188 	}
    189 	return false, nil
    190 }
    191 
    192 const solarYearSecs = 31556926
    193 
    194 func RelativeTime(t time.Time) string {
    195 	d := time.Since(t)
    196 	var metric string
    197 	var amount int
    198 	if d.Seconds() < 60 {
    199 		amount = int(d.Seconds())
    200 		metric = "second"
    201 	} else if d.Minutes() < 60 {
    202 		amount = int(d.Minutes())
    203 		metric = "minute"
    204 	} else if d.Hours() < 24 {
    205 		amount = int(d.Hours())
    206 		metric = "hour"
    207 	} else if d.Seconds() < solarYearSecs {
    208 		amount = int(d.Hours()) / 24
    209 		metric = "day"
    210 	} else {
    211 		amount = int(d.Seconds()) / solarYearSecs
    212 		metric = "year"
    213 	}
    214 	if amount == 1 {
    215 		return fmt.Sprintf("%d %s ago", amount, metric)
    216 	} else {
    217 		return fmt.Sprintf("%d %ss ago", amount, metric)
    218 	}
    219 }
    220 
    221 func ReadConfig(confpath string) types.Config {
    222 	ed := Describe("config")
    223 	_, err := CreateIfNotExist(confpath, defaults.DEFAULT_CONFIG)
    224 	ed.Check(err, "create default config")
    225 
    226 	data, err := os.ReadFile(confpath)
    227 	ed.Check(err, "read file")
    228 
    229 	var conf types.Config
    230 	decoder := json.NewDecoder(toml.New(bytes.NewBuffer(data)))
    231 
    232 	err = decoder.Decode(&conf)
    233 	ed.Check(err, "decode toml with json decoder")
    234 
    235 	return conf
    236 }
    237 
    238 func LoadFile(key, docpath, defaultContent string) ([]byte, error) {
    239 	ed := Describe("load file")
    240 	_, err := CreateIfNotExist(docpath, defaultContent)
    241 	err = ed.Eout(err, "create if not exist (%s) %s", key, docpath)
    242 	if err != nil {
    243 		return nil, err
    244 	}
    245 	data, err := os.ReadFile(docpath)
    246 	err = ed.Eout(err, "read %s", docpath)
    247 	if err != nil {
    248 		return nil, err
    249 	}
    250 	return data, nil
    251 }