Merge pull request #9618 from acrogenesis/security/plugin-auth-boundary

Restrict third-party plugin access to authentication services
This commit is contained in:
Ryan Hughes authored and GitHub committed 2026-09-07 03:14:48 -04:00
commit e78d89ee2a
27 files changed
+2016 -58

No files matched your search

+704 -29
View File
@@ -7,6 +7,7 @@ import qs.Commons
import "plugins/bar"
import "services"
import "services/AuthServiceStore.js" as AuthServiceStore
ShellRoot {
id: shell
@@ -113,7 +114,10 @@ ShellRoot {
}
readonly property var barConfig: shellConfig && Util.isPlainObject(shellConfig.bar) ? shellConfig.bar : builtinShellConfig.bar
onBarConfigChanged: if (bar && "barConfig" in bar) bar.barConfig = shell.barConfig
onBarConfigChanged: {
if (bar && "barConfig" in bar)
bar.barConfig = shell.barConfigFor(shell.activeBarManifest)
}
FileView {
id: defaultsFile
path: shell.defaultsPath
@@ -214,11 +218,11 @@ ShellRoot {
function configureBar(target, manifest) {
if (!target) return
if ("omarchyPath" in target) target.omarchyPath = shell.omarchyPath
if ("shell" in target) target.shell = shell
if ("manifest" in target) target.manifest = manifest
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.barWidgetRegistry
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistry
if ("barConfig" in target) target.barConfig = shell.barConfig
if ("shell" in target) target.shell = shell.pluginShellFor(manifest)
if ("manifest" in target) target.manifest = shell.publicPluginManifest(manifest)
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(manifest)
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistryFor(manifest)
if ("barConfig" in target) target.barConfig = shell.barConfigFor(manifest)
shell.bar = target
}
@@ -253,8 +257,7 @@ ShellRoot {
onActiveChanged: if (!active) shell.bar = null
onStatusChanged: {
if (status === Loader.Error) {
var detail = errorString && errorString() ? errorString() : ""
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId + ":", detail)
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId)
shell.failedBarId = shell.activeBarId
}
}
@@ -271,6 +274,599 @@ ShellRoot {
}
property var _services: ({})
property var _pluginShellApis: ({})
property var _pluginShellApiDescriptors: ({})
property var _pluginBarEntryShellApis: ({})
property var _pluginRegistryApis: ({})
property var _pluginBarWidgetRegistryApis: ({})
property var _pluginAppLibraryApis: ({})
property var _pluginBarStateApis: ({})
property var _pluginFirstPartyServiceApis: ({})
Component {
id: pluginShellApiComponent
PluginShellApi { }
}
Component {
id: pluginRegistryApiComponent
PluginRegistryApi { }
}
Component {
id: pluginBarWidgetRegistryApiComponent
PluginBarWidgetRegistryApi { }
}
Component {
id: pluginAppLibraryApiComponent
PluginAppLibraryApi { }
}
Component {
id: pluginBarStateApiComponent
PluginBarStateApi { }
}
Component {
id: pluginFirstPartyServiceApiComponent
PluginFirstPartyServiceApi { }
}
function publicPluginManifest(manifest) {
if (!manifest) return null
if (manifest.__isFirstParty) return manifest
var copy = JSON.parse(JSON.stringify(manifest))
delete copy.__sourceDir
delete copy.__isFirstParty
delete copy.__hostCapabilities
return copy
}
function publicBarConfig() {
return JSON.parse(JSON.stringify(shell.barConfig || {}))
}
function barConfigFor(manifest) {
return !manifest || manifest.__isFirstParty
? shell.barConfig : shell.publicBarConfig()
}
function publicBarWidgetSnapshot() {
var source = shell.barWidgetRegistry.widgets || {}
var snapshot = {}
for (var id in source) {
var entry = source[id]
if (!entry) continue
snapshot[id] = {
component: entry.component,
metadata: JSON.parse(JSON.stringify(entry.metadata || {}))
}
}
return snapshot
}
function manifestHasKind(manifest, kind) {
return !!manifest && Array.isArray(manifest.kinds)
&& manifest.kinds.indexOf(kind) !== -1
}
function pluginHasBarCapabilities(manifest) {
return shell.manifestHasKind(manifest, "bar")
}
function publicIdleConfigFor(manifest) {
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
if (!metadata || String(metadata.clonedFrom || "") !== "omarchy.idle") return ({})
var idle = shell.shellConfig && Util.isPlainObject(shell.shellConfig.idle)
? shell.shellConfig.idle : ({})
return JSON.parse(JSON.stringify(idle))
}
function pluginCloneMaySummon(manifest, requestedId) {
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
var sourceId = metadata ? String(metadata.clonedFrom || "") : ""
var allowed = {
"omarchy.audio": ["omarchy.osd"],
"omarchy.media": ["omarchy.osd"],
"omarchy.monitor": ["omarchy.osd"],
"omarchy.network": ["omarchy.speedtest", "omarchy.wifiqr"]
}
var targets = allowed[sourceId] || []
return targets.indexOf(String(requestedId || "")) !== -1
}
function pluginOwnsTarget(pluginId, requestedId) {
var caller = String(pluginId || "")
if (!caller) return false
return shell.pluginRegistry.resolveEnabledId(String(requestedId || "")) === caller
}
function pluginServiceFor(pluginId, requestedId) {
if (!shell.pluginOwnsTarget(pluginId, requestedId)) return null
return shell.serviceFor(shell.pluginRegistry.resolveEnabledId(requestedId))
}
function barEntryConfigured(pluginId) {
var location = shell.pluginRegistry.findEntryLocation(shell.shellConfig, pluginId)
return location && location.kind === "bar"
}
function barPluginMayControl(manifest, requestedId) {
if (!shell.pluginHasBarCapabilities(manifest)) return false
var id = shell.pluginRegistry.resolveEnabledId(String(requestedId || ""))
var target = shell.pluginRegistry.installedPlugins[id]
if (!target || shell.isAuthenticationService(target, id)) return false
if (shell.barEntryConfigured(id)) return true
var uiKinds = ["bar-widget", "panel", "overlay", "menu"]
for (var i = 0; i < uiKinds.length; i++)
if (shell.manifestHasKind(target, uiKinds[i])) return true
return false
}
function mutatePluginBarConfig(mutator) {
if (typeof mutator !== "function") return false
shell.mutateShellConfig(function(config) {
var scoped = { bar: JSON.parse(JSON.stringify(config.bar || {})) }
mutator(scoped)
if (Util.isPlainObject(scoped.bar)) config.bar = JSON.parse(JSON.stringify(scoped.bar))
})
return true
}
function pluginAppLibraryFor(cacheKey, pluginId) {
if (_pluginAppLibraryApis[cacheKey]) return _pluginAppLibraryApis[cacheKey]
var api = pluginAppLibraryApiComponent.createObject(null, {
ownerPluginId: pluginId,
_entryName: function(entry) { return shell.appLibrary.entryName(entry) },
_entrySubtext: function(entry) { return shell.appLibrary.entrySubtext(entry) },
_sortedEntries: function(query) { return shell.appLibrary.sortedEntries(query) },
_iconSource: function(icon) { return shell.appLibrary.iconSource(icon) },
_refreshIcons: function() { shell.appLibrary.refreshIcons() },
_launch: function(desktopId, name) { shell.appLibrary.launch(desktopId, name) },
_remove: function(desktopId, name) { shell.appLibrary.remove(desktopId, name) }
})
if (!api) return null
var next = ({})
for (var id in _pluginAppLibraryApis) next[id] = _pluginAppLibraryApis[id]
next[cacheKey] = api
_pluginAppLibraryApis = next
return api
}
function pluginBarStateFor(cacheKey, pluginId) {
if (_pluginBarStateApis[cacheKey]) return _pluginBarStateApis[cacheKey]
var api = pluginBarStateApiComponent.createObject(null, { ownerPluginId: pluginId })
if (!api) return null
api.barHidden = Qt.binding(function() { return shell.bar ? shell.bar.barHidden === true : false })
api.barSize = Qt.binding(function() { return shell.bar ? Math.max(0, shell.bar.barSize || 0) : 0 })
api.fontFamily = Qt.binding(function() { return shell.bar ? String(shell.bar.fontFamily || "") : "" })
api.position = Qt.binding(function() { return shell.bar ? String(shell.bar.position || "top") : "top" })
var next = ({})
for (var id in _pluginBarStateApis) next[id] = _pluginBarStateApis[id]
next[cacheKey] = api
_pluginBarStateApis = next
return api
}
function pluginFirstPartyServiceFor(cacheKey, pluginId, requestedId) {
var id = String(requestedId || "")
var allowed = ["omarchy.idle", "omarchy.media", "omarchy.nightlight", "omarchy.notifications"]
if (allowed.indexOf(id) === -1) return null
var proxyKey = cacheKey + "::" + id
if (_pluginFirstPartyServiceApis[proxyKey]) return _pluginFirstPartyServiceApis[proxyKey]
function service() {
return shell.serviceFor(shell.pluginRegistry.resolveEnabledId(id))
}
var api = pluginFirstPartyServiceApiComponent.createObject(null, {
ownerPluginId: pluginId,
serviceId: id,
_setIdleEnabled: function(value) {
var target = service()
if (target && typeof target.setIdleEnabled === "function") target.setIdleEnabled(value)
},
_setNightlight: function(value) {
var target = service()
if (target && typeof target.setNightlight === "function") target.setNightlight(value)
},
_setDoNotDisturb: function(value) {
var target = service()
if (target && typeof target.setDoNotDisturb === "function") target.setDoNotDisturb(value)
},
_runAction: function(action, showFeedback, targetKey) {
var target = service()
if (target && typeof target.runAction === "function") target.runAction(action, showFeedback, targetKey)
},
_playerKey: function(player) {
var target = service()
return target && typeof target.playerKey === "function" ? target.playerKey(player) : ""
},
_selectPlayer: function(playerKey) {
var target = service()
if (target && typeof target.selectPlayer === "function") target.selectPlayer(playerKey)
}
})
if (!api) return null
api.stayAwake = Qt.binding(function() {
var target = service()
return target ? target.stayAwake === true : false
})
api.enabled = Qt.binding(function() {
var target = service()
return target ? target.enabled === true : false
})
api.doNotDisturb = Qt.binding(function() {
var target = service()
return target ? target.doNotDisturb === true : false
})
api.activePlayer = Qt.binding(function() {
var target = service()
return target ? target.activePlayer : null
})
api.sourcePlayers = Qt.binding(function() {
var target = service()
return target && Array.isArray(target.sourcePlayers) ? target.sourcePlayers : []
})
var next = ({})
for (var existing in _pluginFirstPartyServiceApis) next[existing] = _pluginFirstPartyServiceApis[existing]
next[proxyKey] = api
_pluginFirstPartyServiceApis = next
return api
}
function pluginShellCapabilityProfile(manifest, allowOwnService, barCapabilities) {
return [
allowOwnService ? "own-service" : "no-own-service",
barCapabilities ? "bar" : "no-bar",
shell.manifestHasKind(manifest, "menu") ? "menu" : "no-menu"
].join("|")
}
function cacheWithoutKey(cache, key, destroyValue) {
var next = ({})
for (var existing in cache) {
if (existing === key) {
var value = cache[existing]
if (destroyValue && value && typeof value.destroy === "function") value.destroy()
} else {
next[existing] = cache[existing]
}
}
return next
}
function cacheWithoutPrefix(cache, prefix) {
var next = ({})
for (var existing in cache) {
if (existing.indexOf(prefix) === 0) {
var value = cache[existing]
if (value && typeof value.destroy === "function") value.destroy()
} else {
next[existing] = cache[existing]
}
}
return next
}
function revokePluginShellApi(cacheKey) {
var key = String(cacheKey || "")
if (!key) return
_pluginAppLibraryApis = shell.cacheWithoutKey(_pluginAppLibraryApis, key, true)
_pluginFirstPartyServiceApis = shell.cacheWithoutPrefix(_pluginFirstPartyServiceApis, key + "::")
_pluginBarEntryShellApis = shell.cacheWithoutPrefix(_pluginBarEntryShellApis, key + ":")
_pluginShellApis = shell.cacheWithoutKey(_pluginShellApis, key, true)
_pluginShellApiDescriptors = shell.cacheWithoutKey(_pluginShellApiDescriptors, key, false)
}
function createScopedPluginShell(manifest, cacheKey, allowOwnService, barCapabilities) {
var key = String(manifest && manifest.id || "")
if (!key) return null
var profile = shell.pluginShellCapabilityProfile(manifest, allowOwnService, barCapabilities)
var cached = _pluginShellApis[cacheKey]
var descriptor = _pluginShellApiDescriptors[cacheKey]
if (cached && descriptor && descriptor.pluginId === key
&& descriptor.profile === profile) return cached
if (cached || descriptor) shell.revokePluginShellApi(cacheKey)
function currentManifest() {
return shell.pluginRegistry.installedPlugins[key] || null
}
function hasCurrentBarCapabilities() {
return barCapabilities && shell.pluginHasBarCapabilities(currentManifest())
}
// Construct the narrow service proxies before any plugin binding can call
// firstPartyServiceFor(). Creating a QObject while evaluating that binding
// makes QML re-enter the binding and report a loop on the caller's service
// property, even though the resulting proxy is otherwise acyclic.
var firstPartyServices = ({})
if (barCapabilities) {
var serviceIds = ["omarchy.idle", "omarchy.media", "omarchy.nightlight", "omarchy.notifications"]
for (var i = 0; i < serviceIds.length; i++) {
var serviceId = serviceIds[i]
firstPartyServices[serviceId] = shell.pluginFirstPartyServiceFor(cacheKey, key, serviceId)
}
}
var api = pluginShellApiComponent.createObject(null, {
pluginId: key,
appLibrary: shell.manifestHasKind(manifest, "menu")
? shell.pluginAppLibraryFor(cacheKey, key) : null,
bar: shell.pluginBarStateFor(cacheKey, key),
barConfig: shell.publicBarConfig(),
idleConfig: shell.publicIdleConfigFor(manifest),
_serviceLookup: function(requestedId) {
return allowOwnService ? shell.pluginServiceFor(key, requestedId) : null
},
_firstPartyServiceLookup: function(requestedId) {
if (allowOwnService && shell.pluginOwnsTarget(key, requestedId))
return shell.pluginServiceFor(key, requestedId)
return hasCurrentBarCapabilities() ? (firstPartyServices[requestedId] || null) : null
},
_barEntryShellLookup: function(ownerId, moduleName) {
return hasCurrentBarCapabilities()
? shell.pluginShellForBarEntry(cacheKey + ":" + ownerId, moduleName) : null
},
_summon: function(requestedId, payloadJson) {
if (!shell.pluginOwnsTarget(key, requestedId)
&& !shell.barPluginMayControl(currentManifest(), requestedId)
&& !shell.pluginCloneMaySummon(currentManifest(), requestedId)) return false
return shell.summon(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
},
_hide: function(requestedId) {
if (!shell.pluginOwnsTarget(key, requestedId)
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
return shell.hide(shell.pluginRegistry.resolveEnabledId(requestedId))
},
_toggle: function(requestedId, payloadJson) {
if (!shell.pluginOwnsTarget(key, requestedId)
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
return shell.toggle(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
},
_isOpen: function(requestedId) {
if (!shell.pluginOwnsTarget(key, requestedId)
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
return shell.isPluginOpen(shell.pluginRegistry.resolveEnabledId(requestedId))
},
_updateSettings: function(requestedId, settings) {
if (shell.pluginOwnsTarget(key, requestedId)) return shell.updateEntryInline(key, settings)
if (hasCurrentBarCapabilities() && shell.barEntryConfigured(requestedId))
return shell.updateEntryInline(requestedId, settings)
return false
},
_mutateBarConfig: function(mutator) {
return hasCurrentBarCapabilities() ? shell.mutatePluginBarConfig(mutator) : false
}
})
if (!api) return null
var next = ({})
for (var id in _pluginShellApis) next[id] = _pluginShellApis[id]
next[cacheKey] = api
_pluginShellApis = next
var descriptorNext = ({})
for (var descriptorKey in _pluginShellApiDescriptors)
descriptorNext[descriptorKey] = _pluginShellApiDescriptors[descriptorKey]
descriptorNext[cacheKey] = {
pluginId: key,
allowOwnService: allowOwnService === true,
profile: profile
}
_pluginShellApiDescriptors = descriptorNext
return api
}
function scopedPluginShellForId(pluginId) {
var key = String(pluginId || "")
var manifest = shell.pluginRegistry.installedPlugins[key]
if (!manifest) return null
if (!manifest.__isFirstParty) return shell.pluginShellFor(manifest)
return shell.createScopedPluginShell(manifest, "hosted:" + key, false, false)
}
function pluginShellForId(pluginId) {
return shell.scopedPluginShellForId(pluginId)
}
function pluginShellForBarEntry(ownerId, moduleName) {
var owner = String(ownerId || "")
var target = String(moduleName || "")
if (!owner || !target) return null
if (!shell.barEntryConfigured(target)) return null
var cacheKey = owner + "::" + target
if (_pluginBarEntryShellApis[cacheKey]) return _pluginBarEntryShellApis[cacheKey]
function owns(requestedId) {
return shell.pluginRegistry.resolveEnabledId(String(requestedId || ""))
=== shell.pluginRegistry.resolveEnabledId(target)
}
function currentManifest() {
var id = shell.pluginRegistry.resolveEnabledId(target)
return shell.pluginRegistry.installedPlugins[id] || null
}
var api = pluginShellApiComponent.createObject(null, {
pluginId: target,
barConfig: shell.publicBarConfig(),
_summon: function(requestedId, payloadJson) {
if (!owns(requestedId)
&& !shell.pluginCloneMaySummon(currentManifest(), requestedId)) return false
return shell.summon(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
},
_hide: function(requestedId) {
return owns(requestedId)
? shell.hide(shell.pluginRegistry.resolveEnabledId(target)) : false
},
_toggle: function(requestedId, payloadJson) {
return owns(requestedId)
? shell.toggle(shell.pluginRegistry.resolveEnabledId(target), payloadJson) : false
},
_isOpen: function(requestedId) {
return owns(requestedId)
? shell.isPluginOpen(shell.pluginRegistry.resolveEnabledId(target)) : false
},
_updateSettings: function(requestedId, settings) {
return String(requestedId || "") === target
? shell.updateEntryInline(target, settings) : false
}
})
if (!api) return null
var next = ({})
for (var id in _pluginBarEntryShellApis) next[id] = _pluginBarEntryShellApis[id]
next[cacheKey] = api
_pluginBarEntryShellApis = next
return api
}
function pluginShellFor(manifest) {
if (!manifest || manifest.__isFirstParty) return shell
var key = String(manifest.id || "")
if (!key) return null
return shell.createScopedPluginShell(manifest, key, true, shell.pluginHasBarCapabilities(manifest))
}
function pluginRegistryFor(manifest) {
if (!manifest || manifest.__isFirstParty) return shell.pluginRegistry
var key = String(manifest.id || "")
if (!key) return null
if (_pluginRegistryApis[key]) return _pluginRegistryApis[key]
var api = pluginRegistryApiComponent.createObject(null, {
pluginId: key,
manifest: shell.publicPluginManifest(manifest),
enabled: shell.pluginRegistry.isEnabled(key),
_entryPointUrl: function(kind) {
var current = shell.pluginRegistry.installedPlugins[key]
return current ? shell.pluginRegistry.entryPointUrl(current, kind) : ""
}
})
if (!api) return null
var next = ({})
for (var id in _pluginRegistryApis) next[id] = _pluginRegistryApis[id]
next[key] = api
_pluginRegistryApis = next
return api
}
function pluginBarWidgetRegistryFor(manifest) {
if (!manifest || manifest.__isFirstParty) return shell.barWidgetRegistry
var key = String(manifest.id || "")
if (!key) return null
if (_pluginBarWidgetRegistryApis[key]) return _pluginBarWidgetRegistryApis[key]
var api = pluginBarWidgetRegistryApiComponent.createObject(null, {
widgets: shell.publicBarWidgetSnapshot(),
revision: shell.barWidgetRegistry.revision
})
if (!api) return null
var next = ({})
for (var id in _pluginBarWidgetRegistryApis) next[id] = _pluginBarWidgetRegistryApis[id]
next[key] = api
_pluginBarWidgetRegistryApis = next
return api
}
function pluginApiActive(api, plugins) {
var id = api ? String(api.pluginId || api.ownerPluginId || "") : ""
var manifest = id ? plugins[id] : null
return !!manifest && shell.pluginRegistry.isEnabled(id)
}
function prunePluginApis() {
var plugins = shell.pluginRegistry.installedPlugins
var shellKeys = Object.keys(_pluginShellApis)
for (var si = 0; si < shellKeys.length; si++) {
var shellKey = shellKeys[si]
var shellApi = _pluginShellApis[shellKey]
var descriptor = _pluginShellApiDescriptors[shellKey]
var manifest = descriptor ? plugins[descriptor.pluginId] : null
var barCapabilities = descriptor && descriptor.allowOwnService
&& shell.pluginHasBarCapabilities(manifest)
var expectedProfile = descriptor
? shell.pluginShellCapabilityProfile(manifest, descriptor.allowOwnService, barCapabilities) : ""
var active = descriptor && manifest && shell.pluginRegistry.isEnabled(descriptor.pluginId)
if (!active || descriptor.profile !== expectedProfile)
shell.revokePluginShellApi(shellKey)
}
var registryNext = ({})
for (var registryKey in _pluginRegistryApis) {
var registryApi = _pluginRegistryApis[registryKey]
if (shell.pluginApiActive(registryApi, plugins)) registryNext[registryKey] = registryApi
else if (registryApi && typeof registryApi.destroy === "function") registryApi.destroy()
}
_pluginRegistryApis = registryNext
var widgetNext = ({})
for (var widgetKey in _pluginBarWidgetRegistryApis) {
var widgetApi = _pluginBarWidgetRegistryApis[widgetKey]
if (plugins[widgetKey] && shell.pluginRegistry.isEnabled(widgetKey)) widgetNext[widgetKey] = widgetApi
else if (widgetApi && typeof widgetApi.destroy === "function") widgetApi.destroy()
}
_pluginBarWidgetRegistryApis = widgetNext
var appNext = ({})
for (var appKey in _pluginAppLibraryApis) {
var appApi = _pluginAppLibraryApis[appKey]
if (shell.pluginApiActive(appApi, plugins)) appNext[appKey] = appApi
else if (appApi && typeof appApi.destroy === "function") appApi.destroy()
}
_pluginAppLibraryApis = appNext
var barStateNext = ({})
for (var barStateKey in _pluginBarStateApis) {
var barStateApi = _pluginBarStateApis[barStateKey]
if (shell.pluginApiActive(barStateApi, plugins)) barStateNext[barStateKey] = barStateApi
else if (barStateApi && typeof barStateApi.destroy === "function") barStateApi.destroy()
}
_pluginBarStateApis = barStateNext
var serviceNext = ({})
for (var serviceKey in _pluginFirstPartyServiceApis) {
var serviceApi = _pluginFirstPartyServiceApis[serviceKey]
if (shell.pluginApiActive(serviceApi, plugins)) serviceNext[serviceKey] = serviceApi
else if (serviceApi && typeof serviceApi.destroy === "function") serviceApi.destroy()
}
_pluginFirstPartyServiceApis = serviceNext
var entryNext = ({})
for (var entryKey in _pluginBarEntryShellApis) {
var entryApi = _pluginBarEntryShellApis[entryKey]
if (entryApi && shell.barEntryConfigured(entryApi.pluginId)) entryNext[entryKey] = entryApi
else if (entryApi && typeof entryApi.destroy === "function") entryApi.destroy()
}
_pluginBarEntryShellApis = entryNext
}
function syncPluginApis() {
shell.prunePluginApis()
var plugins = shell.pluginRegistry.installedPlugins
for (var id in _pluginRegistryApis) {
var registryApi = _pluginRegistryApis[id]
var manifest = plugins[id]
registryApi.manifest = shell.publicPluginManifest(manifest)
registryApi.enabled = !!manifest && shell.pluginRegistry.isEnabled(id)
}
for (var widgetId in _pluginBarWidgetRegistryApis) {
var widgetApi = _pluginBarWidgetRegistryApis[widgetId]
widgetApi.widgets = shell.publicBarWidgetSnapshot()
widgetApi.revision = shell.barWidgetRegistry.revision
}
for (var shellKey in _pluginShellApis) {
var shellApi = _pluginShellApis[shellKey]
var descriptor = _pluginShellApiDescriptors[shellKey]
var shellManifest = descriptor ? plugins[descriptor.pluginId] : null
shellApi.barConfig = shell.publicBarConfig()
shellApi.idleConfig = shell.publicIdleConfigFor(shellManifest)
}
for (var entryKey in _pluginBarEntryShellApis)
_pluginBarEntryShellApis[entryKey].barConfig = shell.publicBarConfig()
}
// Reassigned as each service registers, so a binding that reads this before
// looking a service up by id re-evaluates once that service exists.
@@ -281,7 +877,14 @@ ShellRoot {
}
function firstPartyServiceFor(pluginId) {
return serviceFor(pluginId)
return serviceFor(shell.pluginRegistry.resolveEnabledId(pluginId))
}
function isAuthenticationService(manifest, pluginId) {
var key = String(pluginId || (manifest && manifest.id) || "")
return AuthServiceStore.isTrusted(key)
|| (!!manifest && Array.isArray(manifest.__hostCapabilities)
&& manifest.__hostCapabilities.indexOf("authentication") !== -1)
}
function ensureService(pluginId) {
@@ -294,6 +897,8 @@ ShellRoot {
if (!manifest.entryPoints || !manifest.entryPoints.service) return null
var url = pluginRegistry.entryPointUrl(manifest, "service")
if (!url) return null
var authenticationService = shell.isAuthenticationService(manifest, key)
if (authenticationService && AuthServiceStore.has(key)) return null
var comp = Qt.createComponent(url, Component.PreferSynchronous)
function finalize() {
@@ -301,27 +906,37 @@ ShellRoot {
console.warn("service plugin load failed for " + key + ": " + comp.errorString())
return
}
var inst = comp.createObject(serviceHost)
// Authentication services and third-party services have no visual
// parent. Parenting either to serviceHost would let a plugin's object
// traversal walk between the host and credential-bearing QML.
var inst = comp.createObject(manifest.__isFirstParty && !authenticationService ? serviceHost : null)
if (!inst) {
console.warn("service plugin createObject returned null for", key)
return
}
if ("omarchyPath" in inst) inst.omarchyPath = shell.omarchyPath
if ("shell" in inst) inst.shell = shell
if ("manifest" in inst) inst.manifest = manifest
if ("barWidgetRegistry" in inst) inst.barWidgetRegistry = shell.barWidgetRegistry
if ("pluginRegistry" in inst) inst.pluginRegistry = shell.pluginRegistry
var snext = ({})
for (var sk in _services) snext[sk] = _services[sk]
snext[key] = inst
_services = snext
if ("shell" in inst) inst.shell = shell.pluginShellFor(manifest)
if ("manifest" in inst) inst.manifest = shell.publicPluginManifest(manifest)
if ("barWidgetRegistry" in inst) inst.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(manifest)
if ("pluginRegistry" in inst) inst.pluginRegistry = shell.pluginRegistryFor(manifest)
if (authenticationService) {
// Never publish lock/polkit through ShellRoot._services. The private JS
// import retains their lifetime without adding a traversable property
// or QObject parent back to the host shell.
AuthServiceStore.put(key, inst)
} else {
var snext = ({})
for (var sk in _services) snext[sk] = _services[sk]
snext[key] = inst
_services = snext
}
}
if (comp.status === Component.Loading) {
comp.statusChanged.connect(finalize)
return null
}
finalize()
return _services[key] || null
return authenticationService ? null : (_services[key] || null)
}
function _syncServices() {
@@ -333,11 +948,33 @@ ShellRoot {
if (!Array.isArray(m.kinds) || m.kinds.indexOf("service") === -1) continue
if (!m.entryPoints || !m.entryPoints.service) continue
if (!pluginRegistry.isEnabled(id)) continue
var authenticationService = shell.isAuthenticationService(m, id)
if (_services[id]) {
// A kept instance outlives the rescan; hand it the fresh manifest.
var kept = _services[id]
if (kept && "manifest" in kept) kept.manifest = m
continue
if (authenticationService) {
// A service that gains a trusted authentication capability must move
// out of the host's public service map before it is recreated.
var published = _services[id]
if (published && typeof published.destroy === "function") published.destroy()
var withoutPublished = ({})
for (var publishedId in _services)
if (publishedId !== id) withoutPublished[publishedId] = _services[publishedId]
_services = withoutPublished
} else {
// A kept instance outlives the rescan; hand it the fresh manifest.
var kept = _services[id]
if (kept && "shell" in kept) kept.shell = shell.pluginShellFor(m)
if (kept && "manifest" in kept) kept.manifest = shell.publicPluginManifest(m)
continue
}
}
if (AuthServiceStore.has(id)) {
if (authenticationService) {
AuthServiceStore.updateManifest(id, shell.publicPluginManifest(m))
continue
}
// A service that loses its trusted authentication capability can move
// back to the ordinary service map only after the isolated copy dies.
AuthServiceStore.destroy(id)
}
ensureService(id)
}
@@ -356,6 +993,21 @@ ShellRoot {
for (var k in _services) if (k !== existingId) next[k] = _services[k]
_services = next
}
// Authentication services are retained outside the root object graph, so
// reconcile their disable/remove lifecycle separately from _services.
var authenticationIds = AuthServiceStore.ids()
for (var ai = 0; ai < authenticationIds.length; ai++) {
var authenticationId = authenticationIds[ai]
var authenticationManifest = plugins[authenticationId]
var stillAuthenticationService = authenticationManifest
&& Array.isArray(authenticationManifest.kinds)
&& authenticationManifest.kinds.indexOf("service") !== -1
&& authenticationManifest.entryPoints
&& authenticationManifest.entryPoints.service
if (stillAuthenticationService && pluginRegistry.isEnabled(authenticationId)
&& shell.isAuthenticationService(authenticationManifest, authenticationId)) continue
AuthServiceStore.destroy(authenticationId)
}
}
function serviceKeepLoaded(pluginId) {
@@ -378,11 +1030,33 @@ ShellRoot {
if (inst && typeof inst.destroy === "function") inst.destroy()
}
_services = next
var authenticationIds = AuthServiceStore.ids()
for (var ai = 0; ai < authenticationIds.length; ai++) {
var authenticationId = authenticationIds[ai]
if (!serviceKeepLoaded(authenticationId))
AuthServiceStore.destroy(authenticationId)
}
}
Connections {
target: shell.pluginRegistry
function onPluginsChanged() { if (!shell.pluginReloading) shell._syncServices() }
function onPluginsChanged() {
shell.syncPluginApis()
if (!shell.pluginReloading) shell._syncServices()
}
}
Connections {
target: shell.barWidgetRegistry
function onChanged() { shell.syncPluginApis() }
}
Connections {
target: shell.appLibrary
function onAppsChanged() {
for (var id in shell._pluginAppLibraryApis)
shell._pluginAppLibraryApis[id].appsChanged()
}
}
// Writes inline settings to a bar layout entry or top-level plugin entry in
@@ -654,10 +1328,10 @@ ShellRoot {
onLoaded: {
if (!item) return
if ("omarchyPath" in item) item.omarchyPath = shell.omarchyPath
if ("shell" in item) item.shell = shell
if ("manifest" in item) item.manifest = panelEntry.manifest
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.barWidgetRegistry
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistry
if ("shell" in item) item.shell = shell.pluginShellFor(panelEntry.manifest)
if ("manifest" in item) item.manifest = shell.publicPluginManifest(panelEntry.manifest)
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(panelEntry.manifest)
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistryFor(panelEntry.manifest)
// Plugins that pair a panel UI with a service entry read shared
// state off `service`. Hand them the matching singleton if one was
// loaded.
@@ -723,7 +1397,8 @@ ShellRoot {
schema: meta.schema || [],
pluginId: manifest.id,
sourceDir: manifest.__sourceDir || "",
source: "plugin"
source: "plugin",
firstParty: !!manifest.__isFirstParty
}
// A load already in flight for this URL registers itself when it