diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index e83aacb8..f1a0cfae 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -30,6 +30,10 @@ var ( // allowed/denied lists. ErrURLNotAuthorized = errors.New("URL not authorized") + // ErrInvalidEvaluationExpression happens if an evaluation expression + // returns an exception or undefined. + ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression") + // ErrInvalidPrinterSettings happens if the Options have one or more // aberrant values. ErrInvalidPrinterSettings = errors.New("invalid printer settings") @@ -71,6 +75,11 @@ type Options struct { // Optional. WaitWindowStatus string + // WaitForExpression is the custom JavaScript expression to wait before + // converting an HTML document to PDF until it returns true + // Optional. + WaitForExpression string + // UserAgent overrides the default User-Agent header. // Optional. UserAgent string @@ -149,6 +158,7 @@ func DefaultOptions() Options { return Options{ WaitDelay: 0, WaitWindowStatus: "", + WaitForExpression: "", UserAgent: "", ExtraHTTPHeaders: nil, Landscape: false, @@ -454,42 +464,61 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath return nil }), chromedp.ActionFunc(func(ctx context.Context) error { - if options.WaitWindowStatus == "" { + if options.WaitWindowStatus == "" && options.WaitForExpression == "" { return nil } - // We wait until the evaluation of - // "window.status === options.WaitWindowStatus" is true or - // until the context is done. - logger.Debug(fmt.Sprintf("wait for window.status === '%s' before print", options.WaitWindowStatus)) + evaluate := func(expression string) error { + // We wait until the evaluation of the expression is true or + // until the context is done. + logger.Debug(fmt.Sprintf("wait until '%s' is true before print", expression)) - ticker := time.NewTicker(time.Duration(100) * time.Millisecond) + ticker := time.NewTicker(time.Duration(100) * time.Millisecond) - for { - select { - case <-ctx.Done(): - ticker.Stop() - - return fmt.Errorf("wait for window.status === '%s': %w", options.WaitWindowStatus, ctx.Err()) - case <-ticker.C: - var ok bool - - evaluate := chromedp.Evaluate(fmt.Sprintf("window.status === '%s'", options.WaitWindowStatus), &ok) - err := evaluate.Do(ctx) - - if err != nil { - return fmt.Errorf("evaluate: %w", err) - } - - if ok { + for { + select { + case <-ctx.Done(): ticker.Stop() - return nil - } + return fmt.Errorf("context done while evaluating '%s': %w", expression, ctx.Err()) + case <-ticker.C: + var ok bool - continue + evaluate := chromedp.Evaluate(expression, &ok) + err := evaluate.Do(ctx) + + if err != nil { + return fmt.Errorf("evaluate: %v: %w", err, ErrInvalidEvaluationExpression) + } + + if ok { + ticker.Stop() + + return nil + } + + continue + } } } + + if options.WaitWindowStatus != "" { + logger.Warn("option 'WaitWindowStatus' is deprecated; prefer 'WaitForExpression' instead") + + err := evaluate(fmt.Sprintf("window.status === '%s'", options.WaitWindowStatus)) + if err != nil { + return fmt.Errorf("wait for window.status === '%s': %w", options.WaitWindowStatus, err) + } + } + + if options.WaitForExpression != "" { + err := evaluate(options.WaitForExpression) + if err != nil { + return fmt.Errorf("wait for expression '%s': %w", options.WaitForExpression, err) + } + } + + return nil }), chromedp.ActionFunc(func(ctx context.Context) error { printToPDF := page.PrintToPDF(). diff --git a/pkg/modules/chromium/chromium_test.go b/pkg/modules/chromium/chromium_test.go index f63bd0f1..c8ee8507 100644 --- a/pkg/modules/chromium/chromium_test.go +++ b/pkg/modules/chromium/chromium_test.go @@ -285,6 +285,21 @@ func TestChromium_PDF(t *testing.T) { WaitWindowStatus: "ready", }, }, + { + timeout: time.Duration(3) * time.Second, + URL: "file:///tests/test/testdata/chromium/html/sample2/index.html", + options: Options{ + WaitForExpression: "window.status === 'foo'", + }, + expectErr: true, + }, + { + timeout: time.Duration(3) * time.Second, + URL: "file:///tests/test/testdata/chromium/html/sample2/index.html", + options: Options{ + WaitForExpression: "window.status === 'ready'", + }, + }, { URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", options: Options{ diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 2b59b77a..eec620da 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -28,6 +28,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) { var ( waitDelay time.Duration waitWindowStatus string + waitForExpression string userAgent string extraHTTPHeaders map[string]string landscape, printBackground bool @@ -41,6 +42,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) { form := ctx.FormData(). 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 == "" { @@ -73,6 +75,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) { options := Options{ WaitDelay: waitDelay, WaitWindowStatus: waitWindowStatus, + WaitForExpression: waitForExpression, UserAgent: userAgent, ExtraHTTPHeaders: extraHTTPHeaders, Landscape: landscape, @@ -291,6 +294,23 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL, ) } + if errors.Is(err, ErrInvalidEvaluationExpression) { + if options.WaitForExpression == "" { + // We do not expect the 'waitWindowStatus' form field to return + // an ErrInvalidEvaluationExpression error. In such a scenario, + // we return a 500. + return fmt.Errorf("convert to PDF: %w", err) + } + + return api.WrapError( + fmt.Errorf("convert to PDF: %w", err), + api.NewSentinelHTTPError( + http.StatusBadRequest, + fmt.Sprintf("The expression '%s' (waitForExpression) returned an exception or undefined", options.WaitForExpression), + ), + ) + } + if errors.Is(err, ErrInvalidPrinterSettings) { return api.WrapError( fmt.Errorf("convert to PDF: %w", err), diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go index a9c69be2..b218bee5 100644 --- a/pkg/modules/chromium/routes_test.go +++ b/pkg/modules/chromium/routes_test.go @@ -523,6 +523,7 @@ func TestConvertURL(t *testing.T) { api API engine gotenberg.PDFEngine PDFformat string + options Options expectErr bool expectHTTPErr bool expectHTTPStatus int @@ -538,10 +539,44 @@ func TestConvertURL(t *testing.T) { return chromiumAPI }(), + options: DefaultOptions(), expectErr: true, expectHTTPErr: true, expectHTTPStatus: http.StatusForbidden, }, + { + ctx: &api.MockContext{Context: &api.Context{}}, + api: func() API { + chromiumAPI := struct{ ProtoAPI }{} + chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { + return ErrInvalidEvaluationExpression + } + + return chromiumAPI + }(), + options: DefaultOptions(), + expectErr: true, + }, + { + ctx: &api.MockContext{Context: &api.Context{}}, + api: func() API { + chromiumAPI := struct{ ProtoAPI }{} + chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { + return ErrInvalidEvaluationExpression + } + + return chromiumAPI + }(), + options: func() Options { + options := DefaultOptions() + options.WaitForExpression = "foo" + + return options + }(), + expectErr: true, + expectHTTPErr: true, + expectHTTPStatus: http.StatusBadRequest, + }, { ctx: &api.MockContext{Context: &api.Context{}}, api: func() API { @@ -552,6 +587,7 @@ func TestConvertURL(t *testing.T) { return chromiumAPI }(), + options: DefaultOptions(), expectErr: true, expectHTTPErr: true, expectHTTPStatus: http.StatusBadRequest, @@ -566,6 +602,7 @@ func TestConvertURL(t *testing.T) { return chromiumAPI }(), + options: DefaultOptions(), expectErr: true, expectHTTPErr: true, expectHTTPStatus: http.StatusBadRequest, @@ -580,6 +617,7 @@ func TestConvertURL(t *testing.T) { return chromiumAPI }(), + options: DefaultOptions(), expectErr: true, }, { @@ -600,6 +638,7 @@ func TestConvertURL(t *testing.T) { } }(), PDFformat: "foo", + options: DefaultOptions(), expectErr: true, expectHTTPErr: true, expectHTTPStatus: http.StatusBadRequest, @@ -622,6 +661,7 @@ func TestConvertURL(t *testing.T) { } }(), PDFformat: "foo", + options: DefaultOptions(), expectErr: true, }, { @@ -642,6 +682,7 @@ func TestConvertURL(t *testing.T) { } }(), PDFformat: "foo", + options: DefaultOptions(), expectOutputPathsCount: 1, }, { @@ -659,6 +700,7 @@ func TestConvertURL(t *testing.T) { return chromiumAPI }(), + options: DefaultOptions(), expectErr: true, }, { @@ -671,10 +713,11 @@ func TestConvertURL(t *testing.T) { return chromiumAPI }(), + options: DefaultOptions(), expectOutputPathsCount: 1, }, } { - err := convertURL(tc.ctx.Context, tc.api, tc.engine, "", tc.PDFformat, DefaultOptions()) + err := convertURL(tc.ctx.Context, tc.api, tc.engine, "", tc.PDFformat, tc.options) if tc.expectErr && err == nil { t.Errorf("test %d: expected error but got: %v", i, err)