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 }