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] }