summaryrefslogtreecommitdiffstats
path: root/app/plugins.go
diff options
context:
space:
mode:
authorJoram Wilander <jwawilander@gmail.com>2017-11-27 17:23:35 -0500
committerGitHub <noreply@github.com>2017-11-27 17:23:35 -0500
commit6176bcff6977bda71f4fde10a52dde6d7d7ceb9a (patch)
treeb4a4a22879f4b88ffc4fb59f46ca69d441569ddd /app/plugins.go
parente85ec3830164ffdfbe8fd5696ab99446b38a01ef (diff)
downloadchat-6176bcff6977bda71f4fde10a52dde6d7d7ceb9a.tar.gz
chat-6176bcff6977bda71f4fde10a52dde6d7d7ceb9a.tar.bz2
chat-6176bcff6977bda71f4fde10a52dde6d7d7ceb9a.zip
PLT-8131 (part2) Add plugin key value store support (#7902)
* Add plugin key value store support * Add localization strings * Updates per feedback
Diffstat (limited to 'app/plugins.go')
-rw-r--r--app/plugins.go649
1 files changed, 0 insertions, 649 deletions
diff --git a/app/plugins.go b/app/plugins.go
deleted file mode 100644
index 8f81086df..000000000
--- a/app/plugins.go
+++ /dev/null
@@ -1,649 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package app
-
-import (
- "context"
- "encoding/json"
- "io"
- "io/ioutil"
- "net/http"
- "os"
- "path/filepath"
- "strings"
-
- l4g "github.com/alecthomas/log4go"
-
- "github.com/gorilla/mux"
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/utils"
-
- builtinplugin "github.com/mattermost/mattermost-server/app/plugin"
- "github.com/mattermost/mattermost-server/app/plugin/jira"
- "github.com/mattermost/mattermost-server/app/plugin/ldapextras"
-
- "github.com/mattermost/mattermost-server/plugin"
- "github.com/mattermost/mattermost-server/plugin/pluginenv"
-)
-
-type PluginAPI struct {
- id string
- app *App
-}
-
-func (api *PluginAPI) LoadPluginConfiguration(dest interface{}) error {
- if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
- return err
- } else {
- return json.Unmarshal(b, dest)
- }
-}
-
-func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
- return api.app.CreateTeam(team)
-}
-
-func (api *PluginAPI) DeleteTeam(teamId string) *model.AppError {
- return api.app.SoftDeleteTeam(teamId)
-}
-
-func (api *PluginAPI) GetTeam(teamId string) (*model.Team, *model.AppError) {
- return api.app.GetTeam(teamId)
-}
-
-func (api *PluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
- return api.app.GetTeamByName(name)
-}
-
-func (api *PluginAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
- return api.app.UpdateTeam(team)
-}
-
-func (api *PluginAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
- return api.app.CreateUser(user)
-}
-
-func (api *PluginAPI) DeleteUser(userId string) *model.AppError {
- user, err := api.app.GetUser(userId)
- if err != nil {
- return err
- }
- _, err = api.app.UpdateActive(user, false)
- return err
-}
-
-func (api *PluginAPI) GetUser(userId string) (*model.User, *model.AppError) {
- return api.app.GetUser(userId)
-}
-
-func (api *PluginAPI) GetUserByEmail(email string) (*model.User, *model.AppError) {
- return api.app.GetUserByEmail(email)
-}
-
-func (api *PluginAPI) GetUserByUsername(name string) (*model.User, *model.AppError) {
- return api.app.GetUserByUsername(name)
-}
-
-func (api *PluginAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) {
- return api.app.UpdateUser(user, true)
-}
-
-func (api *PluginAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
- return api.app.CreateChannel(channel, false)
-}
-
-func (api *PluginAPI) DeleteChannel(channelId string) *model.AppError {
- channel, err := api.app.GetChannel(channelId)
- if err != nil {
- return err
- }
- return api.app.DeleteChannel(channel, "")
-}
-
-func (api *PluginAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) {
- return api.app.GetChannel(channelId)
-}
-
-func (api *PluginAPI) GetChannelByName(name, teamId string) (*model.Channel, *model.AppError) {
- return api.app.GetChannelByName(name, teamId)
-}
-
-func (api *PluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
- return api.app.GetDirectChannel(userId1, userId2)
-}
-
-func (api *PluginAPI) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
- return api.app.CreateGroupChannel(userIds, "")
-}
-
-func (api *PluginAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
- return api.app.UpdateChannel(channel)
-}
-
-func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
- return api.app.CreatePostMissingChannel(post, true)
-}
-
-func (api *PluginAPI) DeletePost(postId string) *model.AppError {
- _, err := api.app.DeletePost(postId)
- return err
-}
-
-func (api *PluginAPI) GetPost(postId string) (*model.Post, *model.AppError) {
- return api.app.GetSinglePost(postId)
-}
-
-func (api *PluginAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
- return api.app.UpdatePost(post, false)
-}
-
-type BuiltInPluginAPI struct {
- id string
- router *mux.Router
- app *App
-}
-
-func (api *BuiltInPluginAPI) LoadPluginConfiguration(dest interface{}) error {
- if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
- return err
- } else {
- return json.Unmarshal(b, dest)
- }
-}
-
-func (api *BuiltInPluginAPI) PluginRouter() *mux.Router {
- return api.router
-}
-
-func (api *BuiltInPluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
- return api.app.GetTeamByName(name)
-}
-
-func (api *BuiltInPluginAPI) GetUserByName(name string) (*model.User, *model.AppError) {
- return api.app.GetUserByUsername(name)
-}
-
-func (api *BuiltInPluginAPI) GetChannelByName(teamId, name string) (*model.Channel, *model.AppError) {
- return api.app.GetChannelByName(name, teamId)
-}
-
-func (api *BuiltInPluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
- return api.app.GetDirectChannel(userId1, userId2)
-}
-
-func (api *BuiltInPluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
- return api.app.CreatePostMissingChannel(post, true)
-}
-
-func (api *BuiltInPluginAPI) GetLdapUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError) {
- if api.app.Ldap == nil {
- return nil, model.NewAppError("GetLdapUserAttributes", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
- }
-
- user, err := api.app.GetUser(userId)
- if err != nil {
- return nil, err
- }
-
- if user.AuthData == nil {
- return map[string]string{}, nil
- }
-
- return api.app.Ldap.GetUserAttributes(*user.AuthData, attributes)
-}
-
-func (api *BuiltInPluginAPI) GetSessionFromRequest(r *http.Request) (*model.Session, *model.AppError) {
- token := ""
- isTokenFromQueryString := false
-
- // Attempt to parse token out of the header
- authHeader := r.Header.Get(model.HEADER_AUTH)
- if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER {
- // Default session token
- token = authHeader[7:]
-
- } else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN {
- // OAuth token
- token = authHeader[6:]
- }
-
- // Attempt to parse the token from the cookie
- if len(token) == 0 {
- if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
- token = cookie.Value
-
- if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
- return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
- }
- }
- }
-
- // Attempt to parse token out of the query string
- if len(token) == 0 {
- token = r.URL.Query().Get("access_token")
- isTokenFromQueryString = true
- }
-
- if len(token) == 0 {
- return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
- }
-
- session, err := api.app.GetSession(token)
-
- if err != nil {
- return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
- } else if !session.IsOAuth && isTokenFromQueryString {
- return nil, model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
- }
-
- return session, nil
-}
-
-func (api *BuiltInPluginAPI) I18n(id string, r *http.Request) string {
- if r != nil {
- f, _ := utils.GetTranslationsAndLocale(nil, r)
- return f(id)
- }
- f, _ := utils.GetTranslationsBySystemLocale()
- return f(id)
-}
-
-func (a *App) initBuiltInPlugins() {
- plugins := map[string]builtinplugin.Plugin{
- "jira": &jira.Plugin{},
- "ldapextras": &ldapextras.Plugin{},
- }
- for id, p := range plugins {
- l4g.Info("Initializing plugin: " + id)
- api := &BuiltInPluginAPI{
- id: id,
- router: a.Srv.Router.PathPrefix("/plugins/" + id).Subrouter(),
- app: a,
- }
- p.Initialize(api)
- }
- utils.AddConfigListener(func(before, after *model.Config) {
- for _, p := range plugins {
- p.OnConfigurationChange()
- }
- })
- for _, p := range plugins {
- p.OnConfigurationChange()
- }
-}
-
-// ActivatePlugins will activate any plugins enabled in the config
-// and deactivate all other plugins.
-func (a *App) ActivatePlugins() {
- if a.PluginEnv == nil {
- l4g.Error("plugin env not initialized")
- return
- }
-
- plugins, err := a.PluginEnv.Plugins()
- if err != nil {
- l4g.Error("failed to activate plugins: " + err.Error())
- return
- }
-
- for _, plugin := range plugins {
- id := plugin.Manifest.Id
-
- pluginState := &model.PluginState{Enable: false}
- if state, ok := a.Config().PluginSettings.PluginStates[id]; ok {
- pluginState = state
- }
-
- active := a.PluginEnv.IsPluginActive(id)
-
- if pluginState.Enable && !active {
- if err := a.PluginEnv.ActivatePlugin(id); err != nil {
- l4g.Error(err.Error())
- continue
- }
-
- if plugin.Manifest.HasClient() {
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil)
- message.Add("manifest", plugin.Manifest.ClientManifest())
- a.Publish(message)
- }
-
- l4g.Info("Activated %v plugin", id)
- } else if !pluginState.Enable && active {
- if err := a.PluginEnv.DeactivatePlugin(id); err != nil {
- l4g.Error(err.Error())
- continue
- }
-
- if plugin.Manifest.HasClient() {
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
- message.Add("manifest", plugin.Manifest.ClientManifest())
- a.Publish(message)
- }
-
- l4g.Info("Deactivated %v plugin", id)
- }
- }
-}
-
-// InstallPlugin unpacks and installs a plugin but does not activate it.
-func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
- if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
- return nil, model.NewAppError("InstallPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
- }
-
- tmpDir, err := ioutil.TempDir("", "plugintmp")
- if err != nil {
- return nil, model.NewAppError("InstallPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
- defer os.RemoveAll(tmpDir)
-
- if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
- return nil, model.NewAppError("InstallPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
- }
-
- tmpPluginDir := tmpDir
- dir, err := ioutil.ReadDir(tmpDir)
- if err != nil {
- return nil, model.NewAppError("InstallPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
-
- if len(dir) == 1 && dir[0].IsDir() {
- tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name())
- }
-
- manifest, _, err := model.FindManifest(tmpPluginDir)
- if err != nil {
- return nil, model.NewAppError("InstallPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
- }
-
- bundles, err := a.PluginEnv.Plugins()
- if err != nil {
- return nil, model.NewAppError("InstallPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
-
- for _, bundle := range bundles {
- if bundle.Manifest.Id == manifest.Id {
- return nil, model.NewAppError("InstallPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
- }
- }
-
- err = utils.CopyDir(tmpPluginDir, filepath.Join(a.PluginEnv.SearchPath(), manifest.Id))
- if err != nil {
- return nil, model.NewAppError("InstallPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
-
- // Should add manifest validation and error handling here
-
- return manifest, nil
-}
-
-func (a *App) GetPluginManifests() (*model.PluginsResponse, *model.AppError) {
- if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
- return nil, model.NewAppError("GetPluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
- }
-
- plugins, err := a.PluginEnv.Plugins()
- if err != nil {
- return nil, model.NewAppError("GetPluginManifests", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
-
- resp := &model.PluginsResponse{Active: []*model.Manifest{}, Inactive: []*model.Manifest{}}
- for _, plugin := range plugins {
- if a.PluginEnv.IsPluginActive(plugin.Manifest.Id) {
- resp.Active = append(resp.Active, plugin.Manifest)
- } else {
- resp.Inactive = append(resp.Inactive, plugin.Manifest)
- }
- }
-
- return resp, nil
-}
-
-func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
- if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
- return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
- }
-
- plugins := a.PluginEnv.ActivePlugins()
-
- manifests := make([]*model.Manifest, len(plugins))
- for i, plugin := range plugins {
- manifests[i] = plugin.Manifest
- }
-
- return manifests, nil
-}
-
-func (a *App) RemovePlugin(id string) *model.AppError {
- if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
- return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
- }
-
- plugins, err := a.PluginEnv.Plugins()
- if err != nil {
- return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
- }
-
- var manifest *model.Manifest
- for _, p := range plugins {
- if p.Manifest.Id == id {
- manifest = p.Manifest
- break
- }
- }
-
- if manifest == nil {
- return model.NewAppError("RemovePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
- }
-
- if a.PluginEnv.IsPluginActive(id) {
- err := a.PluginEnv.DeactivatePlugin(id)
- if err != nil {
- return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
- }
-
- if manifest.HasClient() {
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
- message.Add("manifest", manifest.ClientManifest())
- a.Publish(message)
- }
- }
-
- err = os.RemoveAll(filepath.Join(a.PluginEnv.SearchPath(), id))
- if err != nil {
- return model.NewAppError("RemovePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
-
- return nil
-}
-
-// EnablePlugin will set the config for an installed plugin to enabled, triggering activation if inactive.
-func (a *App) EnablePlugin(id string) *model.AppError {
- if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
- return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
- }
-
- plugins, err := a.PluginEnv.Plugins()
- if err != nil {
- return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
-
- var manifest *model.Manifest
- for _, p := range plugins {
- if p.Manifest.Id == id {
- manifest = p.Manifest
- break
- }
- }
-
- if manifest == nil {
- return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
- }
-
- a.UpdateConfig(func(cfg *model.Config) {
- cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true}
- })
-
- if err := a.SaveConfig(a.Config(), true); err != nil {
- return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
-
- return nil
-}
-
-// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
-func (a *App) DisablePlugin(id string) *model.AppError {
- if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
- return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
- }
-
- plugins, err := a.PluginEnv.Plugins()
- if err != nil {
- return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
-
- var manifest *model.Manifest
- for _, p := range plugins {
- if p.Manifest.Id == id {
- manifest = p.Manifest
- break
- }
- }
-
- if manifest == nil {
- return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
- }
-
- a.UpdateConfig(func(cfg *model.Config) {
- cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false}
- })
-
- if err := a.SaveConfig(a.Config(), true); err != nil {
- return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
- }
-
- return nil
-}
-
-func (a *App) InitPlugins(pluginPath, webappPath string) {
- if !*a.Config().PluginSettings.Enable {
- return
- }
-
- if a.PluginEnv != nil {
- return
- }
-
- l4g.Info("Starting up plugins")
-
- err := os.Mkdir(pluginPath, 0744)
- if err != nil && !os.IsExist(err) {
- l4g.Error("failed to start up plugins: " + err.Error())
- return
- }
-
- err = os.Mkdir(webappPath, 0744)
- if err != nil && !os.IsExist(err) {
- l4g.Error("failed to start up plugins: " + err.Error())
- return
- }
-
- a.PluginEnv, err = pluginenv.New(
- pluginenv.SearchPath(pluginPath),
- pluginenv.WebappPath(webappPath),
- pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) {
- return &PluginAPI{
- id: m.Id,
- app: a,
- }, nil
- }),
- )
-
- if err != nil {
- l4g.Error("failed to start up plugins: " + err.Error())
- return
- }
-
- utils.RemoveConfigListener(a.PluginConfigListenerId)
- a.PluginConfigListenerId = utils.AddConfigListener(func(prevCfg, cfg *model.Config) {
- if a.PluginEnv == nil {
- return
- }
-
- if *prevCfg.PluginSettings.Enable && *cfg.PluginSettings.Enable {
- a.ActivatePlugins()
- }
-
- for _, err := range a.PluginEnv.Hooks().OnConfigurationChange() {
- l4g.Error(err.Error())
- }
- })
-
- a.ActivatePlugins()
-}
-
-func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
- if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
- err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented)
- err.Translate(utils.T)
- l4g.Error(err.Error())
- w.WriteHeader(err.StatusCode)
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(err.ToJson()))
- return
- }
-
- token := ""
-
- authHeader := r.Header.Get(model.HEADER_AUTH)
- if strings.HasPrefix(strings.ToUpper(authHeader), model.HEADER_BEARER+":") {
- token = authHeader[len(model.HEADER_BEARER)+1:]
- } else if strings.HasPrefix(strings.ToLower(authHeader), model.HEADER_TOKEN+":") {
- token = authHeader[len(model.HEADER_TOKEN)+1:]
- } else if cookie, _ := r.Cookie(model.SESSION_COOKIE_TOKEN); cookie != nil && (r.Method == "GET" || r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML) {
- token = cookie.Value
- } else {
- token = r.URL.Query().Get("access_token")
- }
-
- r.Header.Del("Mattermost-User-Id")
- if token != "" {
- if session, err := a.GetSession(token); err != nil {
- r.Header.Set("Mattermost-User-Id", session.UserId)
- }
- }
-
- cookies := r.Cookies()
- r.Header.Del("Cookie")
- for _, c := range cookies {
- if c.Name != model.SESSION_COOKIE_TOKEN {
- r.AddCookie(c)
- }
- }
- r.Header.Del(model.HEADER_AUTH)
- r.Header.Del("Referer")
-
- newQuery := r.URL.Query()
- newQuery.Del("access_token")
- r.URL.RawQuery = newQuery.Encode()
-
- params := mux.Vars(r)
- a.PluginEnv.Hooks().ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "plugin_id", params["plugin_id"])))
-}
-
-func (a *App) ShutDownPlugins() {
- if a.PluginEnv == nil {
- return
- }
-
- l4g.Info("Shutting down plugins")
-
- for _, err := range a.PluginEnv.Shutdown() {
- l4g.Error(err.Error())
- }
- utils.RemoveConfigListener(a.PluginConfigListenerId)
- a.PluginConfigListenerId = ""
- a.PluginEnv = nil
-}