Skip to content

Commit

Permalink
Escape data passed to templated translation messages
Browse files Browse the repository at this point in the history
  • Loading branch information
hawx committed Jan 16, 2024
1 parent 0f5f93c commit 06e4597
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 48 deletions.
2 changes: 1 addition & 1 deletion cmd/event-received/cloud_watch_event_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (h *cloudWatchEventHandler) Handle(ctx context.Context, event events.CloudW
return handleEvidenceReceived(ctx, h.dynamoClient, event)

case "reduced-fee-approved":
bundle := localize.NewBundle("./lang/en.json", "./lang/cy.json")
bundle, _ := localize.NewBundle("./lang/en.json", "./lang/cy.json")

Check warning on line 49 in cmd/event-received/cloud_watch_event_handler.go

View check run for this annotation

Codecov / codecov/patch

cmd/event-received/cloud_watch_event_handler.go#L48-L49

Added lines #L48 - L49 were not covered by tests
//TODO do this in handleFeeApproved when/if we save lang preference in LPA
appData := page.AppData{Localizer: bundle.For(localize.En)}
Expand Down
5 changes: 4 additions & 1 deletion cmd/mlpa/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ func main() {
logger.Fatal(err)
}

bundle := localize.NewBundle("lang/en.json", "lang/cy.json")
bundle, err := localize.NewBundle("lang/en.json", "lang/cy.json")
if err != nil {
logger.Fatal(err)
}

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
Expand Down
189 changes: 167 additions & 22 deletions internal/localize/localizer.go
Original file line number Diff line number Diff line change
@@ -1,67 +1,212 @@
package localize

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
"os"
"path"
"strings"
"time"
"unicode"
"unicode/utf8"

"github.com/ministryofjustice/opg-modernising-lpa/internal/date"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)

type Message struct {
S string

// when plural
One string
Other string

// for Welsh only
Two string
Few string
Many string
}

func (m *Message) UnmarshalJSON(text []byte) error {
var s string
if err := json.Unmarshal(text, &s); err == nil {
m.S = s
return nil
}

var v map[string]string
if err := json.Unmarshal(text, &v); err == nil {
m.One = v["one"]
m.Other = v["other"]
m.Two = v["two"]
m.Few = v["few"]
m.Many = v["many"]
return nil
}

return errors.New("message malformed")
}

type Messages map[string]Message

func (m Messages) Find(key string) (string, bool) {
if msg, ok := m[key]; ok {
return msg.S, true
}

return "", false
}

func (m Messages) FindPlural(key string, count int) (string, bool) {
msg, ok := m[key]
if !ok {
return "", false
}

if count == 1 {
return msg.One, true
}

if count == 2 && msg.Two != "" {
return msg.Two, true
}

if count == 3 && msg.Few != "" {
return msg.Few, true
}

if count == 6 && msg.Many != "" {
return msg.Many, true
}

return msg.Other, true
}

type Bundle struct {
*i18n.Bundle
messages map[string]Messages
}

func NewBundle(paths ...string) Bundle {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
func NewBundle(paths ...string) (*Bundle, error) {
bundle := &Bundle{messages: map[string]Messages{}}

for _, path := range paths {
bundle.LoadMessageFile(path)
if err := bundle.LoadMessageFile(path); err != nil {
return nil, err
}
}

return Bundle{bundle}
return bundle, nil
}

func (b Bundle) For(lang Lang) *Localizer {
return &Localizer{
i18n.NewLocalizer(b.Bundle, lang.String()),
false,
lang,
func (b *Bundle) LoadMessageFile(p string) error {
data, err := os.ReadFile(p)
if err != nil {
return err
}

var v map[string]Message
if err := json.Unmarshal(data, &v); err != nil {
return err

Check warning on line 111 in internal/localize/localizer.go

View check run for this annotation

Codecov / codecov/patch

internal/localize/localizer.go#L111

Added line #L111 was not covered by tests
}

lang, _ := strings.CutSuffix(path.Base(p), ".json")

if lang == "en" {
if err := verifyEn(v); err != nil {
return err
}
} else if lang == "cy" {
if err := verifyCy(v); err != nil {
return err
}
} else {
return errors.New("only supports en or cy")
}

b.messages[lang] = v
return nil
}

func verifyEn(v map[string]Message) error {
for key, message := range v {
if message.S != "" {
continue
}

if message.One != "" && message.Other != "" && message.Two == "" && message.Few == "" && message.Many == "" {
continue
}

return fmt.Errorf("problem with key: %s", key)
}

return nil
}

func verifyCy(v map[string]Message) error {
for key, message := range v {
if message.S != "" {
continue
}

if message.One != "" && message.Other != "" && message.Two != "" && message.Few != "" && message.Many != "" {
continue
}

return fmt.Errorf("problem with key: %s", key)
}

return nil
}

func (b *Bundle) For(lang Lang) *Localizer {
return &Localizer{b.messages[lang.String()], false, lang}
}

type Localizer struct {
*i18n.Localizer
messages Messages
showTranslationKeys bool
Lang Lang
}

func (l Localizer) T(messageID string) string {
msg, err := l.Localize(&i18n.LocalizeConfig{MessageID: messageID})

if err != nil {
func (l *Localizer) T(messageID string) string {
msg, ok := l.messages.Find(messageID)
if !ok {
return l.translate(messageID, messageID)
}

return l.translate(msg, messageID)
}

func (l Localizer) Format(messageID string, data map[string]interface{}) string {
return l.translate(l.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID, TemplateData: data}), messageID)
msg, ok := l.messages.Find(messageID)
if !ok {
return l.translate(messageID, messageID)
}

Check warning on line 187 in internal/localize/localizer.go

View check run for this annotation

Codecov / codecov/patch

internal/localize/localizer.go#L186-L187

Added lines #L186 - L187 were not covered by tests

return l.translateFormat(msg, messageID, data)
}

func (l Localizer) Count(messageID string, count int) string {
return l.translate(l.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID, PluralCount: count}), messageID)
return l.FormatCount(messageID, count, map[string]any{})
}

func (l Localizer) FormatCount(messageID string, count int, data map[string]interface{}) string {
func (l Localizer) FormatCount(messageID string, count int, data map[string]any) string {
msg, ok := l.messages.FindPlural(messageID, count)
if !ok {
return l.translate(messageID, messageID)
}

data["PluralCount"] = count
return l.translate(l.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID, PluralCount: count, TemplateData: data}), messageID)
return l.translateFormat(msg, messageID, data)
}

