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	if t == nil {
90		return "<nil>"
91	}
92
93	switch t.Kind() {
94	case reflect.Bool,
95		reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
96		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
97		reflect.Float32, reflect.Float64:
98		return data
99
100	case reflect.String:
101		return data
102
103	case reflect.Interface, reflect.Ptr:
104		return s.Sanitize(v.Elem().Interface())
105
106	case reflect.Array, reflect.Slice:
107		ret := make([]interface{}, v.Len())
108		for i := 0; i < v.Len(); i++ {
109			ret[i] = s.Sanitize(v.Index(i).Interface())
110		}
111		return ret
112
113	case reflect.Map:
114		return s.sanitizeMap(v)
115
116	case reflect.Struct:
117		return s.sanitizeStruct(v, t)
118
119		// Things JSON can't serialize:
120		// case t.Chan, t.Func, reflect.Complex64, reflect.Complex128, reflect.UnsafePointer:
121	default:
122		return "[" + t.String() + "]"
123
124	}
125
126}
127
128func (s sanitizer) sanitizeMap(v reflect.Value) interface{} {
129	ret := make(map[string]interface{})
130
131	for _, key := range v.MapKeys() {
132		val := s.Sanitize(v.MapIndex(key).Interface())
133		newKey := fmt.Sprintf("%v", key.Interface())
134
135		if s.shouldRedact(newKey) {
136			val = "[REDACTED]"
137		}
138
139		ret[newKey] = val
140	}
141
142	return ret
143}
144
145func (s sanitizer) sanitizeStruct(v reflect.Value, t reflect.Type) interface{} {
146	ret := make(map[string]interface{})
147
148	for i := 0; i < v.NumField(); i++ {
149
150		val := v.Field(i)
151		// Don't export private fields
152		if !val.CanInterface() {
153			continue
154		}
155
156		name := t.Field(i).Name
157		var opts tagOptions
158
159		// Parse JSON tags. Supports name and "omitempty"
160		if jsonTag := t.Field(i).Tag.Get("json"); len(jsonTag) != 0 {
161			name, opts = parseTag(jsonTag)
162		}
163
164		if s.shouldRedact(name) {
165			ret[name] = "[REDACTED]"
166		} else {
167			sanitized := s.Sanitize(val.Interface())
168			if str, ok := sanitized.(string); ok {
169				if !(opts.Contains("omitempty") && len(str) == 0) {
170					ret[name] = str
171				}
172			} else {
173				ret[name] = sanitized
174			}
175
176		}
177	}
178
179	return ret
180}
181
182func (s sanitizer) shouldRedact(key string) bool {
183	for _, filter := range s.Filters {
184		if strings.Contains(strings.ToLower(filter), strings.ToLower(key)) {
185			return true
186		}
187	}
188	return false
189}
190