alias.go (4972B)
1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package hugolib 15 16 import ( 17 "bytes" 18 "errors" 19 "fmt" 20 "io" 21 "path" 22 "path/filepath" 23 "runtime" 24 "strings" 25 26 "github.com/gohugoio/hugo/common/loggers" 27 28 "github.com/gohugoio/hugo/output" 29 "github.com/gohugoio/hugo/publisher" 30 "github.com/gohugoio/hugo/resources/page" 31 "github.com/gohugoio/hugo/tpl" 32 ) 33 34 type aliasHandler struct { 35 t tpl.TemplateHandler 36 log loggers.Logger 37 allowRoot bool 38 } 39 40 func newAliasHandler(t tpl.TemplateHandler, l loggers.Logger, allowRoot bool) aliasHandler { 41 return aliasHandler{t, l, allowRoot} 42 } 43 44 type aliasPage struct { 45 Permalink string 46 page.Page 47 } 48 49 func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, error) { 50 var templ tpl.Template 51 var found bool 52 53 templ, found = a.t.Lookup("alias.html") 54 if !found { 55 // TODO(bep) consolidate 56 templ, found = a.t.Lookup("_internal/alias.html") 57 if !found { 58 return nil, errors.New("no alias template found") 59 } 60 } 61 62 data := aliasPage{ 63 permalink, 64 p, 65 } 66 67 buffer := new(bytes.Buffer) 68 err := a.t.Execute(templ, buffer, data) 69 if err != nil { 70 return nil, err 71 } 72 return buffer, nil 73 } 74 75 func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format, p page.Page) (err error) { 76 return s.publishDestAlias(false, path, permalink, outputFormat, p) 77 } 78 79 func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) { 80 handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot) 81 82 s.Log.Debugln("creating alias:", path, "redirecting to", permalink) 83 84 targetPath, err := handler.targetPathAlias(path) 85 if err != nil { 86 return err 87 } 88 89 aliasContent, err := handler.renderAlias(permalink, p) 90 if err != nil { 91 return err 92 } 93 94 pd := publisher.Descriptor{ 95 Src: aliasContent, 96 TargetPath: targetPath, 97 StatCounter: &s.PathSpec.ProcessingStats.Aliases, 98 OutputFormat: outputFormat, 99 } 100 101 if s.Info.relativeURLs || s.Info.canonifyURLs { 102 pd.AbsURLPath = s.absURLPath(targetPath) 103 } 104 105 return s.publisher.Publish(pd) 106 } 107 108 func (a aliasHandler) targetPathAlias(src string) (string, error) { 109 originalAlias := src 110 if len(src) <= 0 { 111 return "", fmt.Errorf("alias \"\" is an empty string") 112 } 113 114 alias := path.Clean(filepath.ToSlash(src)) 115 116 if !a.allowRoot && alias == "/" { 117 return "", fmt.Errorf("alias \"%s\" resolves to website root directory", originalAlias) 118 } 119 120 components := strings.Split(alias, "/") 121 122 // Validate against directory traversal 123 if components[0] == ".." { 124 return "", fmt.Errorf("alias \"%s\" traverses outside the website root directory", originalAlias) 125 } 126 127 // Handle Windows file and directory naming restrictions 128 // See "Naming Files, Paths, and Namespaces" on MSDN 129 // https://msdn.microsoft.com/en-us/library/aa365247%28v=VS.85%29.aspx?f=255&MSPPError=-2147217396 130 msgs := []string{} 131 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"} 132 133 if strings.ContainsAny(alias, ":*?\"<>|") { 134 msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains invalid characters on Windows: : * ? \" < > |", originalAlias)) 135 } 136 for _, ch := range alias { 137 if ch < ' ' { 138 msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains ASCII control code (0x00 to 0x1F), invalid on Windows: : * ? \" < > |", originalAlias)) 139 continue 140 } 141 } 142 for _, comp := range components { 143 if strings.HasSuffix(comp, " ") || strings.HasSuffix(comp, ".") { 144 msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with a trailing space or period, problematic on Windows", originalAlias)) 145 } 146 for _, r := range reservedNames { 147 if comp == r { 148 msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with reserved name \"%s\" on Windows", originalAlias, r)) 149 } 150 } 151 } 152 if len(msgs) > 0 { 153 if runtime.GOOS == "windows" { 154 for _, m := range msgs { 155 a.log.Errorln(m) 156 } 157 return "", fmt.Errorf("cannot create \"%s\": Windows filename restriction", originalAlias) 158 } 159 for _, m := range msgs { 160 a.log.Infoln(m) 161 } 162 } 163 164 // Add the final touch 165 alias = strings.TrimPrefix(alias, "/") 166 if strings.HasSuffix(alias, "/") { 167 alias = alias + "index.html" 168 } else if !strings.HasSuffix(alias, ".html") { 169 alias = alias + "/" + "index.html" 170 } 171 172 return filepath.FromSlash(alias), nil 173 }