snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
package renderer
import "github.com/cortezaproject/corteza/server/pkg/valuestore"
func envGetter() func(k string) any {
return func(k string) any {
return valuestore.Global().Env(k)
}
}

View File

@@ -0,0 +1,108 @@
package renderer
import (
"bytes"
"context"
"io"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
genericHTML struct {
def DriverDefinition
}
genericHTMLDriver struct{}
)
func newGenericHTML() driverFactory {
return &genericHTML{
def: DriverDefinition{
Name: "genericHTML",
InputTypes: []types.DocumentType{
types.DocumentTypePlain,
types.DocumentTypeHTML,
},
OutputTypes: []types.DocumentType{
types.DocumentTypeHTML,
},
},
}
}
func (d *genericHTML) Define() DriverDefinition {
return d.def
}
func (d *genericHTML) CanRender(t types.DocumentType) bool {
for _, i := range d.def.InputTypes {
if i == t {
return true
}
}
return false
}
func (d *genericHTML) CanProduce(t types.DocumentType) bool {
for _, o := range d.def.OutputTypes {
if o == t {
return true
}
}
return false
}
func (d *genericHTML) Driver() driver {
return &genericHTMLDriver{}
}
func (d *genericHTMLDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
if pl.Header == nil && pl.Footer == nil {
t, err := preprocHTMLTemplate(pl)
if err != nil {
return nil, err
}
dd := &bytes.Buffer{}
err = t.Execute(dd, pl.Variables)
return bytes.NewReader(dd.Bytes()), err
}
// Header and footer are rendered with the same variables and joined with
// the content into a single document; partials are one-shot readers, so
// they are buffered to let each of the three templates parse its own copy
partials := make(map[string][]byte, len(pl.Partials))
for h, r := range pl.Partials {
bb, err := io.ReadAll(r)
if err != nil {
return nil, err
}
partials[h] = bb
}
dd := &bytes.Buffer{}
for _, src := range []io.Reader{pl.Header, pl.Template, pl.Footer} {
if src == nil {
continue
}
aux := *pl
aux.Template = src
aux.Partials = make(map[string]io.Reader, len(partials))
for h, bb := range partials {
aux.Partials[h] = bytes.NewReader(bb)
}
t, err := preprocHTMLTemplate(&aux)
if err != nil {
return nil, err
}
if err = t.Execute(dd, aux.Variables); err != nil {
return nil, err
}
}
return bytes.NewReader(dd.Bytes()), nil
}

View File

