github.com/cryptohub-digital/blockbook-fork@v0.0.0-20230713133354-673c927af7f1/server/html_templates.go (about)

     1  package server
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"html/template"
     7  	"math/big"
     8  	"net/http"
     9  	"runtime/debug"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/cryptohub-digital/blockbook-fork/api"
    15  	"github.com/cryptohub-digital/blockbook-fork/common"
    16  	"github.com/golang/glog"
    17  )
    18  
    19  type tpl int
    20  
    21  const (
    22  	noTpl = tpl(iota)
    23  	errorTpl
    24  	errorInternalTpl
    25  )
    26  
    27  // htmlTemplateHandler is a handle to public http server
    28  type htmlTemplates[TD any] struct {
    29  	metrics                  *common.Metrics
    30  	templates                []*template.Template
    31  	debug                    bool
    32  	newTemplateData          func(r *http.Request) *TD
    33  	newTemplateDataWithError func(error *api.APIError, r *http.Request) *TD
    34  	parseTemplates           func() []*template.Template
    35  	postHtmlTemplateHandler  func(data *TD, w http.ResponseWriter, r *http.Request)
    36  }
    37  
    38  func (s *htmlTemplates[TD]) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TD, error)) func(w http.ResponseWriter, r *http.Request) {
    39  	handlerName := getFunctionName(handler)
    40  	return func(w http.ResponseWriter, r *http.Request) {
    41  		var t tpl
    42  		var data *TD
    43  		var err error
    44  		defer func() {
    45  			if e := recover(); e != nil {
    46  				glog.Error(handlerName, " recovered from panic: ", e)
    47  				debug.PrintStack()
    48  				t = errorInternalTpl
    49  				if s.debug {
    50  					data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprint("Internal server error: recovered from panic ", e)}, r)
    51  				} else {
    52  					data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r)
    53  				}
    54  			}
    55  			// noTpl means the handler completely handled the request
    56  			if t != noTpl {
    57  				w.Header().Set("Content-Type", "text/html; charset=utf-8")
    58  				// return 500 Internal Server Error with errorInternalTpl
    59  				if t == errorInternalTpl {
    60  					w.WriteHeader(http.StatusInternalServerError)
    61  				}
    62  				if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil {
    63  					glog.Error(err)
    64  				}
    65  			}
    66  			if s.metrics != nil {
    67  				s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec()
    68  			}
    69  		}()
    70  		if s.metrics != nil {
    71  			s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc()
    72  		}
    73  		if s.debug {
    74  			// reload templates on each request
    75  			// to reflect changes during development
    76  			s.templates = s.parseTemplates()
    77  		}
    78  		t, data, err = handler(w, r)
    79  		if err != nil || (data == nil && t != noTpl) {
    80  			t = errorInternalTpl
    81  			if apiErr, ok := err.(*api.APIError); ok {
    82  				data = s.newTemplateDataWithError(apiErr, r)
    83  				if apiErr.Public {
    84  					t = errorTpl
    85  				}
    86  			} else {
    87  				if err != nil {
    88  					glog.Error(handlerName, " error: ", err)
    89  				}
    90  				if s.debug {
    91  					data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprintf("Internal server error: %v, data %+v", err, data)}, r)
    92  				} else {
    93  					data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r)
    94  				}
    95  			}
    96  		}
    97  		if s.postHtmlTemplateHandler != nil {
    98  			s.postHtmlTemplateHandler(data, w, r)
    99  		}
   100  
   101  	}
   102  }
   103  
   104  func relativeTimeUnit(d int64) string {
   105  	var u string
   106  	if d < 60 {
   107  		if d == 1 {
   108  			u = " sec"
   109  		} else {
   110  			u = " secs"
   111  		}
   112  	} else if d < 3600 {
   113  		d /= 60
   114  		if d == 1 {
   115  			u = " min"
   116  		} else {
   117  			u = " mins"
   118  		}
   119  	} else if d < 3600*24 {
   120  		d /= 3600
   121  		if d == 1 {
   122  			u = " hour"
   123  		} else {
   124  			u = " hours"
   125  		}
   126  	} else {
   127  		d /= 3600 * 24
   128  		if d == 1 {
   129  			u = " day"
   130  		} else {
   131  			u = " days"
   132  		}
   133  	}
   134  	return strconv.FormatInt(d, 10) + u
   135  }
   136  
   137  func relativeTime(d int64) string {
   138  	r := relativeTimeUnit(d)
   139  	if d > 3600*24 {
   140  		d = d % (3600 * 24)
   141  		if d >= 3600 {
   142  			r += " " + relativeTimeUnit(d)
   143  		}
   144  	} else if d > 3600 {
   145  		d = d % 3600
   146  		if d >= 60 {
   147  			r += " " + relativeTimeUnit(d)
   148  		}
   149  	}
   150  	return r
   151  }
   152  
   153  func unixTimeSpan(ut int64) template.HTML {
   154  	t := time.Unix(ut, 0)
   155  	return timeSpan(&t)
   156  }
   157  
   158  var timeNow = time.Now
   159  
   160  func timeSpan(t *time.Time) template.HTML {
   161  	if t == nil {
   162  		return ""
   163  	}
   164  	u := t.Unix()
   165  	if u <= 0 {
   166  		return ""
   167  	}
   168  	d := timeNow().Unix() - u
   169  	f := t.UTC().Format("2006-01-02 15:04:05")
   170  	if d < 0 {
   171  		return template.HTML(f)
   172  	}
   173  	r := relativeTime(d)
   174  	return template.HTML(`<span tt="` + f + `">` + r + " ago</span>")
   175  }
   176  
   177  func toJSON(data interface{}) string {
   178  	json, err := json.Marshal(data)
   179  	if err != nil {
   180  		return ""
   181  	}
   182  	return string(json)
   183  }
   184  
   185  func formatAmountWithDecimals(a *api.Amount, d int) string {
   186  	if a == nil {
   187  		return "0"
   188  	}
   189  	return a.DecimalString(d)
   190  }
   191  
   192  func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) {
   193  	rv.WriteString(`<span`)
   194  	if class != "" {
   195  		rv.WriteString(` class="`)
   196  		rv.WriteString(class)
   197  		rv.WriteString(`"`)
   198  	}
   199  	if txDate != "" {
   200  		rv.WriteString(` tm="`)
   201  		rv.WriteString(txDate)
   202  		rv.WriteString(`"`)
   203  	}
   204  	rv.WriteString(">")
   205  	i := strings.IndexByte(amount, '.')
   206  	if i < 0 {
   207  		appendSeparatedNumberSpans(rv, amount, "nc")
   208  	} else {
   209  		appendSeparatedNumberSpans(rv, amount[:i], "nc")
   210  		rv.WriteString(`.`)
   211  		rv.WriteString(`<span class="amt-dec">`)
   212  		appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns")
   213  		rv.WriteString("</span>")
   214  	}
   215  	if shortcut != "" {
   216  		rv.WriteString(" ")
   217  		rv.WriteString(shortcut)
   218  	}
   219  	rv.WriteString("</span>")
   220  }
   221  
   222  func appendAmountSpanBitcoinType(rv *strings.Builder, class, amount, shortcut, txDate string) {
   223  	if amount == "0" {
   224  		appendAmountSpan(rv, class, amount, shortcut, txDate)
   225  		return
   226  	}
   227  	rv.WriteString(`<span`)
   228  	if class != "" {
   229  		rv.WriteString(` class="`)
   230  		rv.WriteString(class)
   231  		rv.WriteString(`"`)
   232  	}
   233  	if txDate != "" {
   234  		rv.WriteString(` tm="`)
   235  		rv.WriteString(txDate)
   236  		rv.WriteString(`"`)
   237  	}
   238  	rv.WriteString(">")
   239  	i := strings.IndexByte(amount, '.')
   240  	var decimals string
   241  	if i < 0 {
   242  		appendSeparatedNumberSpans(rv, amount, "nc")
   243  		decimals = "00000000"
   244  	} else {
   245  		appendSeparatedNumberSpans(rv, amount[:i], "nc")
   246  		decimals = amount[i+1:] + "00000000"
   247  	}
   248  	rv.WriteString(`.`)
   249  	rv.WriteString(`<span class="amt-dec">`)
   250  	rv.WriteString(decimals[:2])
   251  	rv.WriteString(`<span class="ns">`)
   252  	rv.WriteString(decimals[2:5])
   253  	rv.WriteString("</span>")
   254  	rv.WriteString(`<span class="ns">`)
   255  	rv.WriteString(decimals[5:8])
   256  	rv.WriteString("</span>")
   257  	rv.WriteString("</span>")
   258  	if shortcut != "" {
   259  		rv.WriteString(" ")
   260  		rv.WriteString(shortcut)
   261  	}
   262  	rv.WriteString("</span>")
   263  }
   264  
   265  func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) {
   266  	rv.WriteString(`<span class="amt`)
   267  	if classes != "" {
   268  		rv.WriteString(` `)
   269  		rv.WriteString(classes)
   270  	}
   271  	rv.WriteString(`" cc="`)
   272  	rv.WriteString(primary)
   273  	rv.WriteString(" ")
   274  	rv.WriteString(symbol)
   275  	rv.WriteString(`">`)
   276  }
   277  
   278  func formatInt(i int) template.HTML {
   279  	return formatInt64(int64(i))
   280  }
   281  
   282  func formatUint32(i uint32) template.HTML {
   283  	return formatInt64(int64(i))
   284  }
   285  
   286  func appendSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) {
   287  	if len(s) > 0 && s[0] == '-' {
   288  		s = s[1:]
   289  		rv.WriteByte('-')
   290  	}
   291  	t := (len(s) - 1) / 3
   292  	if t <= 0 {
   293  		rv.WriteString(s)
   294  	} else {
   295  		t *= 3
   296  		rv.WriteString(s[:len(s)-t])
   297  		for i := len(s) - t; i < len(s); i += 3 {
   298  			rv.WriteString(`<span class="`)
   299  			rv.WriteString(separatorClass)
   300  			rv.WriteString(`">`)
   301  			rv.WriteString(s[i : i+3])
   302  			rv.WriteString("</span>")
   303  		}
   304  	}
   305  }
   306  
   307  func appendLeftSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) {
   308  	l := len(s)
   309  	if l <= 3 {
   310  		rv.WriteString(s)
   311  	} else {
   312  		rv.WriteString(s[:3])
   313  		for i := 3; i < len(s); i += 3 {
   314  			rv.WriteString(`<span class="`)
   315  			rv.WriteString(separatorClass)
   316  			rv.WriteString(`">`)
   317  			e := i + 3
   318  			if e > l {
   319  				e = l
   320  			}
   321  			rv.WriteString(s[i:e])
   322  			rv.WriteString("</span>")
   323  		}
   324  	}
   325  }
   326  
   327  func formatInt64(i int64) template.HTML {
   328  	s := strconv.FormatInt(i, 10)
   329  	var rv strings.Builder
   330  	appendSeparatedNumberSpans(&rv, s, "ns")
   331  	return template.HTML(rv.String())
   332  }
   333  
   334  func formatBigInt(i *big.Int) template.HTML {
   335  	if i == nil {
   336  		return ""
   337  	}
   338  	s := i.String()
   339  	var rv strings.Builder
   340  	appendSeparatedNumberSpans(&rv, s, "ns")
   341  	return template.HTML(rv.String())
   342  }