mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
feb3d494b9
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
199 lines
5.9 KiB
Go
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:])
|
|
}
|