Files
hugo/hugolib/alias.go
T
hexbinoct feb3d494b9 hugolib: Use the output format's suffix for alias paths
Alias generation hardcoded "index.html" and only recognized a ".html"
suffix, so an alias with an explicit ".htm" extension (or any other
suffix configured for the html media type) was written to the wrong
path. For example "d.htm" produced the directory "d.htm/index.html"
instead of the file "d.htm".

Build the alias file name from the output format's BaseName and the
media type's first suffix, and detect an explicit file by matching the
alias extension against the configured suffixes. The same check now
drives the uglyURLs case in Aliases.

Fixes #15066
2026-07-04 14:31:34 +02:00

199 lines
5.9 KiB
Go

// Copyright 2019 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 hugolib
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/tpl/tplimpl"
)
type aliasHandler struct {
ts *tplimpl.TemplateStore
log loggers.Logger
allowRoot bool
}
func newAliasHandler(ts *tplimpl.TemplateStore, l loggers.Logger, allowRoot bool) aliasHandler {
return aliasHandler{ts, l, allowRoot}
}
type aliasPage struct {
Permalink string
page.Page
}
func (a aliasHandler) renderAlias(permalink string, p page.Page, matrix sitesmatrix.VectorProvider) (io.Reader, error) {
var templateDesc tplimpl.TemplateDescriptor
var base string = ""
if ps, ok := p.(*pageState); ok {
base, templateDesc = ps.GetInternalTemplateBasePathAndDescriptor()
}
templateDesc.LayoutFromUser = ""
templateDesc.Kind = ""
templateDesc.OutputFormat = output.AliasHTMLFormat.Name
templateDesc.MediaType = output.AliasHTMLFormat.MediaType.Type
q := tplimpl.TemplateQuery{
Path: base,
Category: tplimpl.CategoryLayout,
Desc: templateDesc,
Sites: matrix,
}
t := a.ts.LookupPagesLayout(q)
if t == nil {
return nil, errors.New("no alias template found")
}
if p == nil {
p = page.NopPage
}
data := aliasPage{
permalink,
p,
}
ctx := a.ts.PrepareTopLevelRenderCtx(context.Background(), p)
buffer := new(bytes.Buffer)
err := a.ts.ExecuteWithContext(ctx, t, buffer, data)
if err != nil {
return nil, err
}
return buffer, nil
}
func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format, p page.Page) (err error) {
return s.publishDestAlias(false, path, permalink, outputFormat, p)
}
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
handler := newAliasHandler(s.GetTemplateStore(), s.Log, allowRoot)
targetPath, err := handler.targetPathAlias(path, outputFormat)
if err != nil {
return err
}
aliasContent, err := handler.renderAlias(permalink, p, s.siteVector)
if err != nil {
return err
}
pd := publisher.Descriptor{
Src: aliasContent,
TargetPath: targetPath,
StatCounter: &s.PathSpec.ProcessingStats.Aliases,
OutputFormat: outputFormat,
}
if s.conf.RelativeURLs || s.conf.CanonifyURLs {
pd.AbsURLPath = s.absURLPath(targetPath)
}
return s.publisher.Publish(pd)
}
func (a aliasHandler) targetPathAlias(src string, of output.Format) (string, error) {
originalAlias := src
if len(src) <= 0 {
return "", fmt.Errorf("alias \"\" is an empty string")
}
alias := path.Clean(filepath.ToSlash(src))
if !a.allowRoot && alias == "/" {
return "", fmt.Errorf("alias \"%s\" resolves to website root directory", originalAlias)
}
components := strings.Split(alias, "/")
// Validate against directory traversal
if components[0] == ".." {
return "", fmt.Errorf("alias \"%s\" traverses outside the website root directory", originalAlias)
}
// Handle Windows file and directory naming restrictions
// See "Naming Files, Paths, and Namespaces" on MSDN
// https://msdn.microsoft.com/en-us/library/aa365247%28v=VS.85%29.aspx?f=255&MSPPError=-2147217396
msgs := []string{}
reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}
if strings.ContainsAny(alias, ":*?\"<>|") {
msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains invalid characters on Windows: : * ? \" < > |", originalAlias))
}
for _, ch := range alias {
if ch < ' ' {
msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains ASCII control code (0x00 to 0x1F), invalid on Windows: : * ? \" < > |", originalAlias))
continue
}
}
for _, comp := range components {
if strings.HasSuffix(comp, " ") || strings.HasSuffix(comp, ".") {
msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with a trailing space or period, problematic on Windows", originalAlias))
}
for _, r := range reservedNames {
if comp == r {
msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with reserved name \"%s\" on Windows", originalAlias, r))
}
}
}
if len(msgs) > 0 {
if runtime.GOOS == "windows" {
for _, m := range msgs {
a.log.Errorln(m)
}
return "", fmt.Errorf("cannot create \"%s\": Windows filename restriction", originalAlias)
}
for _, m := range msgs {
a.log.Infoln(m)
}
}
// Add the final touch. When the alias does not already end in one of the
// output format's configured suffixes, treat it as a directory and append
// the format's base name and suffix (e.g. index.html).
alias = strings.TrimPrefix(alias, "/")
baseFile := of.BaseName + of.MediaType.FirstSuffix.FullSuffix
if strings.HasSuffix(alias, "/") {
alias = alias + baseFile
} else if !pathHasOutputFormatSuffix(alias, of) {
alias = alias + "/" + baseFile
}
return filepath.FromSlash(alias), nil
}
// pathHasOutputFormatSuffix reports whether the last element of p ends in one
// of the suffixes configured for the output format's media type.
func pathHasOutputFormatSuffix(p string, of output.Format) bool {
ext := path.Ext(p)
return ext != "" && of.MediaType.HasSuffix(ext[1:])
}