package cabana import ( "net/http" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/kiosk" "git.golem15.com/golem15/summercms/modules/party" ) // The admin's read-only Plugins screen (Settings → System → Plugins): the // plugins the application was built with, their versions and, when the // plugin registry is configured, available updates and a marketplace // search. The SPA calls only these endpoints; cabana calls the registry. // PermissionViewPlugins opens the Plugins screen. No role is granted it in // code: until an administrator grants it, only superusers see the screen. const PermissionViewPlugins = "backend.view_plugins" // pluginsSettingsCode is the settings code of the Plugins screen entry and // pluginsScreenPath the SPA route it opens. const ( pluginsSettingsCode = "plugins" pluginsScreenPath = "/system/plugins" ) // States of the plugin registry on the Plugins screen. const ( // registryStatusOK: the registry answered (or is configured and was // not needed for this answer). registryStatusOK = "ok" // registryStatusDisabled: plugins.registry_url is empty; no request // leaves the process. registryStatusDisabled = "disabled" // registryStatusUnavailable: the registry did not answer in time or // answered with an error. registryStatusUnavailable = "unavailable" ) // PluginsOverview is the GET /plugins payload: the framework version the // application was built with, the state of the plugin registry and every // activated plugin in summer.yaml order. type PluginsOverview struct { Framework string `json:"framework"` Registry RegistryState `json:"registry"` Plugins []InstalledPlugin `json:"plugins"` } // RegistryState tells the admin whether the plugin registry is configured // (Enabled, false when plugins.registry_url is empty) and how it answered: // Status is ok, disabled or unavailable. type RegistryState struct { Enabled bool `json:"enabled"` Status string `json:"status"` } // InstalledPlugin is one activated plugin with the metadata summer build // baked in (party.Info) and, for a plugin the registry lists, the newest // release the built framework version allows. Name, Description, Version, // Source, Module and Package are empty for an application built before // plugin metadata existed and for a plugin without plugin.yaml (Name and // Description). Latest is the newest compatible release; UpdateAvailable // is true when it is newer than the installed version, and UpdateCommand // is then the summer command that installs it. LatestFramework names the // framework version a newer, incompatible release needs. type InstalledPlugin struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Version string `json:"version"` Source string `json:"source"` Module string `json:"module"` Package string `json:"package"` Listed bool `json:"listed"` Latest string `json:"latest"` LatestFramework string `json:"latestFramework"` UpdateAvailable bool `json:"updateAvailable"` UpdateCommand string `json:"updateCommand"` } // pluginsSettingsEntry is the Settings → System link to the Plugins // screen. It opens an SPA route, not an admin controller. func pluginsSettingsEntry(label, description, category string) SettingsEntry { return SettingsEntry{ Code: pluginsSettingsCode, Label: label, Description: description, Category: category, Icon: "puzzle", Order: 120, Keywords: []string{}, Path: pluginsScreenPath, } } // pluginsPrincipal answers 401 without a backend principal and 403 when // the principal may not open the Plugins screen; ok is false after either. func pluginsPrincipal(w http.ResponseWriter, r *http.Request) (*bouncer.Principal, bool) { principal, ok := bouncer.User(r.Context()) if !ok || principal == nil || !principal.Backend { WriteError(w, http.StatusUnauthorized, "unauthenticated", msgUnauthenticated) return nil, false } if !Allows(principal, []string{PermissionViewPlugins}) { WriteError(w, http.StatusForbidden, "forbidden", msgForbidden) return nil, false } return principal, true } // pluginsInstalled answers GET /plugins. func (s *service) pluginsInstalled(w http.ResponseWriter, r *http.Request) { if _, ok := pluginsPrincipal(w, r); !ok { return } overview := s.installedPlugins() overview.Registry = s.registryState() WriteData(w, http.StatusOK, overview, nil) } // registryState reports whether plugins.registry_url is set. func (s *service) registryState() RegistryState { if kiosk.RegistryURL(s.config()) == "" { return RegistryState{Enabled: false, Status: registryStatusDisabled} } return RegistryState{Enabled: true, Status: registryStatusOK} } // installedPlugins lists the activated plugins from the metadata summer // build published (party.Installed). An application built before that // metadata existed lists the IDs of party.Activated instead, and its // framework version comes from the binary's build information. func (s *service) installedPlugins() PluginsOverview { overview := PluginsOverview{Plugins: []InstalledPlugin{}} if s == nil || s.app == nil { overview.Framework = bonfire.BuildVersion().Framework return overview } if installed, ok := s.app.Lookup[party.Installed](); ok { overview.Framework = installed.Framework for _, info := range installed.Plugins { overview.Plugins = append(overview.Plugins, InstalledPlugin{ ID: info.ID, Name: info.Name, Description: info.Description, Version: info.Version, Source: info.Source, Module: info.Module, Package: info.Package, }) } } else if activated, ok := s.app.Lookup[party.Activated](); ok { for _, plugin := range activated { if plugin == nil || plugin.ID() == backendScreensPluginID { continue } overview.Plugins = append(overview.Plugins, InstalledPlugin{ID: plugin.ID()}) } } if overview.Framework == "" { overview.Framework = bonfire.BuildVersion().Framework } return overview }