func (l Localizer) translateFormat(msg, messageID string, data any) string {
var buf bytes.Buffer
template.Must(template.New("").Parse(msg)).Execute(&buf, data)
return l.translate(buf.String(), messageID)
}

func (l Localizer) translate(translation, messageID string) string {
Expand Down
26 changes: 23 additions & 3 deletions internal/localize/localizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (

func TestNewBundle(t *testing.T) {
assert := assert.New(t)
bundle := NewBundle("testdata/en.json", "testdata/cy.json")
bundle, err := NewBundle("testdata/en.json", "testdata/cy.json")
assert.Nil(err)

en := bundle.For(En)
assert.Equal("A", en.T("a"))
Expand All @@ -20,6 +21,7 @@ func TestNewBundle(t *testing.T) {

assert.Equal("1 ONE", en.Count("c", 1))
assert.Equal("2 OTHER", en.Count("c", 2))
assert.Equal("key does not exist", en.Count("key does not exist", 3))

assert.Equal("1 ONE FORMATTED", en.FormatCount("d", 1, map[string]interface{}{"x": "FORMATTED"}))
assert.Equal("2 OTHER FORMATTED", en.FormatCount("d", 2, map[string]interface{}{"x": "FORMATTED"}))
Expand All @@ -46,9 +48,27 @@ func TestNewBundle(t *testing.T) {
assert.Equal("7 other formatted", cy.FormatCount("d", 7, map[string]interface{}{"x": "formatted"}))
}

func TestNewBundleWhenBadFormat(t *testing.T) {
_, err := NewBundle("testdata/bad/en.json")
assert.NotNil(t, err)

_, err = NewBundle("testdata/bad/cy.json")
assert.NotNil(t, err)
}

func TestNewBundleWhenMissingFile(t *testing.T) {
_, err := NewBundle("testdata/a.json")
assert.NotNil(t, err)
}

func TestNewBundleWhenOtherLang(t *testing.T) {
_, err := NewBundle("testdata/zz.json")
assert.NotNil(t, err)
}

func TestNewBundleWithTransKeys(t *testing.T) {
assert := assert.New(t)
bundle := NewBundle("testdata/en.json", "testdata/cy.json")
bundle, _ := NewBundle("testdata/en.json", "testdata/cy.json")

en := bundle.For(En)
en.SetShowTranslationKeys(true)
Expand Down Expand Up @@ -121,7 +141,7 @@ func TestPossessive(t *testing.T) {
}

func TestConcat(t *testing.T) {
bundle := NewBundle("testdata/en.json", "testdata/cy.json")
bundle, _ := NewBundle("testdata/en.json", "testdata/cy.json")
en := bundle.For(En)

assert.Equal(t, "Bob Smith, Alice Jones, John Doe or Paul Compton", en.Concat([]string{"Bob Smith", "Alice Jones", "John Doe", "Paul Compton"}, "or"))
Expand Down
5 changes: 5 additions & 0 deletions internal/localize/testdata/bad/cy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"x": {
"other": "other"
}
}
5 changes: 5 additions & 0 deletions internal/localize/testdata/bad/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"s": {
"one": "one"
}
}
1 change: 1 addition & 0 deletions internal/localize/testdata/zz.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
12 changes: 6 additions & 6 deletions internal/page/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,16 @@ type Bundle interface {
}

type Localizer interface {
Format(string, map[string]any) string
T(string) string
Concat([]string, string) string
Count(string, int) string
Format(string, map[string]any) string
FormatCount(string, int, map[string]interface{}) string
ShowTranslationKeys() bool
SetShowTranslationKeys(bool)
Possessive(string) string
Concat([]string, string) string
FormatDate(date.TimeOrDate) string
FormatDateTime(time.Time) string
Possessive(string) string
SetShowTranslationKeys(bool)
ShowTranslationKeys() bool
T(string) string
}

func PostFormString(r *http.Request, name string) string {
Expand Down
Loading

0 comments on commit 06e4597

Please sign in to comment.