Files
hugo/tpl/template.go
T
Bjørn Erik Pedersen 4d775cbe95 tpl/templates: Reject Defer inside partialCached
A partial's rendered output (placeholder included) is cached by
partialCached across rebuilds, but BuildState.DeferredExecutions
is reset every stage. On a fast-render rebuild the cached string
replays the placeholder while doDefer is not called this build,
leaving executeDeferredTemplates to panic with "deferred execution
with id ... not found".

Mark the ctx inside IncludeCached's body execution and have Defer
return a clear error if it sees the flag. Catches transitive cases
(partialCached -> partial -> Defer) via ctx propagation. Defer in
baseof.html and in a plain partial is unaffected.

Fixes #13492

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:17:42 +02:00

205 lines
6.0 KiB
Go

// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package tpl contains template functions and related types.
package tpl
import (
"context"
"slices"
"strings"
"sync"
"unicode"
"github.com/bep/helpers/contexthelpers"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
// Template is the common interface between text/template and html/template.
type Template interface {
Name() string
Prepare() (*texttemplate.Template, error)
}
// RenderingContext represents the currently rendered site/language.
type RenderingContext struct {
Site site
SiteOutIdx int
}
type (
contextKey uint8
)
const (
contextKeyDependencyManagerScopedProvider contextKey = iota
contextKeyDependencyScope
contextKeyPage
contextKeyIsInGoldmark
cntextKeyCurrentTemplateInfo
contextKeyPartialDecoratorIDStack
contextKeyIsInPartialCached
)
// Context manages values passed in the context to templates.
var Context = struct {
DependencyManagerScopedProvider contexthelpers.ContextDispatcher[identity.DependencyManagerScopedProvider]
GetDependencyManagerInCurrentScope func(context.Context) identity.Manager
DependencyScope contexthelpers.ContextDispatcher[int]
Page contexthelpers.ContextDispatcher[page]
IsInGoldmark contexthelpers.ContextDispatcher[bool]
CurrentTemplate contexthelpers.ContextDispatcher[*CurrentTemplateInfo]
PartialDecoratorIDStack contexthelpers.ContextDispatcher[*collections.Stack[*StringBool]]
IsInPartialCached contexthelpers.ContextDispatcher[bool]
}{
DependencyManagerScopedProvider: contexthelpers.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKeyDependencyManagerScopedProvider),
DependencyScope: contexthelpers.NewContextDispatcher[int](contextKeyDependencyScope),
Page: contexthelpers.NewContextDispatcher[page](contextKeyPage),
IsInGoldmark: contexthelpers.NewContextDispatcher[bool](contextKeyIsInGoldmark),
CurrentTemplate: contexthelpers.NewContextDispatcher[*CurrentTemplateInfo](cntextKeyCurrentTemplateInfo),
PartialDecoratorIDStack: contexthelpers.NewContextDispatcher[*collections.Stack[*StringBool]](contextKeyPartialDecoratorIDStack),
IsInPartialCached: contexthelpers.NewContextDispatcher[bool](contextKeyIsInPartialCached),
}
func init() {
Context.GetDependencyManagerInCurrentScope = func(ctx context.Context) identity.Manager {
idmsp := Context.DependencyManagerScopedProvider.Get(ctx)
if idmsp != nil {
return idmsp.GetDependencyManagerForScope(Context.DependencyScope.Get(ctx))
}
return nil
}
}
// StringBool is a helper struct to hold a string and a bool value.
type StringBool struct {
Str string
Bool bool
}
type page interface {
IsNode() bool
}
type site interface {
Language() *langs.Language
}
const (
// HugoDeferredTemplatePrefix is the prefix for placeholders for deferred templates.
HugoDeferredTemplatePrefix = "__hdeferred/"
// HugoDeferredTemplateSuffix is the suffix for placeholders for deferred templates.
HugoDeferredTemplateSuffix = "__d="
)
const hugoNewLinePlaceholder = "___hugonl_"
var stripHTMLReplacerPre = strings.NewReplacer("\n", " ", "</p>", hugoNewLinePlaceholder, "<br>", hugoNewLinePlaceholder, "<br />", hugoNewLinePlaceholder)
// StripHTML strips out all HTML tags in s.
func StripHTML(s string) string {
// Shortcut strings with no tags in them
if !strings.ContainsAny(s, "<>") {
return s
}
pre := stripHTMLReplacerPre.Replace(s)
preReplaced := pre != s
s = htmltemplate.StripTags(pre)
if preReplaced {
s = strings.ReplaceAll(s, hugoNewLinePlaceholder, "\n")
}
var wasSpace bool
b := bp.GetBuffer()
defer bp.PutBuffer(b)
for _, r := range s {
isSpace := unicode.IsSpace(r)
if !(isSpace && wasSpace) {
b.WriteRune(r)
}
wasSpace = isSpace
}
if b.Len() > 0 {
s = b.String()
}
return s
}
// DeferredExecution holds the template and data for a deferred execution.
type DeferredExecution struct {
Mu sync.Mutex
Ctx context.Context
TemplatePath string
Data any
Executed bool
Result string
}
type CurrentTemplateInfoOps interface {
CurrentTemplateInfoCommonOps
Base() CurrentTemplateInfoCommonOps
}
type CurrentTemplateInfoCommonOps interface {
// Template name.
Name() string
// Template source filename.
// Will be empty for internal templates.
Filename() string
}
// CurrentTemplateInfo as returned in templates.Current.
type CurrentTemplateInfo struct {
Parent *CurrentTemplateInfo
Level int
Key string
CurrentTemplateInfoOps
}
// CurrentTemplateInfos is a slice of CurrentTemplateInfo.
type CurrentTemplateInfos []*CurrentTemplateInfo
// Reverse creates a copy of the slice and reverses it.
func (c CurrentTemplateInfos) Reverse() CurrentTemplateInfos {
if len(c) == 0 {
return c
}
r := make(CurrentTemplateInfos, len(c))
copy(r, c)
slices.Reverse(r)
return r
}
// Ancestors returns the ancestors of the current template.
func (ti *CurrentTemplateInfo) Ancestors() CurrentTemplateInfos {
var ancestors []*CurrentTemplateInfo
for ti.Parent != nil {
ti = ti.Parent
ancestors = append(ancestors, ti)
}
return ancestors
}