package chromium
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4"
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
"go.uber.org/multierr"
)
// FormDataChromiumPDFOptions creates Options form the form data. Fallback to
// default value if the considered key is not present.
func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
defaultOptions := DefaultOptions()
var (
failOnConsoleExceptions bool
waitDelay time.Duration
waitWindowStatus string
waitForExpression string
userAgent string
extraHTTPHeaders map[string]string
emulatedMediaType string
landscape, printBackground bool
scale, paperWidth, paperHeight float64
marginTop, marginBottom, marginLeft, marginRight float64
pageRanges string
headerTemplate, footerTemplate string
preferCSSPageSize bool
)
form := ctx.FormData().
Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions).
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
String("userAgent", &userAgent, defaultOptions.UserAgent).
Custom("extraHttpHeaders", func(value string) error {
if value == "" {
extraHTTPHeaders = defaultOptions.ExtraHTTPHeaders
return nil
}
err := json.Unmarshal([]byte(value), &extraHTTPHeaders)
if err != nil {
return fmt.Errorf("unmarshal extra HTTP headers: %w", err)
}
return nil
}).
Custom("emulatedMediaType", func(value string) error {
if value == "" {
emulatedMediaType = defaultOptions.EmulatedMediaType
return nil
}
if value != "screen" && value != "print" {
return fmt.Errorf("wrong value, expected either 'screen', 'print' or empty")
}
emulatedMediaType = value
return nil
}).
Bool("landscape", &landscape, defaultOptions.Landscape).
Bool("printBackground", &printBackground, defaultOptions.PrintBackground).
Float64("scale", &scale, defaultOptions.Scale).
Float64("paperWidth", &paperWidth, defaultOptions.PaperWidth).
Float64("paperHeight", &paperHeight, defaultOptions.PaperHeight).
Float64("marginTop", &marginTop, defaultOptions.MarginTop).
Float64("marginBottom", &marginBottom, defaultOptions.MarginBottom).
Float64("marginLeft", &marginLeft, defaultOptions.MarginLeft).
Float64("marginRight", &marginRight, defaultOptions.MarginRight).
String("nativePageRanges", &pageRanges, defaultOptions.PageRanges).
Content("header.html", &headerTemplate, defaultOptions.HeaderTemplate).
Content("footer.html", &footerTemplate, defaultOptions.FooterTemplate).
Bool("preferCssPageSize", &preferCSSPageSize, defaultOptions.PreferCSSPageSize)
options := Options{
FailOnConsoleExceptions: failOnConsoleExceptions,
WaitDelay: waitDelay,
WaitWindowStatus: waitWindowStatus,
WaitForExpression: waitForExpression,
UserAgent: userAgent,
ExtraHTTPHeaders: extraHTTPHeaders,
ExtraLinkTags: defaultOptions.ExtraLinkTags,
EmulatedMediaType: emulatedMediaType,
ExtraScriptTags: defaultOptions.ExtraScriptTags,
Landscape: landscape,
PrintBackground: printBackground,
Scale: scale,
PaperWidth: paperWidth,
PaperHeight: paperHeight,
MarginTop: marginTop,
MarginBottom: marginBottom,
MarginLeft: marginLeft,
MarginRight: marginRight,
PageRanges: pageRanges,
HeaderTemplate: headerTemplate,
FooterTemplate: footerTemplate,
PreferCSSPageSize: preferCSSPageSize,
}
return form, options
}
// convertURLRoute returns an api.Route which can convert a URL to PDF.
func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/chromium/convert/url",
IsMultipart: true,
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPDFOptions(ctx)
var (
URL string
PDFformat string
linkPaths []string
scriptPaths []string
)
err := form.
MandatoryString("url", &URL).
String("pdfFormat", &PDFformat, "").
Custom("extraLinkTags", func(value string) error {
if value == "" {
return nil
}
err := json.Unmarshal([]byte(value), &options.ExtraLinkTags)
if err != nil {
return fmt.Errorf("unmarshal extra link tags: %w", err)
}
return nil
}).
Custom("extraScriptTags", func(value string) error {
if value == "" {
return nil
}
err := json.Unmarshal([]byte(value), &options.ExtraScriptTags)
if err != nil {
return fmt.Errorf("unmarshal extra script tags: %w", err)
}
return nil
}).
Paths([]string{".woff2", ".woff", ".ttf", ".css"}, &linkPaths).
Paths([]string{".js"}, &scriptPaths).
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
// Thanks to Options.LinkTags and Options.ScriptTags, one may
// "hijack" the content of a remote HTML document (for instance, a
// website) by loading external assets like scripts or CSS
// stylesheets.
//
// There are two possibilities:
//
// 1. We auto-detect all files sent in the request that match the
// following file extensions: ".woff2", ".woff", ".ttf", ".css",
// ".css", and ".js".
//
// 2. The user has sent both files and a JSON mapping via the
// "extraLinkTags" and/or "extraScriptTags" form fields. In such a
// scenario, the JSON mapping has the priority for ordering, and
// files which are a not mapped are added at the end. The user may
// also have sent remote URLs in the JSON mapping.
// First, let's handle the HTML elements.
hasExtraLinkTags := len(options.ExtraLinkTags) > 0
hasLinkPaths := len(linkPaths) > 0
if !hasExtraLinkTags && hasLinkPaths {
// First scenario: there is no JSON mapping, we simply add the
// paths.
options.ExtraLinkTags = make([]LinkTag, len(linkPaths))
for i, path := range linkPaths {
options.ExtraLinkTags[i] = LinkTag{
Href: filepath.Base(path),
}
}
} else if hasExtraLinkTags && hasLinkPaths {
// Second scenario: there are both files and a JSON mapping.
// First, find the filenames of the files.
filenames := make([]string, len(linkPaths))
for i, path := range linkPaths {
filenames[i] = filepath.Base(path)
}
var extraLinkTags []LinkTag
// Then, let's find the filenames that exist in the JSON
// mapping, plus the entries that do only exist in the JSON
// mapping.
for _, linkTagFromMapping := range options.ExtraLinkTags {
found := false
for _, filename := range filenames {
if linkTagFromMapping.Href == filename {
extraLinkTags = append(extraLinkTags, linkTagFromMapping)
found = true
break
}
}
if !found {
// This entry only exist in the JSON mapping.
extraLinkTags = append(extraLinkTags, linkTagFromMapping)
}
}
// Then, add the remaining filenames.
for _, filename := range filenames {
found := false
for _, linkTag := range extraLinkTags {
if linkTag.Href == filename {
found = true
break
}
}
if !found {
extraLinkTags = append(extraLinkTags, LinkTag{
Href: filename,
})
}
}
// Last but not least, update the options.
options.ExtraLinkTags = extraLinkTags
}
// Next, let's handle the HTML