Fix manifest encoding (#14114)
The previous URL encoding would encode spaces to '+' for the app name which is incorrect. Use base64 encoding instead which does not have such issues.
This commit is contained in:
		
							parent
							
								
									e0c753e770
								
							
						
					
					
						commit
						cd5278a44c
					
				
					 3 changed files with 105 additions and 10 deletions
				
			
		|  | @ -7,8 +7,8 @@ package setting | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html/template" |  | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"math" | 	"math" | ||||||
|  | @ -104,6 +104,7 @@ var ( | ||||||
| 	GracefulHammerTime   time.Duration | 	GracefulHammerTime   time.Duration | ||||||
| 	StartupTimeout       time.Duration | 	StartupTimeout       time.Duration | ||||||
| 	StaticURLPrefix      string | 	StaticURLPrefix      string | ||||||
|  | 	AbsoluteAssetURL     string | ||||||
| 
 | 
 | ||||||
| 	SSH = struct { | 	SSH = struct { | ||||||
| 		Disabled                       bool              `ini:"DISABLE_SSH"` | 		Disabled                       bool              `ini:"DISABLE_SSH"` | ||||||
|  | @ -294,7 +295,7 @@ var ( | ||||||
| 	CSRFCookieName     = "_csrf" | 	CSRFCookieName     = "_csrf" | ||||||
| 	CSRFCookieHTTPOnly = true | 	CSRFCookieHTTPOnly = true | ||||||
| 
 | 
 | ||||||
| 	ManifestData template.URL | 	ManifestData string | ||||||
| 
 | 
 | ||||||
| 	// Mirror settings
 | 	// Mirror settings
 | ||||||
| 	Mirror struct { | 	Mirror struct { | ||||||
|  | @ -600,6 +601,11 @@ func NewContext() { | ||||||
| 		Domain = urlHostname | 		Domain = urlHostname | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	AbsoluteAssetURL = MakeAbsoluteAssetURL(AppURL, StaticURLPrefix) | ||||||
|  | 
 | ||||||
|  | 	manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL) | ||||||
|  | 	ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes) | ||||||
|  | 
 | ||||||
| 	var defaultLocalURL string | 	var defaultLocalURL string | ||||||
| 	switch Protocol { | 	switch Protocol { | ||||||
| 	case UnixSocket: | 	case UnixSocket: | ||||||
|  | @ -645,8 +651,6 @@ func NewContext() { | ||||||
| 		LandingPageURL = LandingPageHome | 		LandingPageURL = LandingPageHome | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ManifestData = makeManifestData() |  | ||||||
| 
 |  | ||||||
| 	if len(SSH.Domain) == 0 { | 	if len(SSH.Domain) == 0 { | ||||||
| 		SSH.Domain = Domain | 		SSH.Domain = Domain | ||||||
| 	} | 	} | ||||||
|  | @ -1045,12 +1049,74 @@ func loadOrGenerateInternalToken(sec *ini.Section) string { | ||||||
| 	return token | 	return token | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func makeManifestData() template.URL { | // MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash
 | ||||||
| 	name := url.QueryEscape(AppName) | func MakeAbsoluteAssetURL(appURL string, staticURLPrefix string) string { | ||||||
| 	prefix := url.QueryEscape(StaticURLPrefix) | 	parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/")) | ||||||
| 	subURL := url.QueryEscape(AppSubURL) + "/" | 	if err != nil { | ||||||
|  | 		log.Fatal("Unable to parse STATIC_URL_PREFIX: %v", err) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	return template.URL(`data:application/json,{"short_name":"` + name + `","name":"` + name + `","icons":[{"src":"` + prefix + `/img/logo-lg.png","type":"image/png","sizes":"880x880"},{"src":"` + prefix + `/img/logo-sm.png","type":"image/png","sizes":"120x120"},{"src":"` + prefix + `/img/logo-512.png","type":"image/png","sizes":"512x512"},{"src":"` + prefix + `/img/logo-192.png","type":"image/png","sizes":"192x192"}],"start_url":"` + subURL + `","scope":"` + subURL + `","background_color":"%23FAFAFA","display":"standalone"}`) | 	if err == nil && parsedPrefix.Hostname() == "" { | ||||||
|  | 		if staticURLPrefix == "" { | ||||||
|  | 			return strings.TrimSuffix(appURL, "/") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// StaticURLPrefix is just a path
 | ||||||
|  | 		return strings.TrimSuffix(appURL, "/") + strings.TrimSuffix(staticURLPrefix, "/") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return strings.TrimSuffix(staticURLPrefix, "/") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MakeManifestData generates web app manifest JSON
 | ||||||
|  | func MakeManifestData(appName string, appURL string, absoluteAssetURL string) []byte { | ||||||
|  | 	type manifestIcon struct { | ||||||
|  | 		Src   string `json:"src"` | ||||||
|  | 		Type  string `json:"type"` | ||||||
|  | 		Sizes string `json:"sizes"` | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	type manifestJSON struct { | ||||||
|  | 		Name      string         `json:"name"` | ||||||
|  | 		ShortName string         `json:"short_name"` | ||||||
|  | 		StartURL  string         `json:"start_url"` | ||||||
|  | 		Icons     []manifestIcon `json:"icons"` | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bytes, err := json.Marshal(&manifestJSON{ | ||||||
|  | 		Name:      appName, | ||||||
|  | 		ShortName: appName, | ||||||
|  | 		StartURL:  appURL, | ||||||
|  | 		Icons: []manifestIcon{ | ||||||
|  | 			{ | ||||||
|  | 				Src:   absoluteAssetURL + "/img/logo-lg.png", | ||||||
|  | 				Type:  "image/png", | ||||||
|  | 				Sizes: "880x880", | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				Src:   absoluteAssetURL + "/img/logo-512.png", | ||||||
|  | 				Type:  "image/png", | ||||||
|  | 				Sizes: "512x512", | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				Src:   absoluteAssetURL + "/img/logo-192.png", | ||||||
|  | 				Type:  "image/png", | ||||||
|  | 				Sizes: "192x192", | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				Src:   absoluteAssetURL + "/img/logo-sm.png", | ||||||
|  | 				Type:  "image/png", | ||||||
|  | 				Sizes: "120x120", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("unable to marshal manifest JSON. Error: %v", err) | ||||||
|  | 		return make([]byte, 0) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return bytes | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewServices initializes the services
 | // NewServices initializes the services
 | ||||||
|  |  | ||||||
							
								
								
									
										29
									
								
								modules/setting/setting_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								modules/setting/setting_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | // Copyright 2020 The Gitea Authors. All rights reserved.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | package setting | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestMakeAbsoluteAssetURL(t *testing.T) { | ||||||
|  | 	assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234", "https://localhost:2345")) | ||||||
|  | 	assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234/", "https://localhost:2345")) | ||||||
|  | 	assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234/", "https://localhost:2345/")) | ||||||
|  | 	assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234", "/foo")) | ||||||
|  | 	assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/", "/foo")) | ||||||
|  | 	assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/", "/foo/")) | ||||||
|  | 	assert.Equal(t, "https://localhost:1234/foo/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo", "/bar")) | ||||||
|  | 	assert.Equal(t, "https://localhost:1234/foo/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/bar")) | ||||||
|  | 	assert.Equal(t, "https://localhost:1234/foo/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/bar/")) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestMakeManifestData(t *testing.T) { | ||||||
|  | 	jsonBytes := MakeManifestData(`Example App '\"`, "https://example.com", "https://example.com/foo/bar") | ||||||
|  | 	assert.True(t, json.Valid(jsonBytes)) | ||||||
|  | } | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1"> | 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
| 	<meta http-equiv="x-ua-compatible" content="ie=edge"> | 	<meta http-equiv="x-ua-compatible" content="ie=edge"> | ||||||
| 	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}} {{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}} </title> | 	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}} {{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}} </title> | ||||||
| 	<link rel="manifest" href="{{.ManifestData}}"/> | 	<link rel="manifest" href="data:{{.ManifestData}}"/> | ||||||
| 	<meta name="theme-color" content="{{ThemeColorMetaTag}}"> | 	<meta name="theme-color" content="{{ThemeColorMetaTag}}"> | ||||||
| 	<meta name="default-theme" content="{{DefaultTheme}}" /> | 	<meta name="default-theme" content="{{DefaultTheme}}" /> | ||||||
| 	<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}" /> | 	<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}" /> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue