Restrict third-party shell plugin capabilities
Reported-by: Roger Piñol <rogerpicar@gmail.com>
This commit is contained in:
1 parent
4d017913d0
commit
1702cf0bee
25 files changed
+1403
-49
No files matched your search
@@ -0,0 +1,34 @@
|
||||
// Intentionally not `.pragma library`: QML JavaScript imports get a private
|
||||
// module instance per importing component. shell.qml's instance retains the
|
||||
// authentication services; a third-party plugin importing this file receives
|
||||
// a separate empty store rather than a shared path to credential-bearing QML.
|
||||
|
||||
var services = ({})
|
||||
|
||||
function has(id) {
|
||||
return services[String(id || "")] !== undefined
|
||||
}
|
||||
|
||||
function put(id, service) {
|
||||
var key = String(id || "")
|
||||
if (!key || !service) return
|
||||
if (services[key] && services[key] !== service && typeof services[key].destroy === "function")
|
||||
services[key].destroy()
|
||||
services[key] = service
|
||||
}
|
||||
|
||||
function ids() {
|
||||
return Object.keys(services)
|
||||
}
|
||||
|
||||
function destroy(id) {
|
||||
var key = String(id || "")
|
||||
var service = services[key]
|
||||
if (service && typeof service.destroy === "function") service.destroy()
|
||||
delete services[key]
|
||||
}
|
||||
|
||||
function destroyAll() {
|
||||
var keys = ids()
|
||||
for (var i = 0; i < keys.length; i++) destroy(keys[i])
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
|
||||
// Detached application-library capability for third-party menus. Callbacks
|
||||
// expose the supported app-list operations without retaining AppLibrary or its
|
||||
// ShellRoot parent in the plugin-visible object graph.
|
||||
QtObject {
|
||||
required property string ownerPluginId
|
||||
|
||||
signal appsChanged()
|
||||
|
||||
property var _entryName: null
|
||||
property var _entrySubtext: null
|
||||
property var _sortedEntries: null
|
||||
property var _iconSource: null
|
||||
property var _refreshIcons: null
|
||||
property var _launch: null
|
||||
property var _remove: null
|
||||
|
||||
function entryName(entry) {
|
||||
return _entryName ? _entryName(entry) : ""
|
||||
}
|
||||
|
||||
function entrySubtext(entry) {
|
||||
return _entrySubtext ? _entrySubtext(entry) : ""
|
||||
}
|
||||
|
||||
function sortedEntries(query) {
|
||||
return _sortedEntries ? _sortedEntries(String(query || "")) : []
|
||||
}
|
||||
|
||||
function iconSource(icon) {
|
||||
return _iconSource ? _iconSource(icon) : ""
|
||||
}
|
||||
|
||||
function refreshIcons() {
|
||||
if (_refreshIcons) _refreshIcons()
|
||||
}
|
||||
|
||||
function launch(desktopId, name) {
|
||||
if (_launch) _launch(String(desktopId || ""), String(name || ""))
|
||||
}
|
||||
|
||||
function remove(desktopId, name) {
|
||||
if (_remove) _remove(String(desktopId || ""), String(name || ""))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
|
||||
// Scalar-only view of the active bar for plugins that position independent
|
||||
// windows. The active Bar QObject is never retained here.
|
||||
QtObject {
|
||||
required property string ownerPluginId
|
||||
|
||||
property bool barHidden: false
|
||||
property int barSize: 0
|
||||
property string fontFamily: ""
|
||||
property string position: "top"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
|
||||
// Detached widget-catalogue snapshot for third-party full-bar implementations.
|
||||
// Plugins can render the referenced components, but mutating this local view
|
||||
// cannot replace a registration in the host registry.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
property var widgets: ({})
|
||||
property int revision: 0
|
||||
|
||||
function metadataFor(id) {
|
||||
var entry = widgets[String(id || "")]
|
||||
return entry ? entry.metadata : null
|
||||
}
|
||||
|
||||
function availableIds() {
|
||||
return Object.keys(widgets)
|
||||
}
|
||||
|
||||
function has(id) {
|
||||
return widgets[String(id || "")] !== undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
|
||||
// Narrow proxy for the non-authentication first-party services used by the
|
||||
// built-in bar. It intentionally has no generic property or method forwarding.
|
||||
QtObject {
|
||||
required property string ownerPluginId
|
||||
required property string serviceId
|
||||
|
||||
property bool stayAwake: false
|
||||
property bool enabled: false
|
||||
property bool doNotDisturb: false
|
||||
property var activePlayer: null
|
||||
property var sourcePlayers: []
|
||||
|
||||
property var _setIdleEnabled: null
|
||||
property var _setNightlight: null
|
||||
property var _setDoNotDisturb: null
|
||||
property var _runAction: null
|
||||
property var _playerKey: null
|
||||
property var _selectPlayer: null
|
||||
|
||||
function setIdleEnabled(value) {
|
||||
if (serviceId === "omarchy.idle" && _setIdleEnabled) _setIdleEnabled(!!value)
|
||||
}
|
||||
|
||||
function setNightlight(value) {
|
||||
if (serviceId === "omarchy.nightlight" && _setNightlight) _setNightlight(!!value)
|
||||
}
|
||||
|
||||
function setDoNotDisturb(value) {
|
||||
if (serviceId === "omarchy.notifications" && _setDoNotDisturb) _setDoNotDisturb(!!value)
|
||||
}
|
||||
|
||||
function runAction(action, showFeedback, playerId) {
|
||||
if (serviceId === "omarchy.media" && _runAction)
|
||||
_runAction(String(action || ""), !!showFeedback, String(playerId || ""))
|
||||
}
|
||||
|
||||
function playerKey(player) {
|
||||
return serviceId === "omarchy.media" && _playerKey ? _playerKey(player) : ""
|
||||
}
|
||||
|
||||
function selectPlayer(playerId) {
|
||||
if (serviceId === "omarchy.media" && _selectPlayer) _selectPlayer(String(playerId || ""))
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ QtObject {
|
||||
property var shellConfigProvider: null
|
||||
property var shellConfigMutator: null
|
||||
|
||||
// { pluginId: manifest } — manifests have __sourceDir and __isFirstParty stamped in.
|
||||
// { pluginId: manifest } — manifests have source/trust metadata stamped in.
|
||||
property var installedPlugins: ({})
|
||||
property int registryRevision: 0
|
||||
property bool scanning: false
|
||||
@@ -90,6 +90,32 @@ QtObject {
|
||||
return manifest
|
||||
}
|
||||
|
||||
function trustedCapabilities(manifest) {
|
||||
if (!manifest || !manifest.__isFirstParty) return []
|
||||
var metadata = Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
var declared = metadata && Array.isArray(metadata.capabilities) ? metadata.capabilities : []
|
||||
var out = []
|
||||
for (var i = 0; i < declared.length; i++) {
|
||||
var capability = String(declared[i] || "")
|
||||
if (capability && out.indexOf(capability) === -1) out.push(capability)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function stampHostCapabilities(firstParty, thirdParty) {
|
||||
for (var firstPartyId in firstParty)
|
||||
firstParty[firstPartyId].__hostCapabilities = trustedCapabilities(firstParty[firstPartyId])
|
||||
|
||||
for (var thirdPartyId in thirdParty) {
|
||||
var manifest = thirdParty[thirdPartyId]
|
||||
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
var clonedFrom = metadata ? String(metadata.clonedFrom || "") : ""
|
||||
var source = clonedFrom ? firstParty[clonedFrom] : null
|
||||
manifest.__hostCapabilities = source && Array.isArray(source.__hostCapabilities)
|
||||
? source.__hostCapabilities.slice() : []
|
||||
}
|
||||
}
|
||||
|
||||
function entryPointUrl(manifest, kind) {
|
||||
if (!Util.isPlainObject(manifest)) return ""
|
||||
var ep = manifest.entryPoints ? manifest.entryPoints[kind] : null
|
||||
@@ -594,6 +620,8 @@ QtObject {
|
||||
}
|
||||
flush()
|
||||
|
||||
stampHostCapabilities(firstParty, thirdParty)
|
||||
|
||||
var merged = {}
|
||||
for (var fk in firstParty) merged[fk] = firstParty[fk]
|
||||
// Third-party plugins never shadow first-party ids. The whole
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import QtQuick
|
||||
|
||||
// Read-only, self-scoped registry view for an installed third-party plugin.
|
||||
// The host updates manifest/enabled when it rescans; no host registry object is
|
||||
// retained here, so `parent` and property traversal cannot reach ShellRoot.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
required property string pluginId
|
||||
property var manifest: null
|
||||
property bool enabled: false
|
||||
property var _entryPointUrl: null
|
||||
|
||||
readonly property var installedPlugins: {
|
||||
var out = ({})
|
||||
if (manifest) out[pluginId] = manifest
|
||||
return out
|
||||
}
|
||||
|
||||
function isEnabled(id) {
|
||||
return String(id || "") === pluginId && enabled
|
||||
}
|
||||
|
||||
function resolveEnabledId(id) {
|
||||
return String(id || "") === pluginId ? pluginId : ""
|
||||
}
|
||||
|
||||
function entryPointUrl(candidate, kind) {
|
||||
if (!candidate || String(candidate.id || "") !== pluginId) return ""
|
||||
return _entryPointUrl ? _entryPointUrl(String(kind || "")) : ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
|
||||
// Capability-scoped shell surface for installed third-party plugins.
|
||||
//
|
||||
// The callbacks are closed over one plugin id by shell.qml. A plugin can call
|
||||
// them directly, but it cannot widen their scope: ordinary plugins are limited
|
||||
// to their own id, and full-bar callbacks independently enforce their explicit
|
||||
// non-authentication UI scope. Keeping the host shell out of this object's
|
||||
// properties also prevents ordinary QML object traversal from turning the
|
||||
// facade back into the root ShellRoot.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
required property string pluginId
|
||||
|
||||
property var appLibrary: null
|
||||
property var bar: null
|
||||
property var barConfig: ({})
|
||||
|
||||
property var _serviceLookup: null
|
||||
property var _firstPartyServiceLookup: null
|
||||
property var _pluginShellLookup: null
|
||||
property var _barEntryShellLookup: null
|
||||
property var _summon: null
|
||||
property var _hide: null
|
||||
property var _toggle: null
|
||||
property var _isOpen: null
|
||||
property var _updateSettings: null
|
||||
property var _mutateBarConfig: null
|
||||
|
||||
function serviceFor(id) {
|
||||
return _serviceLookup ? _serviceLookup(String(id || "")) : null
|
||||
}
|
||||
|
||||
// Only full-bar facades receive narrow proxies for the specific
|
||||
// non-authentication services used by the built-in bar widgets.
|
||||
function firstPartyServiceFor(id) {
|
||||
return _firstPartyServiceLookup
|
||||
? _firstPartyServiceLookup(String(id || "")) : null
|
||||
}
|
||||
|
||||
function pluginShellForId(id) {
|
||||
return _pluginShellLookup ? _pluginShellLookup(String(id || "")) : null
|
||||
}
|
||||
|
||||
function pluginShellForBarEntry(ownerId, moduleName) {
|
||||
return _barEntryShellLookup
|
||||
? _barEntryShellLookup(String(ownerId || ""), String(moduleName || "")) : null
|
||||
}
|
||||
|
||||
function summon(id, payloadJson) {
|
||||
return _summon ? _summon(String(id || ""), String(payloadJson || "")) : false
|
||||
}
|
||||
|
||||
function hide(id) {
|
||||
return _hide ? _hide(String(id || "")) : false
|
||||
}
|
||||
|
||||
function toggle(id, payloadJson) {
|
||||
return _toggle ? _toggle(String(id || ""), String(payloadJson || "")) : false
|
||||
}
|
||||
|
||||
function isPluginOpen(id) {
|
||||
return _isOpen ? _isOpen(String(id || "")) : false
|
||||
}
|
||||
|
||||
function updateEntryInline(id, settings) {
|
||||
return _updateSettings ? _updateSettings(String(id || ""), settings) : false
|
||||
}
|
||||
|
||||
function mutateShellConfig(mutator) {
|
||||
return _mutateBarConfig && typeof mutator === "function"
|
||||
? _mutateBarConfig(mutator) : false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user