@@ -0,0 +1,103 @@
package renderer
import (
"bytes"
"context"
"io"
"strings"
"testing"
)
func TestGenericHTMLHeaderFooter(t *testing.T) {
d := &genericHTMLDriver{}
pl := &driverPayload{
Template: bytes.NewBufferString(`<main>{{template "pp" .}}</main>`),
Header: bytes.NewBufferString(`<header>{{.name}} {{template "pp" .}}</header>`),
Footer: bytes.NewBufferString(`<footer>{{.name}}</footer>`),
Variables: map[string]interface{}{"name": "Human"},
Partials: map[string]io.Reader{
"pp": bytes.NewBufferString(`{{define "pp"}}partial:{{.name}}{{end}}`),
},
}
out, err := d.Render(context.Background(), pl)
if err != nil {
t.Fatal(err)
}
bb, _ := io.ReadAll(out)
got := string(bb)
want := `<header>Human partial:Human</header><main>partial:Human</main><footer>Human</footer>`
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
// no header/footer keeps the single-pass path working
pl2 := &driverPayload{
Template: bytes.NewBufferString(`<main>ok</main>`),
Variables: map[string]interface{}{},
}
out2, err := d.Render(context.Background(), pl2)
if err != nil {
t.Fatal(err)
}
bb2, _ := io.ReadAll(out2)
if !strings.Contains(string(bb2), "<main>ok</main>") {
t.Fatalf("plain render broken: %q", string(bb2))
}
}
func TestGenericHTMLSafeHTML(t *testing.T) {
d := &genericHTMLDriver{}
rte := `<h1>Title 1</h1>`
pl := &driverPayload{
Template: bytes.NewBufferString(`<div>{{.body}}</div><div>{{.body | safeHTML}}</div>`),
Variables: map[string]interface{}{"body": rte},
}
out, err := d.Render(context.Background(), pl)
if err != nil {
t.Fatal(err)
}
bb, _ := io.ReadAll(out)
got := string(bb)
want := `<div>&lt;h1&gt;Title 1&lt;/h1&gt;</div><div><h1>Title 1</h1></div>`
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
// the case the partial workaround could not cover: rich text in a list
pl2 := &driverPayload{
Template: bytes.NewBufferString(`{{range .items}}<td>{{.desc | safeHTML}}</td>{{end}}`),
Variables: map[string]interface{}{"items": []map[string]any{{"desc": rte}, {"desc": rte}}},
}
out2, err := d.Render(context.Background(), pl2)
if err != nil {
t.Fatal(err)
}
bb2, _ := io.ReadAll(out2)
if got, want := string(bb2), `<td><h1>Title 1</h1></td><td><h1>Title 1</h1></td>`; got != want {
t.Fatalf("got %q, want %q", got, want)
}
// an unset or empty value must not break the render of the whole document
pl3 := &driverPayload{
Template: bytes.NewBufferString(`<td>{{.unset | safeHTML}}</td><td>{{.blank | safeHTML}}</td><td>{{.count | safeHTML}}</td>`),
Variables: map[string]interface{}{"blank": nil, "count": 3},
}
out3, err := d.Render(context.Background(), pl3)
if err != nil {
t.Fatal(err)
}
bb3, _ := io.ReadAll(out3)
if got, want := string(bb3), `<td></td><td></td><td>3</td>`; got != want {
t.Fatalf("got %q, want %q", got, want)
}
}

View File

@@ -0,0 +1,74 @@
package renderer
import (
"bytes"
"context"
"io"
"regexp"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
genericText struct {
def DriverDefinition
}
genericTextDriver struct{}
)
var (
plainTextRegex = regexp.MustCompile("text(/)?.*")
)
func newGenericText() driverFactory {
return &genericText{
def: DriverDefinition{
Name: "genericText",
InputTypes: []types.DocumentType{
types.DocumentTypePlain,
types.DocumentTypeHTML,
},
OutputTypes: []types.DocumentType{
types.DocumentTypePlain,
},
},
}
}
func (d *genericText) Define() DriverDefinition {
return d.def
}
func (d *genericText) CanRender(t types.DocumentType) bool {
for _, i := range d.def.InputTypes {
if i == t {
return true
}
}
return false
}
func (d *genericText) CanProduce(t types.DocumentType) bool {
for _, o := range d.def.OutputTypes {
if o == t {
return true
}
}
return false
}
func (d *genericText) Driver() driver {
return &genericTextDriver{}
}
func (d *genericTextDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
t, err := preprocPlainTemplate(pl.Template, pl.Partials)
if err != nil {
return nil, err
}
dd := &bytes.Buffer{}
err = t.Execute(dd, pl.Variables)
return bytes.NewReader(dd.Bytes()), err
}

View File

