mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 12:42:16 +01:00
feat(chromium): add 'waitForExpression' form field, deprecate 'waitWindowStatus' form field
This commit is contained in:
@@ -30,6 +30,10 @@ var (
|
|||||||
// allowed/denied lists.
|
// allowed/denied lists.
|
||||||
ErrURLNotAuthorized = errors.New("URL not authorized")
|
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
|
// ErrInvalidPrinterSettings happens if the Options have one or more
|
||||||
// aberrant values.
|
// aberrant values.
|
||||||
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
|
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
|
||||||
@@ -71,6 +75,11 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
WaitWindowStatus string
|
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.
|
// UserAgent overrides the default User-Agent header.
|
||||||
// Optional.
|
// Optional.
|
||||||
UserAgent string
|
UserAgent string
|
||||||
@@ -149,6 +158,7 @@ func DefaultOptions() Options {
|
|||||||
return Options{
|
return Options{
|
||||||
WaitDelay: 0,
|
WaitDelay: 0,
|
||||||
WaitWindowStatus: "",
|
WaitWindowStatus: "",
|
||||||
|
WaitForExpression: "",
|
||||||
UserAgent: "",
|
UserAgent: "",
|
||||||
ExtraHTTPHeaders: nil,
|
ExtraHTTPHeaders: nil,
|
||||||
Landscape: false,
|
Landscape: false,
|
||||||
@@ -454,42 +464,61 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
|
|||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
if options.WaitWindowStatus == "" {
|
if options.WaitWindowStatus == "" && options.WaitForExpression == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// We wait until the evaluation of
|
evaluate := func(expression string) error {
|
||||||
// "window.status === options.WaitWindowStatus" is true or
|
// We wait until the evaluation of the expression is true or
|
||||||
// until the context is done.
|
// until the context is done.
|
||||||
logger.Debug(fmt.Sprintf("wait for window.status === '%s' before print", options.WaitWindowStatus))
|
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 {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
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 {
|
|
||||||
ticker.Stop()
|
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 {
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
printToPDF := page.PrintToPDF().
|
printToPDF := page.PrintToPDF().
|
||||||
|
|||||||
@@ -285,6 +285,21 @@ func TestChromium_PDF(t *testing.T) {
|
|||||||
WaitWindowStatus: "ready",
|
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",
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
options: Options{
|
options: Options{
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
var (
|
var (
|
||||||
waitDelay time.Duration
|
waitDelay time.Duration
|
||||||
waitWindowStatus string
|
waitWindowStatus string
|
||||||
|
waitForExpression string
|
||||||
userAgent string
|
userAgent string
|
||||||
extraHTTPHeaders map[string]string
|
extraHTTPHeaders map[string]string
|
||||||
landscape, printBackground bool
|
landscape, printBackground bool
|
||||||
@@ -41,6 +42,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
form := ctx.FormData().
|
form := ctx.FormData().
|
||||||
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
||||||
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
||||||
|
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
||||||
String("userAgent", &userAgent, defaultOptions.UserAgent).
|
String("userAgent", &userAgent, defaultOptions.UserAgent).
|
||||||
Custom("extraHttpHeaders", func(value string) error {
|
Custom("extraHttpHeaders", func(value string) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
@@ -73,6 +75,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
options := Options{
|
options := Options{
|
||||||
WaitDelay: waitDelay,
|
WaitDelay: waitDelay,
|
||||||
WaitWindowStatus: waitWindowStatus,
|
WaitWindowStatus: waitWindowStatus,
|
||||||
|
WaitForExpression: waitForExpression,
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
ExtraHTTPHeaders: extraHTTPHeaders,
|
ExtraHTTPHeaders: extraHTTPHeaders,
|
||||||
Landscape: landscape,
|
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) {
|
if errors.Is(err, ErrInvalidPrinterSettings) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
|
|||||||
@@ -523,6 +523,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
api API
|
api API
|
||||||
engine gotenberg.PDFEngine
|
engine gotenberg.PDFEngine
|
||||||
PDFformat string
|
PDFformat string
|
||||||
|
options Options
|
||||||
expectErr bool
|
expectErr bool
|
||||||
expectHTTPErr bool
|
expectHTTPErr bool
|
||||||
expectHTTPStatus int
|
expectHTTPStatus int
|
||||||
@@ -538,10 +539,44 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusForbidden,
|
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{}},
|
ctx: &api.MockContext{Context: &api.Context{}},
|
||||||
api: func() API {
|
api: func() API {
|
||||||
@@ -552,6 +587,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
@@ -566,6 +602,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
@@ -580,6 +617,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -600,6 +638,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
PDFformat: "foo",
|
PDFformat: "foo",
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
@@ -622,6 +661,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
PDFformat: "foo",
|
PDFformat: "foo",
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -642,6 +682,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
PDFformat: "foo",
|
PDFformat: "foo",
|
||||||
|
options: DefaultOptions(),
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -659,6 +700,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -671,10 +713,11 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectOutputPathsCount: 1,
|
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 {
|
if tc.expectErr && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user