Skip to content

feat: add applet visibility control to DAppletItemModel and - #1676

Merged
wjyrich merged 1 commit into
linuxdeepin:masterfrom
wjyrich:task-393299-1
Aug 4, 2026
Merged

feat: add applet visibility control to DAppletItemModel and#1676
wjyrich merged 1 commit into
linuxdeepin:masterfrom
wjyrich:task-393299-1

Conversation

@wjyrich

@wjyrich wjyrich commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

DPluginLoader

Implement a framework-level hidden applets mechanism that allows suppressing individual applet root objects from the containment model without destroying them. The DAppletItemModel now maintains both a complete root objects list and a filtered visible subset. A new setRootObjectVisible method enables toggling visibility while preserving the original insertion order upon restoration. The DPluginLoader exposes hiddenApplets, isAppletHidden, and setHiddenApplets APIs, and the shell synchronizes these with a DConfig key hiddenApplets. Hidden applets remain loaded but their root objects are reparented to the applet and removed from the visual model. Unit tests verify order preservation and removal semantics.

Log: Added applet visibility control API and DConfig integration

Influence:

  1. Test applet hiding and showing via DConfig changes
  2. Verify that hidden applets remain loaded and their root objects are properly reparented
  3. Test that removing a plugin ID from the hidden list restores the applet in its original model order
  4. Verify that the model signals emit correctly on visibility changes
  5. Test interaction with applet creation, deletion, and root object changes
  6. Verify that the API works for multiple containments simultaneously

feat: 为 DAppletItemModel 和 DPluginLoader 添加 Applet 显隐控制

实现框架级隐藏 Applet 机制,可在不销毁实例的前提下将其根对象从容
器模型中移除。DAppletItemModel 维护完整的根对象列表和可见子集,
新增 setRootObjectVisible 方法支持按需切换显隐,恢复时保持原有
插入顺序。DPluginLoader 提供 hiddenAppletsisAppletHiddensetHiddenApplets 接口,Shell 通过 DConfig 键 hiddenApplets 同步配 置。隐藏的 Applet 仍保持加载,其根对象重新挂载到 Applet 自身并从视觉模型
中移除。单元测试验证了顺序保持和移除语义。

Log: 新增 Applet 显隐控制接口和 DConfig 集成

Influence:

  1. 通过 DConfig 变更测试 Applet 的隐藏和显示
  2. 验证隐藏的 Applet 依然保持加载,根对象正确重新挂载
  3. 测试从隐藏列表中移除插件 ID 后 Applet 按原有顺序恢复
  4. 验证模型信号在可见性变更时正确发出
  5. 测试与 Applet 创建、删除及根对象变更的交互
  6. 验证该 API 能同时作用于多个容器

PMS: TASK-393299

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @wjyrich, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

Comment thread shell/shell.cpp Outdated
}