@@ -0,0 +1,387 @@
package renderer
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"strings"
"sync"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
gotenbergPDF struct {
url string
def DriverDefinition
}
gotenbergPDFDriver struct {
url string
}
)
// Gotenberg 7+ serves HTML conversion on the chromium module route, older (v6)
// deployments only expose /convert/html; the working route is detected on
// first render and cached per base URL
var gotenbergEndpoints = sync.Map{}
const (
gotenbergModernEndpoint = "/forms/chromium/convert/html"
gotenbergLegacyEndpoint = "/convert/html"
)
// @todo healthcheck, different input data formats
func newGotenbergPDF(url string) driverFactory {
return &gotenbergPDF{
url: url,
def: DriverDefinition{
Name: "gotenbergPDF",
InputTypes: []types.DocumentType{
types.DocumentTypePlain,
types.DocumentTypeHTML,
},
OutputTypes: []types.DocumentType{
types.DocumentTypePDF,
},
},
}
}
func (d *gotenbergPDF) Define() DriverDefinition {
return d.def
}
func (d *gotenbergPDF) CanRender(t types.DocumentType) bool {
for _, i := range d.def.InputTypes {
if i == t {
return true
}
}
return false
}
func (d *gotenbergPDF) CanProduce(t types.DocumentType) bool {
for _, o := range d.def.OutputTypes {
if o == t {
return true
}
}
return false
}
func (d *gotenbergPDF) Driver() driver {
return &gotenbergPDFDriver{
url: d.url,
}
}
func (d *gotenbergPDFDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
// HTTP request body stuff
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// index.html is required by the rendering container; Gotenberg 7+ requires
// the "files" form field name, v6 accepts any name
part, err := writer.CreateFormFile("files", "index.html")
if err != nil {
return nil, err
}
err = d.prepareContent(part, pl)
if err != nil {
return nil, err
}
// Optional header/footer documents rendered with the same variables/partials
if pl.Header != nil {
hPart, err := writer.CreateFormFile("files", "header.html")
if err != nil {
return nil, err
}
if err = d.prepareAux(hPart, pl, pl.Header); err != nil {
return nil, err
}
}
if pl.Footer != nil {
fPart, err := writer.CreateFormFile("files", "footer.html")
if err != nil {
return nil, err
}
if err = d.prepareAux(fPart, pl, pl.Footer); err != nil {
return nil, err
}
}
// Document configurations
err = d.applyOptions(writer, pl.Options)
if err != nil {
return nil, err
}
err = writer.Close()
if err != nil {
return nil, err
}
// HTTP request header stuff
// @todo make sure to use the propper endpoint when you add support
// for different inputs.
url := d.url
if n, has := pl.Options["url"]; has {
url = n
}
endpoints := []string{gotenbergModernEndpoint, gotenbergLegacyEndpoint}
if e, has := gotenbergEndpoints.Load(url); has && e == gotenbergLegacyEndpoint {
endpoints = []string{gotenbergLegacyEndpoint, gotenbergModernEndpoint}
}
var resp *http.Response
for _, e := range endpoints {
req, err := http.NewRequestWithContext(ctx, "POST", url+e, bytes.NewReader(body.Bytes()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// 404 means the route doesn't exist on this Gotenberg version; try the other one
if resp.StatusCode == http.StatusNotFound {
resp.Body.Close()
resp = nil
continue
}
gotenbergEndpoints.Store(url, e)
break
}
if resp == nil {
return nil, fmt.Errorf("gotenberg render failed: no compatible endpoint found on %s", url)
}
defer resp.Body.Close()
ss, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gotenberg render failed: %s: %s", resp.Status, strings.TrimSpace(string(ss)))
}
return bytes.NewReader(ss), nil
}
func (d *gotenbergPDFDriver) prepareContent(w io.Writer, pl *driverPayload) error {
t, err := preprocHTMLTemplate(pl)
if err != nil {
return err
}
return t.Execute(w, pl.Variables)
}
// prepareAux renders an auxiliary document (header/footer) with pl's variables
func (d *gotenbergPDFDriver) prepareAux(w io.Writer, pl *driverPayload, src io.Reader) error {
aux := *pl
aux.Template = src
return d.prepareContent(w, &aux)
}
func (d *gotenbergPDFDriver) applyOptions(mw *multipart.Writer, opts map[string]string) (err error) {
if opts == nil {
return nil
}
for k, v := range opts {
switch k {
case "marginTop",
"marginBottom",
"marginLeft",
"marginRight":
err = d.addFormField(mw, k, v)
case "marginY":
err = d.addFormField(mw, "marginTop", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginBottom", v)
case "marginX":
err = d.addFormField(mw, "marginLeft", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginRight", v)
case "margin":
err = d.addFormField(mw, "marginTop", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginBottom", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginLeft", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginRight", v)
case "documentSize":
w, h := d.documentDimensions(v)
if w+h != "" {
err = d.addFormField(mw, "paperWidth", w)
if err != nil {
return err
}
err = d.addFormField(mw, "paperHeight", h)
}
case "documentWidth":
err = d.addFormField(mw, "paperWidth", v)
case "documentHeight":
err = d.addFormField(mw, "paperHeight", v)
case "contentScale":
err = d.addFormField(mw, "scale", v)
case "orientation":
if v == "landscape" {
err = d.addFormField(mw, "landscape", "true")
} else {
err = d.addFormField(mw, "landscape", "false")
}
}
if err != nil {
return err
}
}
return nil
}
func (d *gotenbergPDFDriver) addFormField(mw *multipart.Writer, k, v string) error {
w, err := mw.CreateFormField(k)
if err != nil {
return err
}
_, err = w.Write([]byte(v))
return err
}
// documentDimensions returns the ISO216 standard document dimensions in inches (Gotenberg uses inches)
func (d *gotenbergPDFDriver) documentDimensions(isoDoc string) (string, string) {
switch strings.ToLower(isoDoc) {
// A series
case "a0":
return "33.1", "46.8"
case "a1":
return "23.4", "33.1"
case "a2":
return "16.5", "23.4"
case "a3":
return "11.7", "16.5"
case "a4":
return "8.3", "11.7"
case "a5":
return "5.8", "8.3"
case "a6":
return "4.1", "5.8"
case "a7":
return "2.9", "4.1"
case "a8":
return "2.0", "2.9"
case "a9":
return "1.5", "2.0"
case "a10":
return "1.0", "1.5"
// B series
case "b0":
return "39.4", "55.7"
case "b1":
return "27.8", "39.4"
case "b2":
return "19.7", "27.8"
case "b3":
return "13.9", "19.7"
case "b4":
return "9.8", "13.9"
case "b5":
return "6.9", "9.8"
case "b6":
return "4.9", "6.9"
case "b7":
return "3.5", "4.9"
case "b8":
return "2.4", "3.5"
case "b9":
return "1.7", "2.4"
case "b10":
return "1.2", "1.7"
// C series
case "c0":
return "36.1", "51.1"
case "c1":
return "25.5", "36.1"
case "c2":
return "18.0", "25.5"
case "c3":
return "12.8", "18.0"
case "c4":
return "9.0", "12.8"
case "c5":
return "6.4", "9.0"
case "c6":
return "4.5", "6.4"
case "c7":
return "3.2", "4.5"
case "c8":
return "2.2", "3.2"
case "c9":
return "1.6", "2.2"
case "c10":
return "1.1", "1.6"
// ANSI
case "ansi a":
return "8.5", "11"
case "ansi b":
return "11", "17"
case "ansi c":
return "17", "22"
case "ansi d":
return "22", "34"
case "ansi e":
return "34", "44"
// Proprietary NA
case "junior legal":
return "8", "5"
case "letter":
return "8.5", "11"
case "legal":
return "8.5", "14"
case "tabloid":
return "11", "17"
}
// This will fallback to the default (A4)
return "", ""
}

View File

@@ -0,0 +1,87 @@
package renderer
import (
"encoding/base64"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"github.com/Masterminds/sprig"
)
func preprocHTMLTemplate(pl *driverPayload) (*template.Template, error) {
bb, err := ioutil.ReadAll(pl.Template)
if err != nil {
return nil, err
}
gtpl := template.New("text/html_render").
Funcs(sprig.FuncMap()).
Funcs(template.FuncMap{
// "attachDataURL": func(name string) template.URL {
// // Find the attachment
// att, has := pl.Attachments[name]
// if !has {
// return template.URL(fmt.Sprintf("error: attachment not found: %s", name))
// }
// // Process source
// bb, err := ioutil.ReadAll(att.Source)
// if err != nil {
// return template.URL(fmt.Sprintf("error: %s", err.Error()))
// }
// return template.URL("data:" + att.Mime + ";base64," + base64.RawStdEncoding.EncodeToString(bb))
// },
"inlineRemote": func(url string) (template.URL, error) {
rsp, err := http.Get(url)
if err != nil {
return "", err
}
defer rsp.Body.Close()
bb, err := ioutil.ReadAll(rsp.Body)
if err != nil {
return "", err
}
raw := base64.RawStdEncoding.EncodeToString(bb)
return template.URL("data:" + rsp.Header.Get("Content-Type") + ";base64," + raw), nil
},
// emits the value as-is, without the contextual escaping
// html/template applies to every other interpolated value;
// unset values render empty, the same as plain interpolation
"safeHTML": func(v any) template.HTML {
if v == nil {
return ""
}
return template.HTML(fmt.Sprintf("%v", v))
},
"env": envGetter(),
})
// Prep the original template
t, err := gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
// Prep partials
for _, p := range pl.Partials {
bb, err = ioutil.ReadAll(p)
if err != nil {
return nil, err
}
t, err = gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
}
return t, nil
}

View File

@@ -0,0 +1,59 @@
package renderer
import (
"context"
"fmt"
"io"
"github.com/cortezaproject/corteza/server/pkg/options"
)
type (
renderer struct {
factories []driverFactory
}
)
func Renderer(cfg options.TemplateOpt) *renderer {
ff := make([]driverFactory, 0, 3)
ff = append(ff, newGenericText(), newGenericHTML())
if cfg.RendererGotenbergEnabled {
ff = append(ff, newGotenbergPDF(cfg.RendererGotenbergAddress))
}
return &renderer{
factories: ff,
}
}
func (r *renderer) Render(ctx context.Context, pl *RendererPayload) (io.ReadSeeker, error) {
for _, f := range r.factories {
if f.CanRender(pl.TemplateType) && f.CanProduce(pl.TargetType) {
pp := make(map[string]io.Reader)
for _, prt := range pl.Partials {
pp[prt.Handle] = prt.Template
}
dpl := &driverPayload{
Template: pl.Template,
Variables: pl.Variables,
Options: pl.Options,
Partials: pp,
Attachments: pl.Attachments,
Header: pl.Header,
Footer: pl.Footer,
}
return f.Driver().Render(ctx, dpl)
}
}
return nil, fmt.Errorf("rendering failed: driver not found")
}
func (r *renderer) Drivers() []DriverDefinition {
dd := make([]DriverDefinition, len(r.factories))
for i, f := range r.factories {
dd[i] = f.Define()
}
return dd
}

View File

@@ -0,0 +1,43 @@
package renderer
import (
"io"
"io/ioutil"
"text/template"
"github.com/Masterminds/sprig"
)
func preprocPlainTemplate(tpl io.Reader, pp map[string]io.Reader) (*template.Template, error) {
bb, err := ioutil.ReadAll(tpl)
if err != nil {
return nil, err
}
gtpl := template.New("text/plain_render").
Funcs(sprig.TxtFuncMap()).
Funcs(template.FuncMap{
"env": envGetter(),
})
// Prep the original template
t, err := gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
// Prep partials
for _, p := range pp {
bb, err = ioutil.ReadAll(p)
if err != nil {
return nil, err
}
t, err = gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
}
return t, nil
}

View File

@@ -0,0 +1,65 @@
package renderer
import (
"context"
"io"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
RendererPayload struct {
Template io.Reader
TemplateType types.DocumentType
TargetType types.DocumentType
Variables map[string]interface{}
Options map[string]string
Partials []*TemplatePartial
Attachments AttachmentIndex
// Optional header/footer documents, attached by drivers that support them
Header io.Reader
Footer io.Reader
}
TemplatePartial struct {
Handle string
Template io.Reader
TemplateType types.DocumentType
}
driverPayload struct {
Template io.Reader
Variables map[string]interface{}
Options map[string]string
Partials map[string]io.Reader
Attachments AttachmentIndex
Header io.Reader
Footer io.Reader
}
AttachmentIndex map[string]*Attachment
Attachment struct {
Source io.Reader
Mime string
Name string
}
DriverDefinition struct {
Name string `json:"name"`
InputTypes []types.DocumentType `json:"inputTypes"`
OutputTypes []types.DocumentType `json:"outputTypes"`
}
driverFactory interface {
Define() DriverDefinition
CanRender(t types.DocumentType) bool
CanProduce(t types.DocumentType) bool
Driver() driver
}
driver interface {
Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error)
}
)