github.com/safing/portbase@v0.19.5/config/option.go (about) 1 package config 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "reflect" 7 "regexp" 8 "sync" 9 10 "github.com/mitchellh/copystructure" 11 "github.com/tidwall/sjson" 12 13 "github.com/safing/portbase/database/record" 14 "github.com/safing/portbase/formats/dsd" 15 ) 16 17 // OptionType defines the value type of an option. 18 type OptionType uint8 19 20 // Various attribute options. Use ExternalOptType for extended types in the frontend. 21 const ( 22 optTypeAny OptionType = 0 23 OptTypeString OptionType = 1 24 OptTypeStringArray OptionType = 2 25 OptTypeInt OptionType = 3 26 OptTypeBool OptionType = 4 27 ) 28 29 func getTypeName(t OptionType) string { 30 switch t { 31 case optTypeAny: 32 return "any" 33 case OptTypeString: 34 return "string" 35 case OptTypeStringArray: 36 return "[]string" 37 case OptTypeInt: 38 return "int" 39 case OptTypeBool: 40 return "bool" 41 default: 42 return "unknown" 43 } 44 } 45 46 // PossibleValue defines a value that is possible for 47 // a configuration setting. 48 type PossibleValue struct { 49 // Name is a human readable name of the option. 50 Name string 51 // Description is a human readable description of 52 // this value. 53 Description string 54 // Value is the actual value of the option. The type 55 // must match the option's value type. 56 Value interface{} 57 } 58 59 // Annotations can be attached to configuration options to 60 // provide hints for user interfaces or other systems working 61 // or setting configuration options. 62 // Annotation keys should follow the below format to ensure 63 // future well-known annotation additions do not conflict 64 // with vendor/product/package specific annoations. 65 // 66 // Format: <vendor/package>:<scope>:<identifier> //. 67 type Annotations map[string]interface{} 68 69 // MigrationFunc is a function that migrates a config option value. 70 type MigrationFunc func(option *Option, value any) any 71 72 // Well known annotations defined by this package. 73 const ( 74 // DisplayHintAnnotation provides a hint for the user 75 // interface on how to render an option. 76 // The value of DisplayHintAnnotation is expected to 77 // be a string. See DisplayHintXXXX constants below 78 // for a list of well-known display hint annotations. 79 DisplayHintAnnotation = "safing/portbase:ui:display-hint" 80 // DisplayOrderAnnotation provides a hint for the user 81 // interface in which order settings should be displayed. 82 // The value of DisplayOrderAnnotations is expected to be 83 // an number (int). 84 DisplayOrderAnnotation = "safing/portbase:ui:order" 85 // UnitAnnotations defines the SI unit of an option (if any). 86 UnitAnnotation = "safing/portbase:ui:unit" 87 // CategoryAnnotations can provide an additional category 88 // to each settings. This category can be used by a user 89 // interface to group certain options together. 90 // User interfaces should treat a CategoryAnnotation, if 91 // supported, with higher priority as a DisplayOrderAnnotation. 92 CategoryAnnotation = "safing/portbase:ui:category" 93 // SubsystemAnnotation can be used to mark an option as part 94 // of a module subsystem. 95 SubsystemAnnotation = "safing/portbase:module:subsystem" 96 // StackableAnnotation can be set on configuration options that 97 // stack on top of the default (or otherwise related) options. 98 // The value of StackableAnnotaiton is expected to be a boolean but 99 // may be extended to hold references to other options in the 100 // future. 101 StackableAnnotation = "safing/portbase:options:stackable" 102 // RestartPendingAnnotation is automatically set on a configuration option 103 // that requires a restart and has been changed. 104 // The value must always be a boolean with value "true". 105 RestartPendingAnnotation = "safing/portbase:options:restart-pending" 106 // QuickSettingAnnotation can be used to add quick settings to 107 // a configuration option. A quick setting can support the user 108 // by switching between pre-configured values. 109 // The type of a quick-setting annotation is []QuickSetting or QuickSetting. 110 QuickSettingsAnnotation = "safing/portbase:ui:quick-setting" 111 // RequiresAnnotation can be used to mark another option as a 112 // requirement. The type of RequiresAnnotation is []ValueRequirement 113 // or ValueRequirement. 114 RequiresAnnotation = "safing/portbase:config:requires" 115 // RequiresFeatureIDAnnotation can be used to mark a setting as only available 116 // when the user has a certain feature ID in the subscription plan. 117 // The type is []string or string. 118 RequiresFeatureIDAnnotation = "safing/portmaster:ui:config:requires-feature" 119 // SettablePerAppAnnotation can be used to mark a setting as settable per-app and 120 // is a boolean. 121 SettablePerAppAnnotation = "safing/portmaster:settable-per-app" 122 // RequiresUIReloadAnnotation can be used to inform the UI that changing the value 123 // of the annotated setting requires a full reload of the user interface. 124 // The value of this annotation does not matter as the sole presence of 125 // the annotation key is enough. Though, users are advised to set the value 126 // of this annotation to true. 127 RequiresUIReloadAnnotation = "safing/portmaster:ui:requires-reload" 128 ) 129 130 // QuickSettingsAction defines the action of a quick setting. 131 type QuickSettingsAction string 132 133 const ( 134 // QuickReplace replaces the current setting with the one from 135 // the quick setting. 136 QuickReplace = QuickSettingsAction("replace") 137 // QuickMergeTop merges the value of the quick setting with the 138 // already configured one adding new values on the top. Merging 139 // is only supported for OptTypeStringArray. 140 QuickMergeTop = QuickSettingsAction("merge-top") 141 // QuickMergeBottom merges the value of the quick setting with the 142 // already configured one adding new values at the bottom. Merging 143 // is only supported for OptTypeStringArray. 144 QuickMergeBottom = QuickSettingsAction("merge-bottom") 145 ) 146 147 // QuickSetting defines a quick setting for a configuration option and 148 // should be used together with the QuickSettingsAnnotation. 149 type QuickSetting struct { 150 // Name is the name of the quick setting. 151 Name string 152 153 // Value is the value that the quick-setting configures. It must match 154 // the expected value type of the annotated option. 155 Value interface{} 156 157 // Action defines the action of the quick setting. 158 Action QuickSettingsAction 159 } 160 161 // ValueRequirement defines a requirement on another configuration option. 162 type ValueRequirement struct { 163 // Key is the key of the configuration option that is required. 164 Key string 165 166 // Value that is required. 167 Value interface{} 168 } 169 170 // Values for the DisplayHintAnnotation. 171 const ( 172 // DisplayHintOneOf is used to mark an option 173 // as a "select"-style option. That is, only one of 174 // the supported values may be set. This option makes 175 // only sense together with the PossibleValues property 176 // of Option. 177 DisplayHintOneOf = "one-of" 178 // DisplayHintOrdered is used to mark a list option as ordered. 179 // That is, the order of items is important and a user interface 180 // is encouraged to provide the user with re-ordering support 181 // (like drag'n'drop). 182 DisplayHintOrdered = "ordered" 183 // DisplayHintFilePicker is used to mark the option as being a file, which 184 // should give the option to use a file picker to select a local file from disk. 185 DisplayHintFilePicker = "file-picker" 186 ) 187 188 // Option describes a configuration option. 189 type Option struct { 190 sync.Mutex 191 // Name holds the name of the configuration options. 192 // It should be human readable and is mainly used for 193 // presentation purposes. 194 // Name is considered immutable after the option has 195 // been created. 196 Name string 197 // Key holds the database path for the option. It should 198 // follow the path format `category/sub/key`. 199 // Key is considered immutable after the option has 200 // been created. 201 Key string 202 // Description holds a human readable description of the 203 // option and what is does. The description should be short. 204 // Use the Help property for a longer support text. 205 // Description is considered immutable after the option has 206 // been created. 207 Description string 208 // Help may hold a long version of the description providing 209 // assistance with the configuration option. 210 // Help is considered immutable after the option has 211 // been created. 212 Help string 213 // Sensitive signifies that the configuration values may contain sensitive 214 // content, such as authentication keys. 215 Sensitive bool 216 // OptType defines the type of the option. 217 // OptType is considered immutable after the option has 218 // been created. 219 OptType OptionType 220 // ExpertiseLevel can be used to set the required expertise 221 // level for the option to be displayed to a user. 222 // ExpertiseLevel is considered immutable after the option has 223 // been created. 224 ExpertiseLevel ExpertiseLevel 225 // ReleaseLevel is used to mark the stability of the option. 226 // ReleaseLevel is considered immutable after the option has 227 // been created. 228 ReleaseLevel ReleaseLevel 229 // RequiresRestart should be set to true if a modification of 230 // the options value requires a restart of the whole application 231 // to take effect. 232 // RequiresRestart is considered immutable after the option has 233 // been created. 234 RequiresRestart bool 235 // DefaultValue holds the default value of the option. Note that 236 // this value can be overwritten during runtime (see activeDefaultValue 237 // and activeFallbackValue). 238 // DefaultValue is considered immutable after the option has 239 // been created. 240 DefaultValue interface{} 241 // ValidationRegex may contain a regular expression used to validate 242 // the value of option. If the option type is set to OptTypeStringArray 243 // the validation regex is applied to all entries of the string slice. 244 // Note that it is recommended to keep the validation regex simple so 245 // it can also be used in other languages (mainly JavaScript) to provide 246 // a better user-experience by pre-validating the expression. 247 // ValidationRegex is considered immutable after the option has 248 // been created. 249 ValidationRegex string 250 // ValidationFunc may contain a function to validate more complex values. 251 // The error is returned beyond the scope of this package and may be 252 // displayed to a user. 253 ValidationFunc func(value interface{}) error `json:"-"` 254 // PossibleValues may be set to a slice of values that are allowed 255 // for this configuration setting. Note that PossibleValues makes most 256 // sense when ExternalOptType is set to HintOneOf 257 // PossibleValues is considered immutable after the option has 258 // been created. 259 PossibleValues []PossibleValue `json:",omitempty"` 260 // Annotations adds additional annotations to the configuration options. 261 // See documentation of Annotations for more information. 262 // Annotations is considered mutable and setting/reading annotation keys 263 // must be performed while the option is locked. 264 Annotations Annotations 265 // Migrations holds migration functions that are given the raw option value 266 // before any validation is run. The returned value is then used. 267 Migrations []MigrationFunc `json:"-"` 268 269 activeValue *valueCache // runtime value (loaded from config file or set by user) 270 activeDefaultValue *valueCache // runtime default value (may be set internally) 271 activeFallbackValue *valueCache // default value from option registration 272 compiledRegex *regexp.Regexp 273 } 274 275 // AddAnnotation adds the annotation key to option if it's not already set. 276 func (option *Option) AddAnnotation(key string, value interface{}) { 277 option.Lock() 278 defer option.Unlock() 279 280 if option.Annotations == nil { 281 option.Annotations = make(Annotations) 282 } 283 284 if _, ok := option.Annotations[key]; ok { 285 return 286 } 287 option.Annotations[key] = value 288 } 289 290 // SetAnnotation sets the value of the annotation key overwritting an 291 // existing value if required. 292 func (option *Option) SetAnnotation(key string, value interface{}) { 293 option.Lock() 294 defer option.Unlock() 295 296 option.setAnnotation(key, value) 297 } 298 299 // setAnnotation sets the value of the annotation key overwritting an 300 // existing value if required. Does not lock the Option. 301 func (option *Option) setAnnotation(key string, value interface{}) { 302 if option.Annotations == nil { 303 option.Annotations = make(Annotations) 304 } 305 option.Annotations[key] = value 306 } 307 308 // GetAnnotation returns the value of the annotation key. 309 func (option *Option) GetAnnotation(key string) (interface{}, bool) { 310 option.Lock() 311 defer option.Unlock() 312 313 if option.Annotations == nil { 314 return nil, false 315 } 316 val, ok := option.Annotations[key] 317 return val, ok 318 } 319 320 // AnnotationEquals returns whether the annotation of the given key matches the 321 // given value. 322 func (option *Option) AnnotationEquals(key string, value any) bool { 323 option.Lock() 324 defer option.Unlock() 325 326 if option.Annotations == nil { 327 return false 328 } 329 setValue, ok := option.Annotations[key] 330 if !ok { 331 return false 332 } 333 return reflect.DeepEqual(value, setValue) 334 } 335 336 // copyOrNil returns a copy of the option, or nil if copying failed. 337 func (option *Option) copyOrNil() *Option { 338 copied, err := copystructure.Copy(option) 339 if err != nil { 340 return nil 341 } 342 return copied.(*Option) //nolint:forcetypeassert 343 } 344 345 // IsSetByUser returns whether the option has been set by the user. 346 func (option *Option) IsSetByUser() bool { 347 option.Lock() 348 defer option.Unlock() 349 350 return option.activeValue != nil 351 } 352 353 // UserValue returns the value set by the user or nil if the value has not 354 // been changed from the default. 355 func (option *Option) UserValue() any { 356 option.Lock() 357 defer option.Unlock() 358 359 if option.activeValue == nil { 360 return nil 361 } 362 return option.activeValue.getData(option) 363 } 364 365 // ValidateValue checks if the given value is valid for the option. 366 func (option *Option) ValidateValue(value any) error { 367 option.Lock() 368 defer option.Unlock() 369 370 value = migrateValue(option, value) 371 if _, err := validateValue(option, value); err != nil { 372 return err 373 } 374 return nil 375 } 376 377 // Export expors an option to a Record. 378 func (option *Option) Export() (record.Record, error) { 379 option.Lock() 380 defer option.Unlock() 381 382 return option.export() 383 } 384 385 func (option *Option) export() (record.Record, error) { 386 data, err := json.Marshal(option) 387 if err != nil { 388 return nil, err 389 } 390 391 if option.activeValue != nil { 392 data, err = sjson.SetBytes(data, "Value", option.activeValue.getData(option)) 393 if err != nil { 394 return nil, err 395 } 396 } 397 398 if option.activeDefaultValue != nil { 399 data, err = sjson.SetBytes(data, "DefaultValue", option.activeDefaultValue.getData(option)) 400 if err != nil { 401 return nil, err 402 } 403 } 404 405 r, err := record.NewWrapper(fmt.Sprintf("config:%s", option.Key), nil, dsd.JSON, data) 406 if err != nil { 407 return nil, err 408 } 409 r.SetMeta(&record.Meta{}) 410 411 return r, nil 412 } 413 414 type sortByKey []*Option 415 416 func (opts sortByKey) Len() int { return len(opts) } 417 func (opts sortByKey) Less(i, j int) bool { return opts[i].Key < opts[j].Key } 418 func (opts sortByKey) Swap(i, j int) { opts[i], opts[j] = opts[j], opts[i] }