url.go (6309B)
1 // Copyright 2015 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 helpers 15 16 import ( 17 "net/url" 18 "path" 19 "path/filepath" 20 "strings" 21 22 "github.com/gohugoio/hugo/common/paths" 23 24 "github.com/PuerkitoBio/purell" 25 ) 26 27 func sanitizeURLWithFlags(in string, f purell.NormalizationFlags) string { 28 s, err := purell.NormalizeURLString(in, f) 29 if err != nil { 30 return in 31 } 32 33 // Temporary workaround for the bug fix and resulting 34 // behavioral change in purell.NormalizeURLString(): 35 // a leading '/' was inadvertently added to relative links, 36 // but no longer, see #878. 37 // 38 // I think the real solution is to allow Hugo to 39 // make relative URL with relative path, 40 // e.g. "../../post/hello-again/", as wished by users 41 // in issues #157, #622, etc., without forcing 42 // relative URLs to begin with '/'. 43 // Once the fixes are in, let's remove this kludge 44 // and restore SanitizeURL() to the way it was. 45 // -- @anthonyfok, 2015-02-16 46 // 47 // Begin temporary kludge 48 u, err := url.Parse(s) 49 if err != nil { 50 panic(err) 51 } 52 if len(u.Path) > 0 && !strings.HasPrefix(u.Path, "/") { 53 u.Path = "/" + u.Path 54 } 55 return u.String() 56 // End temporary kludge 57 58 // return s 59 60 } 61 62 // SanitizeURL sanitizes the input URL string. 63 func SanitizeURL(in string) string { 64 return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveTrailingSlash|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator) 65 } 66 67 // SanitizeURLKeepTrailingSlash is the same as SanitizeURL, but will keep any trailing slash. 68 func SanitizeURLKeepTrailingSlash(in string) string { 69 return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator) 70 } 71 72 // URLize is similar to MakePath, but with Unicode handling 73 // Example: 74 // uri: Vim (text editor) 75 // urlize: vim-text-editor 76 func (p *PathSpec) URLize(uri string) string { 77 return p.URLEscape(p.MakePathSanitized(uri)) 78 } 79 80 // URLizeFilename creates an URL from a filename by escaping unicode letters 81 // and turn any filepath separator into forward slashes. 82 func (p *PathSpec) URLizeFilename(filename string) string { 83 return p.URLEscape(filepath.ToSlash(filename)) 84 } 85 86 // URLEscape escapes unicode letters. 87 func (p *PathSpec) URLEscape(uri string) string { 88 // escape unicode letters 89 parsedURI, err := url.Parse(uri) 90 if err != nil { 91 // if net/url can not parse URL it means Sanitize works incorrectly 92 panic(err) 93 } 94 x := parsedURI.String() 95 return x 96 } 97 98 // AbsURL creates an absolute URL from the relative path given and the BaseURL set in config. 99 func (p *PathSpec) AbsURL(in string, addLanguage bool) string { 100 url, err := url.Parse(in) 101 if err != nil { 102 return in 103 } 104 105 if url.IsAbs() || strings.HasPrefix(in, "//") { 106 // It is already absolute, return it as is. 107 return in 108 } 109 110 baseURL := p.getBaseURLRoot(in) 111 112 if addLanguage { 113 prefix := p.GetLanguagePrefix() 114 if prefix != "" { 115 hasPrefix := false 116 // avoid adding language prefix if already present 117 in2 := in 118 if strings.HasPrefix(in, "/") { 119 in2 = in[1:] 120 } 121 if in2 == prefix { 122 hasPrefix = true 123 } else { 124 hasPrefix = strings.HasPrefix(in2, prefix+"/") 125 } 126 127 if !hasPrefix { 128 addSlash := in == "" || strings.HasSuffix(in, "/") 129 in = path.Join(prefix, in) 130 131 if addSlash { 132 in += "/" 133 } 134 } 135 } 136 } 137 138 return paths.MakePermalink(baseURL, in).String() 139 } 140 141 func (p *PathSpec) getBaseURLRoot(path string) string { 142 if strings.HasPrefix(path, "/") { 143 // Treat it as relative to the server root. 144 return p.BaseURLNoPathString 145 } else { 146 // Treat it as relative to the baseURL. 147 return p.BaseURLString 148 } 149 } 150 151 func (p *PathSpec) RelURL(in string, addLanguage bool) string { 152 baseURL := p.getBaseURLRoot(in) 153 canonifyURLs := p.CanonifyURLs 154 if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") { 155 return in 156 } 157 158 u := in 159 160 if strings.HasPrefix(in, baseURL) { 161 u = strings.TrimPrefix(u, baseURL) 162 } 163 164 if addLanguage { 165 prefix := p.GetLanguagePrefix() 166 if prefix != "" { 167 hasPrefix := false 168 // avoid adding language prefix if already present 169 in2 := in 170 if strings.HasPrefix(in, "/") { 171 in2 = in[1:] 172 } 173 if in2 == prefix { 174 hasPrefix = true 175 } else { 176 hasPrefix = strings.HasPrefix(in2, prefix+"/") 177 } 178 179 if !hasPrefix { 180 hadSlash := strings.HasSuffix(u, "/") 181 182 u = path.Join(prefix, u) 183 184 if hadSlash { 185 u += "/" 186 } 187 } 188 } 189 } 190 191 if !canonifyURLs { 192 u = paths.AddContextRoot(baseURL, u) 193 } 194 195 if in == "" && !strings.HasSuffix(u, "/") && strings.HasSuffix(baseURL, "/") { 196 u += "/" 197 } 198 199 if !strings.HasPrefix(u, "/") { 200 u = "/" + u 201 } 202 203 return u 204 } 205 206 // PrependBasePath prepends any baseURL sub-folder to the given resource 207 func (p *PathSpec) PrependBasePath(rel string, isAbs bool) string { 208 basePath := p.GetBasePath(!isAbs) 209 if basePath != "" { 210 rel = filepath.ToSlash(rel) 211 // Need to prepend any path from the baseURL 212 hadSlash := strings.HasSuffix(rel, "/") 213 rel = path.Join(basePath, rel) 214 if hadSlash { 215 rel += "/" 216 } 217 } 218 return rel 219 } 220 221 // URLizeAndPrep applies misc sanitation to the given URL to get it in line 222 // with the Hugo standard. 223 func (p *PathSpec) URLizeAndPrep(in string) string { 224 return p.URLPrep(p.URLize(in)) 225 } 226 227 // URLPrep applies misc sanitation to the given URL. 228 func (p *PathSpec) URLPrep(in string) string { 229 if p.UglyURLs { 230 return paths.Uglify(SanitizeURL(in)) 231 } 232 pretty := paths.PrettifyURL(SanitizeURL(in)) 233 if path.Ext(pretty) == ".xml" { 234 return pretty 235 } 236 url, err := purell.NormalizeURLString(pretty, purell.FlagAddTrailingSlash) 237 if err != nil { 238 return pretty 239 } 240 return url 241 }