if (m_dconfig && m_dconfig->isValid()) {
connect(m_dconfig, &Dtk::Core::DConfig::valueChanged,

@18202781743 18202781743 Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

配置用subpath的方式去获取,而不是把所有插件都放在一个hiddenApplets里,每个插件都有一个单独的配置,方便后面override, https://github.com/18202781743/cskills/blob/4745ee77b82d808182f2a8681db135b0933fb2ba/dtk-development/references/config/concepts.md?plain=1#L74

Comment thread frame/appletitemmodel.cpp
{
}

QList<QObject *> DAppletItemModel::rootObjects() const

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里rootObjects要改成visibleRootObjects么,要不要加个新的visibleRootObjects属性,

Comment thread frame/appletitemmodel.h Outdated

QList<QObject *> rootObjects() const;
void append(QObject *rootObject);
void append(QObject *rootObject, bool visible = true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是公共接口,不能直接加,另外也不需要,在添加的地方去拦截比较合适,

@wjyrich
wjyrich force-pushed the task-393299-1 branch 2 times, most recently from f159ac7 to c07f118 Compare July 28, 2026 06:39
Comment thread docs/plugin/API_en.md Outdated
Q_PROPERTY(QString id READ id CONSTANT FINAL)
Q_PROPERTY(QString pluginId READ pluginId CONSTANT FINAL)
Q_PROPERTY(QObject *rootObject READ rootObject NOTIFY rootObjectChanged)
Q_PROPERTY(bool enable READ enable WRITE setEnable NOTIFY enableChanged FINAL)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是不是不需要呀,disable了这个applet都不会有了,

@wjyrich
wjyrich force-pushed the task-393299-1 branch 8 times, most recently from ba4eb47 to dccc169 Compare July 29, 2026 03:31
Comment thread frame/pluginloader.cpp
Q_EMIT appletEnabledChanged(pluginId, enabled);
}

DApplet *DPluginLoader::loadApplet(const DAppletData &data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果m_disabledPlugins只是在这里使用的话,可以不用加在pluginloader.cpp里,放在dde-shell的appletloader.cpp里,

Comment thread frame/pluginloader.h
QStringList pluginDirs() const;
void setPluginDirs(const QStringList &dirs);

QStringList disabledApplets() const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不能删接口的,这个也不用删除吧,

@wjyrich
wjyrich force-pushed the task-393299-1 branch 2 times, most recently from 7e03ceb to e46752f Compare July 30, 2026 01:59
@wjyrich

wjyrich commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/test github-pr-review-ci

@wjyrich
wjyrich force-pushed the task-393299-1 branch 5 times, most recently from fc66cf6 to c19da7e Compare July 30, 2026 04:58
Comment thread shell/appletconfigmanager.cpp Outdated
pluginIds.insert(plugin.pluginId());
}

const auto configuredPluginIds = m_appletConfigs.keys();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个刚开始不是空的么,不需要遍历吧,

Comment thread shell/main.cpp Outdated
void quit()
{
for (auto item : std::as_const(m_loaders)) {
const auto loaders = m_loaders;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复制一份,有特殊含义么?另外,这里的m_loaders,应该针对的都是没有parent的applet,

Comment thread shell/appletloader.cpp Outdated
continue;

loadTranslation(child->pluginMetaData());
if (!load(child) || !init(child)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

之前不是有这个流程么,再写一个,之前的不能复用么,

@wjyrich
wjyrich force-pushed the task-393299-1 branch 2 times, most recently from 97eeb3a to ed5c294 Compare August 3, 2026 05:34
@wjyrich
wjyrich force-pushed the task-393299-1 branch 2 times, most recently from d81cf1e to e22ee1d Compare August 3, 2026 05:37
Comment thread shell/appletloader.cpp Outdated
return;
}

d->createRootObject(applet);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

先createRootObject再init的吧,

Comment thread shell/appletloader.cpp Outdated

if (!d->init(d->m_applet))
if (!d->load(applet)) {
if (isTargetedLoad)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不用判断是否是isTargetedLoad吧,当加载失败,删除掉翻译是所有的都需要的吧,

Comment thread shell/appletconfigmanager.cpp Outdated
for (const auto &pluginId : pluginIds) {
if (m_appletConfigs.contains(pluginId))
continue;
if (!isAppletEnabled(pluginId))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个一直为true吧,在初始化时,m_disabledPlugins对于pluginId是空的,

Comment thread shell/main.cpp

DAppletLoader *loaderForPlugin(const QString &pluginId) const
{
QString rootPluginId = pluginId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里之所以有这个查找,是用了个ConfigManager,将所有的都汇总到了一起,导致配置和DAppletLoader分开了,要是每个AppLoader都有个ConfigManager,就没这个问题了,每个ConfigManager只管AppletLoader自己的applet的配置,

@wjyrich

wjyrich commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

/test github-pr-review-ci

@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

★ 总体评分:60分

■ 【总体评价】

代码实现了基于DConfig的Applet动态启停功能,但存在路径遍历安全漏洞和配置状态恢复失败的逻辑缺陷
逻辑正确性存在瑕疵且包含安全漏洞,根据安全优先原则强制限制在60分

■ 【详细分析】

  • 1.语法逻辑(存在错误)✕

appletconfigmanager.cppensureAppletConfigs函数中,当DConfig::create返回空指针时,代码依然执行了m_appletConfigs.insert(pluginId, config),将空指针存入Map。这导致后续调用时m_appletConfigs.contains(pluginId)返回真,使得因暂时性故障创建失败的配置永远无法重试恢复。在appletloader.cppexec函数中,若findApplet找到了已存在的applet实例,代码仍会继续向下执行loadcreateRootObject等操作,可能导致组件被重复初始化引发状态错乱或崩溃。
潜在问题:DConfig创建失败后状态被永久错误标记;已加载的Applet可能被重复初始化导致内存泄漏或UI异常
建议:将m_appletConfigs.insert移动到!config || !config->isValid()判断之后,确保只缓存有效对象;在exec函数中,如果findApplet返回非空,应直接返回或先执行卸载逻辑

  • 2.代码质量(一般)✕

appletconfigmanager.hm_disabledPlugins使用QStringList存储,其containsremoveAll操作时间复杂度为O(N),应替换为QSet<QString>ensureAppletConfigsconfig->value(QLatin1String(EnableKey), true).toBool()重复出现两次,应提取为局部变量。appletloader.cpp构造函数中使用Q_ASSERT(configManager),该宏在Release模式下会被预处理器移除,若传入空指针会导致后续空指针解引用崩溃。
潜在问题:低效的线性查找数据结构;Release模式下断言失效引发的潜在崩溃
建议:将m_disabledPlugins类型改为QSet<QString>并调整相关API调用;提取重复的取值逻辑;使用if (!configManager) qFatal("...")替代Q_ASSERT以保证Release模式下的安全性

  • 3.代码性能(无性能问题)✓

代码逻辑清晰,ensureAppletConfigs通过contains判断跳过已处理项,loaderForPlugin通过QSet记录访问路径防止死循环,findApplet递归深度受限于插件树层级,整体无性能瓶颈
建议:无

  • 4.代码安全(存在 2 个安全漏洞(中危1个,低危1个))✕

漏洞对比统计:新增漏洞 2 个,减少漏洞 0 个,持平 0 个
总体风险描述,由于对插件ID未做格式校验直接拼接为DConfig的subpath,存在路径遍历越权风险;同时配置项权限控制过于宽松可能导致本地拒绝服务

  • 安全漏洞1(中危):路径遍历 在 appletconfigmanager.cppensureAppletConfigs 函数中,使用 QLatin1Char('/') + pluginId 拼接 DConfig 的 subpath。如果恶意插件在 metadata 中提供包含 ../pluginId(如 ../../org.deepin.dde.dock),可能绕过预期目录限制,读取或篡改系统中其他 DConfig 配置项,导致配置越权修改或敏感信息泄露 ——非常重要

  • 安全漏洞2(低危):本地拒绝服务 在 org.deepin.dde.shell.json 配置声明中,enable 键的 permissions 设置为 readwrite 且未配置额外的应用权限管控。如果本地普通用户可通过 dde-dconfig 命令直接修改该值,可恶意禁用如电源管理、网络控制等关键子 Applet,导致系统部分核心功能不可用的拒绝服务攻击 ——非常重要

  • 建议:在ensureAppletConfigs中对pluginId进行严格的正则白名单校验(如仅允许[a-zA-Z0-9._-]);在DConfig的JSON声明中细化权限管控,或在应用层setAppletEnabled接口中增加调用者权限校验逻辑

■ 【改进建议代码示例】

// appletconfigmanager.cpp
#include <QRegularExpression>

void AppletConfigManager::ensureAppletConfigs()
{
    auto loader = DPluginLoader::instance();
    const auto plugins = loader->plugins();
    QSet<QString> pluginIds;
    for (const auto &plugin : plugins) {
        pluginIds.insert(plugin.pluginId());
    }

    // 防御路径遍历的白名单正则
    static const QRegularExpression validPluginId("^[a-zA-Z0-9._-]+$");
    
    for (const auto &pluginId : pluginIds) {
        if (m_appletConfigs.contains(pluginId))
            continue;

        // 安全检查:拦截非法的 pluginId 格式
        if (!validPluginId.match(pluginId).hasMatch()) {
            qCWarning(dsLoaderLog) << "Invalid pluginId format, rejecting DConfig creation:" << pluginId;
            continue;
        }

        auto config = DConfig::create(QLatin1String(AppId),
                                      QLatin1String(ConfigName),
                                      QLatin1Char('/') + pluginId,
                                      this);
        
        // 逻辑修复:仅在 config 有效时插入 map,避免 nullptr 阻断后续重试
        if (!config || !config->isValid()) {
            qCWarning(dsLoaderLog) << "Unable to create applet DConfig; applet remains enabled:" << pluginId;
            setAppletEnabled(pluginId, true);
            continue;
        }

        m_appletConfigs.insert(pluginId, config);
        const bool isEnabled = config->value(QLatin1String(EnableKey), true).toBool();
        setAppletEnabled(pluginId, isEnabled);
        
        QObject::connect(config, &DConfig::valueChanged, this, [this, pluginId, config](const QString &key) {
            if (key == QLatin1String(EnableKey)) {
                const bool enabled = config->value(QLatin1String(EnableKey), true).toBool();
                setAppletEnabled(pluginId, enabled);
            }
        });
    }
}
// appletloader.cpp
void DAppletLoader::exec()
{
    D_D(DAppletLoader);
    auto applet = d->m_applet.data();

    if (!d->m_pluginId.isEmpty()) {
        applet = d->findApplet(applet, d->m_pluginId);
        // 逻辑修复:如果 applet 已经存在,说明已经加载,直接返回避免重复初始化
        if (applet) {
            return;
        }
        
        auto containment = qobject_cast<DContainment *>(d->parentApplet(d->m_pluginId));
        if (!containment) {
            d->fail(d->m_pluginId, QStringLiteral("Parent containment was not found."));
            return;
        }

        applet = containment->createApplet(DAppletData(d->m_pluginId));
        if (!applet) {
            d->fail(d->m_pluginId, QStringLiteral("Failed to create applet in its parent containment."));
            return;
        }
    }

    d->loadTranslation(applet->pluginMetaData());

    if (!d->load(applet)) {
        d->removeTranslations(d->m_pluginId);
        return;
    }
    
    d->createRootObject(applet);

    if (!d->init(applet)) {
        d->removeTranslations(d->m_pluginId);
        return;
    }
}
// appletloader.cpp 构造函数防御性修复
DAppletLoader::DAppletLoader(DApplet *applet, AppletConfigManager *configManager, QObject *parent)
    : QObject(parent)
    , DObject(*new DAppletLoaderPrivate(this))
{
    D_D(DAppletLoader);
    // 质量修复:替换 Q_ASSERT,防止 Release 模式下空指针解引用
    if (!configManager) {
        qFatal("AppletConfigManager must not be null in DAppletLoader");
        return;
    }
    d->m_applet = applet;
    d->m_configManager = configManager;
}

Comment thread shell/appletloader.cpp Outdated

d->createRootObject(d->m_applet);
if (!d->load(applet)) {
d->removeTranslations(d->m_pluginId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个不一定是m_pluginId,它是不是应该从applet里获取,另外,有可能有多个applet,当某个applet加载失败,不应该删除翻译吧,这里不删除也行吧,

@wjyrich
wjyrich force-pushed the task-393299-1 branch 3 times, most recently from 170eb05 to 3545a79 Compare August 3, 2026 08:36
@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: 18202781743, wjyrich

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Add the ability to enable or disable DApplet instances via DConfig,
controlling whether their root objects appear in the parent containment
model.

The new `enable` property is exposed as a Q_PROPERTY on DApplet, backed
by a DConfig key `enable` under `org.deepin.dde.shell`. When loading,
DApplet reads this config and watches for changes.

Disabled applets remain loaded but their root objects are removed from
the containment's `appletItems` model; re-enabling re-inserts them.
This enables runtime toggling of applet visibility without unloading/
reloading plugins.

Unit tests are added for the DAppletItemModel (append/remove) and the
enable property change notification logic.

Log: Added DApplet enable/disable feature via DConfig

Influence:
1. Test creating an applet and verifying its enable property is true
by default
2. Set enable to false via dde-dconfig and verify the applet root object
is removed from the containment model
3. Set enable to true and verify the root object reappears
4. Verify that setEnable with the same value does not emit enableChanged
signal
5. Test loading a root plugin (like dock) - its enable should not be
affected (skipped)
6. Verify DConfig changes are watched and applied dynamically at runtime
7. Run unit tests: EnablePropertyNotifiesOnlyOnChange,
AppendsRootObjects, RemovesRootObject

feat: 添加支持通过 DConfig 启用/禁用 Applet 的接口

新增通过 DConfig 控制 DApplet 实例启用的能力,决定其根对象是否显示在父容
器模型中。

新的 `enable` 属性作为 DApplet 的 Q_PROPERTY 暴露,由
`org.deepin.dde.shell` 下的 DConfig 键 `enable` 驱动。加载时 DApplet 读
取该配置并监听变化。

被禁用的 Applet 实例仍保持加载,但根对象从容器的 `appletItems` 模型中
移除;重新启用后会重新追加。这实现了无需卸载/重新加载插件即可运行时切换
Applet 可见性。

新增了 DAppletItemModel(追加/移除)以及 enable 属性变更通知逻辑的单元
测试。

Log: 新增 DApplet 启用/禁用功能

Influence:
1. 测试创建 Applet 并验证其 enable 属性默认为 true
2. 通过 dde-dconfig 将 enable 设为 false,验证根对象已从容器模型中移除
3. 设为 true 后验证根对象重新出现
4. 验证 setEnable 相同值时不会触发 enableChanged 信号
5. 测试加载根插件(如 dock),其 enable 不应受影响(跳过)
6. 验证 DConfig 变化能被监听并在运行时动态生效
7. 运行单元测试:EnablePropertyNotifiesOnlyOnChange、
AppendsRootObjects、RemovesRootObject

PMS: TASK-393299
@wjyrich
wjyrich merged commit bb9bb88 into linuxdeepin:master Aug 4, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants