1package bugsnag
2
3import (
4	"fmt"
5	"reflect"
6	"strings"
7)
8
9// MetaData is added to the Bugsnag dashboard in tabs. Each tab is
10// a map of strings -> values. You can pass MetaData to Notify, Recover
11// and AutoNotify as rawData.
12type MetaData map[string]map[string]interface{}
13
14// Update the meta-data with more information. Tabs are merged together such
15// that unique keys from both sides are preserved, and duplicate keys end up
16// with the provided values.
17func (meta MetaData) Update(other MetaData) {
18	for name, tab := range other {
19
20		if meta[name] == nil {
21			meta[name] = make(map[string]interface{})
22		}
23
24		for key, value := range tab {
25			meta[name][key] = value
26		}
27	}
28}
29
30// Add creates a tab of Bugsnag meta-data.
31// If the tab doesn't yet exist it will be created.
32// If the key already exists, it will be overwritten.
33func (meta MetaData) Add(tab string, key string, value interface{}) {
34	if meta[tab] == nil {
35		meta[tab] = make(map[string]interface{})
36	}
37
38	meta[tab][key] = value
39}
40
41// AddStruct creates a tab of Bugsnag meta-data.
42// The struct will be converted to an Object using the
43// reflect library so any private fields will not be exported.
44// As a safety measure, if you pass a non-struct the value will be
45// sent to Bugsnag under the "Extra data" tab.
46func (meta MetaData) AddStruct(tab string, obj interface{}) {
47	val := sanitizer{}.Sanitize(obj)
48	content, ok := val.(map[string]interface{})
49	if ok {
50		meta[tab] = content
51	} else {
52		// Wasn't a struct
53		meta.Add("Extra data", tab, obj)
54	}
55
56}
57
58// Remove any values from meta-data that have keys matching the filters,
59// and any that are recursive data-structures
60func (meta MetaData) sanitize(filters []string) interface{} {
61	return sanitizer{
62		Filters: filters,
63		Seen:    make([]interface{}, 0),
64	}.Sanitize(meta)
65
66}
67
68// The sanitizer is used to remove filtered params and recursion from meta-data.
69type sanitizer struct {
70	Filters []string
71	Seen    []interface{}
72}
73
74func (s sanitizer) Sanitize(data interface{}) interface{} {
75	for _, s := range s.Seen {
76		// TODO: we don't need deep equal here, just type-ignoring equality
77		if reflect.DeepEqual(data, s) {
78			return "[RECURSION]"
79		}
80	}
81
82	// Sanitizers are passed by value, so we can modify s and it only affects
83	// s.Seen for nested calls.
84	s.Seen = append(s.Seen, data)
85
86	t := reflect.TypeOf(data)
87	v := reflect.ValueOf(data)
88
89	switch t.Kind() {
90	case reflect.Bool,
91		reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
92		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
93		reflect.Float32, reflect.Float64:
94		return data
95
96	case reflect.String:
97		return data
98
99	case reflect.Interface, reflect.Ptr:
100		return s.Sanitize(v.Elem().Interface())
101
102	case reflect.Array, reflect.Slice:
103		ret := make([]interface{}, v.Len())
104		for i := 0; i < v.Len(); i++ {
105			ret[i] = s.Sanitize(v.Index(i).Interface())
106		}
107		return ret
108
109	case reflect.Map:
110		return s.sanitizeMap(v)
111
112	case reflect.Struct:
113		return s.sanitizeStruct(v, t)
114
115		// Things JSON can't serialize:
116		// case t.Chan, t.Func, reflect.Complex64, reflect.Complex128, reflect.UnsafePointer:
117	default:
118		return "[" + t.String() + "]"
119
120	}
121
122}
123
124func (s sanitizer) sanitizeMap(v reflect.Value) interface{} {
125	ret := make(map[string]interface{})
126
127	for _, key := range v.MapKeys() {
128		val := s.Sanitize(v.MapIndex(key).Interface())
129		newKey := fmt.Sprintf("%v", key.Interface())
130
131		if s.shouldRedact(newKey) {
132			val = "[REDACTED]"
133		}
134
135		ret[newKey] = val
136	}
137
138	return ret
139}
140
141func (s sanitizer) sanitizeStruct(v reflect.Value, t reflect.Type) interface{} {
142	ret := make(map[string]interface{})
143
144	for i := 0; i < v.NumField(); i++ {
145
146		val := v.Field(i)
147		// Don't export private fields
148		if !val.CanInterface() {
149			continue
150		}
151
152		name := t.Field(i).Name
153		var opts tagOptions
154
155		// Parse JSON tags. Supports name and "omitempty"
156		if jsonTag := t.Field(i).Tag.Get("json"); len(jsonTag) != 0 {
157			name, opts = parseTag(jsonTag)
158		}
159
160		if s.shouldRedact(name) {
161			ret[name] = "[REDACTED]"
162		} else {
163			sanitized := s.Sanitize(val.Interface())
164			if str, ok := sanitized.(string); ok {
165				if !(opts.Contains("omitempty") && len(str) == 0) {
166					ret[name] = str
167				}
168			} else {
169				ret[name] = sanitized
170			}
171
172		}
173	}
174
175	return ret
176}
177
178func (s sanitizer) shouldRedact(key string) bool {
179	for _, filter := range s.Filters {
180		if strings.Contains(strings.ToLower(filter), strings.ToLower(key)) {
181			return true
182		}
183	}
184	return false
185}
186