github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/model/manifest.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package model 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "strings" 14 15 "github.com/blang/semver" 16 "github.com/pkg/errors" 17 "gopkg.in/yaml.v2" 18 ) 19 20 type PluginOption struct { 21 // The display name for the option. 22 DisplayName string `json:"display_name" yaml:"display_name"` 23 24 // The string value for the option. 25 Value string `json:"value" yaml:"value"` 26 } 27 28 type PluginSettingType int 29 30 const ( 31 Bool PluginSettingType = iota 32 Dropdown 33 Generated 34 Radio 35 Text 36 LongText 37 Number 38 Username 39 Custom 40 ) 41 42 type PluginSetting struct { 43 // The key that the setting will be assigned to in the configuration file. 44 Key string `json:"key" yaml:"key"` 45 46 // The display name for the setting. 47 DisplayName string `json:"display_name" yaml:"display_name"` 48 49 // The type of the setting. 50 // 51 // "bool" will result in a boolean true or false setting. 52 // 53 // "dropdown" will result in a string setting that allows the user to select from a list of 54 // pre-defined options. 55 // 56 // "generated" will result in a string setting that is set to a random, cryptographically secure 57 // string. 58 // 59 // "radio" will result in a string setting that allows the user to select from a short selection 60 // of pre-defined options. 61 // 62 // "text" will result in a string setting that can be typed in manually. 63 // 64 // "longtext" will result in a multi line string that can be typed in manually. 65 // 66 // "number" will result in in integer setting that can be typed in manually. 67 // 68 // "username" will result in a text setting that will autocomplete to a username. 69 // 70 // "custom" will result in a custom defined setting and will load the custom component registered for the Web App System Console. 71 Type string `json:"type" yaml:"type"` 72 73 // The help text to display to the user. Supports Markdown formatting. 74 HelpText string `json:"help_text" yaml:"help_text"` 75 76 // The help text to display alongside the "Regenerate" button for settings of the "generated" type. 77 RegenerateHelpText string `json:"regenerate_help_text,omitempty" yaml:"regenerate_help_text,omitempty"` 78 79 // The placeholder to display for "generated", "text", "longtext", "number" and "username" types when blank. 80 Placeholder string `json:"placeholder" yaml:"placeholder"` 81 82 // The default value of the setting. 83 Default interface{} `json:"default" yaml:"default"` 84 85 // For "radio" or "dropdown" settings, this is the list of pre-defined options that the user can choose 86 // from. 87 Options []*PluginOption `json:"options,omitempty" yaml:"options,omitempty"` 88 } 89 90 type PluginSettingsSchema struct { 91 // Optional text to display above the settings. Supports Markdown formatting. 92 Header string `json:"header" yaml:"header"` 93 94 // Optional text to display below the settings. Supports Markdown formatting. 95 Footer string `json:"footer" yaml:"footer"` 96 97 // A list of setting definitions. 98 Settings []*PluginSetting `json:"settings" yaml:"settings"` 99 } 100 101 // The plugin manifest defines the metadata required to load and present your plugin. The manifest 102 // file should be named plugin.json or plugin.yaml and placed in the top of your 103 // plugin bundle. 104 // 105 // Example plugin.json: 106 // 107 // 108 // { 109 // "id": "com.mycompany.myplugin", 110 // "name": "My Plugin", 111 // "description": "This is my plugin", 112 // "homepage_url": "https://example.com", 113 // "support_url": "https://example.com/support", 114 // "release_notes_url": "https://example.com/releases/v0.0.1", 115 // "icon_path": "assets/logo.svg", 116 // "version": "0.1.0", 117 // "min_server_version": "5.6.0", 118 // "server": { 119 // "executables": { 120 // "linux-amd64": "server/dist/plugin-linux-amd64", 121 // "darwin-amd64": "server/dist/plugin-darwin-amd64", 122 // "windows-amd64": "server/dist/plugin-windows-amd64.exe" 123 // } 124 // }, 125 // "webapp": { 126 // "bundle_path": "webapp/dist/main.js" 127 // }, 128 // "settings_schema": { 129 // "header": "Some header text", 130 // "footer": "Some footer text", 131 // "settings": [{ 132 // "key": "someKey", 133 // "display_name": "Enable Extra Feature", 134 // "type": "bool", 135 // "help_text": "When true, an extra feature will be enabled!", 136 // "default": "false" 137 // }] 138 // }, 139 // "props": { 140 // "someKey": "someData" 141 // } 142 // } 143 type Manifest struct { 144 // The id is a globally unique identifier that represents your plugin. Ids must be at least 145 // 3 characters, at most 190 characters and must match ^[a-zA-Z0-9-_\.]+$. 146 // Reverse-DNS notation using a name you control is a good option, e.g. "com.mycompany.myplugin". 147 Id string `json:"id" yaml:"id"` 148 149 // The name to be displayed for the plugin. 150 Name string `json:"name,omitempty" yaml:"name,omitempty"` 151 152 // A description of what your plugin is and does. 153 Description string `json:"description,omitempty" yaml:"description,omitempty"` 154 155 // HomepageURL is an optional link to learn more about the plugin. 156 HomepageURL string `json:"homepage_url,omitempty" yaml:"homepage_url,omitempty"` 157 158 // SupportURL is an optional URL where plugin issues can be reported. 159 SupportURL string `json:"support_url,omitempty" yaml:"support_url,omitempty"` 160 161 // ReleaseNotesURL is an optional URL where a changelog for the release can be found. 162 ReleaseNotesURL string `json:"release_notes_url,omitempty" yaml:"release_notes_url,omitempty"` 163 164 // A relative file path in the bundle that points to the plugins svg icon for use with the Plugin Marketplace. 165 // This should be relative to the root of your bundle and the location of the manifest file. Bitmap image formats are not supported. 166 IconPath string `json:"icon_path,omitempty" yaml:"icon_path,omitempty"` 167 168 // A version number for your plugin. Semantic versioning is recommended: http://semver.org 169 Version string `json:"version" yaml:"version"` 170 171 // The minimum Mattermost server version required for your plugin. 172 // 173 // Minimum server version: 5.6 174 MinServerVersion string `json:"min_server_version,omitempty" yaml:"min_server_version,omitempty"` 175 176 // Server defines the server-side portion of your plugin. 177 Server *ManifestServer `json:"server,omitempty" yaml:"server,omitempty"` 178 179 // Backend is a deprecated flag for defining the server-side portion of your plugin. Going forward, use Server instead. 180 Backend *ManifestServer `json:"backend,omitempty" yaml:"backend,omitempty"` 181 182 // If your plugin extends the web app, you'll need to define webapp. 183 Webapp *ManifestWebapp `json:"webapp,omitempty" yaml:"webapp,omitempty"` 184 185 // To allow administrators to configure your plugin via the Mattermost system console, you can 186 // provide your settings schema. 187 SettingsSchema *PluginSettingsSchema `json:"settings_schema,omitempty" yaml:"settings_schema,omitempty"` 188 189 // Plugins can store any kind of data in Props to allow other plugins to use it. 190 Props map[string]interface{} `json:"props,omitempty" yaml:"props,omitempty"` 191 192 // RequiredConfig defines any required server configuration fields for the plugin to function properly. 193 // 194 // Use the plugin helpers CheckRequiredServerConfiguration method to enforce this. 195 RequiredConfig *Config `json:"required_configuration,omitempty" yaml:"required_configuration,omitempty"` 196 } 197 198 type ManifestServer struct { 199 // Executables are the paths to your executable binaries, specifying multiple entry points 200 // for different platforms when bundled together in a single plugin. 201 Executables *ManifestExecutables `json:"executables,omitempty" yaml:"executables,omitempty"` 202 203 // Executable is the path to your executable binary. This should be relative to the root 204 // of your bundle and the location of the manifest file. 205 // 206 // On Windows, this file must have a ".exe" extension. 207 // 208 // If your plugin is compiled for multiple platforms, consider bundling them together 209 // and using the Executables field instead. 210 Executable string `json:"executable" yaml:"executable"` 211 } 212 213 type ManifestExecutables struct { 214 // LinuxAmd64 is the path to your executable binary for the corresponding platform 215 LinuxAmd64 string `json:"linux-amd64,omitempty" yaml:"linux-amd64,omitempty"` 216 // DarwinAmd64 is the path to your executable binary for the corresponding platform 217 DarwinAmd64 string `json:"darwin-amd64,omitempty" yaml:"darwin-amd64,omitempty"` 218 // WindowsAmd64 is the path to your executable binary for the corresponding platform 219 // This file must have a ".exe" extension 220 WindowsAmd64 string `json:"windows-amd64,omitempty" yaml:"windows-amd64,omitempty"` 221 } 222 223 type ManifestWebapp struct { 224 // The path to your webapp bundle. This should be relative to the root of your bundle and the 225 // location of the manifest file. 226 BundlePath string `json:"bundle_path" yaml:"bundle_path"` 227 228 // BundleHash is the 64-bit FNV-1a hash of the webapp bundle, computed when the plugin is loaded 229 BundleHash []byte `json:"-"` 230 } 231 232 func (m *Manifest) ToJson() string { 233 b, _ := json.Marshal(m) 234 return string(b) 235 } 236 237 func ManifestListToJson(m []*Manifest) string { 238 b, _ := json.Marshal(m) 239 return string(b) 240 } 241 242 func ManifestFromJson(data io.Reader) *Manifest { 243 var m *Manifest 244 json.NewDecoder(data).Decode(&m) 245 return m 246 } 247 248 func ManifestListFromJson(data io.Reader) []*Manifest { 249 var manifests []*Manifest 250 json.NewDecoder(data).Decode(&manifests) 251 return manifests 252 } 253 254 func (m *Manifest) HasClient() bool { 255 return m.Webapp != nil 256 } 257 258 func (m *Manifest) ClientManifest() *Manifest { 259 cm := new(Manifest) 260 *cm = *m 261 cm.Name = "" 262 cm.Description = "" 263 cm.Server = nil 264 if cm.Webapp != nil { 265 cm.Webapp = new(ManifestWebapp) 266 *cm.Webapp = *m.Webapp 267 cm.Webapp.BundlePath = "/static/" + m.Id + "/" + fmt.Sprintf("%s_%x_bundle.js", m.Id, m.Webapp.BundleHash) 268 } 269 return cm 270 } 271 272 // GetExecutableForRuntime returns the path to the executable for the given runtime architecture. 273 // 274 // If the manifest defines multiple executables, but none match, or if only a single executable 275 // is defined, the Executable field will be returned. This method does not guarantee that the 276 // resulting binary can actually execute on the given platform. 277 func (m *Manifest) GetExecutableForRuntime(goOs, goArch string) string { 278 server := m.Server 279 280 // Support the deprecated backend parameter. 281 if server == nil { 282 server = m.Backend 283 } 284 285 if server == nil { 286 return "" 287 } 288 289 var executable string 290 if server.Executables != nil { 291 if goOs == "linux" && goArch == "amd64" { 292 executable = server.Executables.LinuxAmd64 293 } else if goOs == "darwin" && goArch == "amd64" { 294 executable = server.Executables.DarwinAmd64 295 } else if goOs == "windows" && goArch == "amd64" { 296 executable = server.Executables.WindowsAmd64 297 } 298 } 299 300 if executable == "" { 301 executable = server.Executable 302 } 303 304 return executable 305 } 306 307 func (m *Manifest) HasServer() bool { 308 return m.Server != nil || m.Backend != nil 309 } 310 311 func (m *Manifest) HasWebapp() bool { 312 return m.Webapp != nil 313 } 314 315 func (m *Manifest) MeetMinServerVersion(serverVersion string) (bool, error) { 316 minServerVersion, err := semver.Parse(m.MinServerVersion) 317 if err != nil { 318 return false, errors.New("failed to parse MinServerVersion") 319 } 320 sv := semver.MustParse(serverVersion) 321 if sv.LT(minServerVersion) { 322 return false, nil 323 } 324 return true, nil 325 } 326 327 func (m *Manifest) IsValid() error { 328 if !IsValidPluginId(m.Id) { 329 return errors.New("invalid plugin ID") 330 } 331 332 if m.HomepageURL != "" && !IsValidHttpUrl(m.HomepageURL) { 333 return errors.New("invalid HomepageURL") 334 } 335 336 if m.SupportURL != "" && !IsValidHttpUrl(m.SupportURL) { 337 return errors.New("invalid SupportURL") 338 } 339 340 if m.ReleaseNotesURL != "" && !IsValidHttpUrl(m.ReleaseNotesURL) { 341 return errors.New("invalid ReleaseNotesURL") 342 } 343 344 if m.Version != "" { 345 _, err := semver.Parse(m.Version) 346 if err != nil { 347 return errors.Wrap(err, "failed to parse Version") 348 } 349 } 350 351 if m.MinServerVersion != "" { 352 _, err := semver.Parse(m.MinServerVersion) 353 if err != nil { 354 return errors.Wrap(err, "failed to parse MinServerVersion") 355 } 356 } 357 358 if m.SettingsSchema != nil { 359 err := m.SettingsSchema.isValid() 360 if err != nil { 361 return errors.Wrap(err, "invalid settings schema") 362 } 363 } 364 365 return nil 366 } 367 368 func (s *PluginSettingsSchema) isValid() error { 369 for _, setting := range s.Settings { 370 err := setting.isValid() 371 if err != nil { 372 return err 373 } 374 } 375 376 return nil 377 } 378 379 func (s *PluginSetting) isValid() error { 380 pluginSettingType, err := convertTypeToPluginSettingType(s.Type) 381 if err != nil { 382 return err 383 } 384 385 if s.RegenerateHelpText != "" && pluginSettingType != Generated { 386 return errors.New("should not set RegenerateHelpText for setting type that is not generated") 387 } 388 389 if s.Placeholder != "" && !(pluginSettingType == Generated || 390 pluginSettingType == Text || 391 pluginSettingType == LongText || 392 pluginSettingType == Number || 393 pluginSettingType == Username) { 394 return errors.New("should not set Placeholder for setting type not in text, generated or username") 395 } 396 397 if s.Options != nil { 398 if pluginSettingType != Radio && pluginSettingType != Dropdown { 399 return errors.New("should not set Options for setting type not in radio or dropdown") 400 } 401 402 for _, option := range s.Options { 403 if option.DisplayName == "" || option.Value == "" { 404 return errors.New("should not have empty Displayname or Value for any option") 405 } 406 } 407 } 408 409 return nil 410 } 411 412 func convertTypeToPluginSettingType(t string) (PluginSettingType, error) { 413 var settingType PluginSettingType 414 switch t { 415 case "bool": 416 return Bool, nil 417 case "dropdown": 418 return Dropdown, nil 419 case "generated": 420 return Generated, nil 421 case "radio": 422 return Radio, nil 423 case "text": 424 return Text, nil 425 case "number": 426 return Number, nil 427 case "longtext": 428 return LongText, nil 429 case "username": 430 return Username, nil 431 case "custom": 432 return Custom, nil 433 default: 434 return settingType, errors.New("invalid setting type: " + t) 435 } 436 } 437 438 // FindManifest will find and parse the manifest in a given directory. 439 // 440 // In all cases other than a does-not-exist error, path is set to the path of the manifest file that was 441 // found. 442 // 443 // Manifests are JSON or YAML files named plugin.json, plugin.yaml, or plugin.yml. 444 func FindManifest(dir string) (manifest *Manifest, path string, err error) { 445 for _, name := range []string{"plugin.yml", "plugin.yaml"} { 446 path = filepath.Join(dir, name) 447 f, ferr := os.Open(path) 448 if ferr != nil { 449 if !os.IsNotExist(ferr) { 450 return nil, "", ferr 451 } 452 continue 453 } 454 b, ioerr := ioutil.ReadAll(f) 455 f.Close() 456 if ioerr != nil { 457 return nil, path, ioerr 458 } 459 var parsed Manifest 460 err = yaml.Unmarshal(b, &parsed) 461 if err != nil { 462 return nil, path, err 463 } 464 manifest = &parsed 465 manifest.Id = strings.ToLower(manifest.Id) 466 return manifest, path, nil 467 } 468 469 path = filepath.Join(dir, "plugin.json") 470 f, ferr := os.Open(path) 471 if ferr != nil { 472 if os.IsNotExist(ferr) { 473 path = "" 474 } 475 return nil, path, ferr 476 } 477 defer f.Close() 478 var parsed Manifest 479 err = json.NewDecoder(f).Decode(&parsed) 480 if err != nil { 481 return nil, path, err 482 } 483 manifest = &parsed 484 manifest.Id = strings.ToLower(manifest.Id) 485 return manifest, path, nil 486 }