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