mudlet/src/dlgConnectionProfiles.h
Vadim Peretokin ed403cabc1
infrastructure: keep the profile-removal file list from going stale (#9739)
#### Brief overview of PR changes/additions
- Hoists the list `slot_deleteProfile()` checks a never-played profile
against out of the function to
`dlgConnectionProfiles::scmConnectionDetailFiles`, beside the file's
other `scm*` constants, and points at it from both `writeProfileData()`
implementations.
- Adds a `ProfileDeletionSafetyTest` case that sets a profile up through
the real dialog (New profile, name it, fill the connection form in,
re-select it) and fails if anything it wrote is missing from the list.
- Pins the deliberate exclusions too: a profile holding a stored
password or a typed-in character name still asks before removal.

#### Motivation for adding to Mudlet
Nothing linked that list to the ~15 places profile data gets written, so
it could silently go stale; because it is an allowlist a stale entry
only ever costs an extra confirmation prompt, but the maintenance trap
was worth closing.

#### Other info (issues closed, discussion etc)
Follows up
https://github.com/Mudlet/Mudlet/pull/9722#discussion_r3740581754 on
#9722 (fix: a profile named "." or ".." deletes every profile when
removed). No behaviour change.

**Test case:** `ctest -R ProfileDeletionSafetyTest` (20 cases). Removing
an entry from the constant makes it fail naming the file; adding `login`
makes the character-name case fail.

Assisted-by: Claude:claude-opus-5
2026-08-08 17:30:26 +00:00

208 lines
9 KiB
C++

#ifndef MUDLET_DLGCONNECTIONPROFILES_H
#define MUDLET_DLGCONNECTIONPROFILES_H
/***************************************************************************
* Copyright (C) 2008-2011 by Heiko Koehn - KoehnHeiko@googlemail.com *
* Copyright (C) 2014 by Ahmed Charles - acharles@outlook.com *
* Copyright (C) 2016, 2020-2022 by Stephen Lyons *
* - slysven@virginmedia.com *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "ui_connection_profiles.h"
#include <optional>
#include <QRegularExpression>
#include <QTimer>
#include <QKeyEvent>
class QDir;
class QTabBar;
namespace pugi {
class xml_document;
}
class dlgConnectionProfiles : public QDialog, public Ui::connection_profiles
{
Q_OBJECT
public:
Q_DISABLE_COPY(dlgConnectionProfiles)
explicit dlgConnectionProfiles(QWidget* parent = nullptr);
~dlgConnectionProfiles();
void fillout_form();
QPair<bool, QString> writeProfileData(const QString& profile, const QString& item, const QString& what);
QString readProfileData(const QString& profile, const QString& item) const;
void accept() override;
QList<QListWidgetItem*> findData(const QListWidget& listWidget, const QVariant& what, const int role = Qt::UserRole) const;
QList<int> findProfilesBeginningWith(const QString&) const;
static const int csmNameRole{Qt::UserRole};
static QChar firstInvalidProfileNameChar(const QString& name);
static bool profileNameUsableAsIs(const QString& name);
static QString profileFolderPath(const QString& profilesPath, const QString& profile);
static const QString scmAllowedProfileNameChars;
static const QRegularExpression scmUnusableProfileNameChars;
// files whose presence alone does not warrant confirming a profile's removal
static const QStringList scmConnectionDetailFiles;
QString btn_connect_enabled_accessDesc;
QString btn_load_enabled_accessDesc;
QString btn_connOrLoad_disabled_accessDesc;
QString item_profile_accessName;
QString item_profile_accessDesc;
signals:
void signal_load_profile(QString profile_name, bool alsoConnect);
public slots:
void slot_updateName(const QString&);
void slot_saveName();
void slot_updateUrl(const QString&);
void slot_updatePort(const QString&);
void slot_updateSslTslPort(int state);
void slot_updateLogin(const QString&);
void slot_updatePassword(const QString&);
// Not used: void slot_updateWebsite(const QString&);
void slot_updateDescription();
void slot_itemClicked(QListWidgetItem*);
void slot_addProfile();
void slot_deleteProfile();
void slot_updateAutoConnect(int state);
void slot_updateAutoReconnect(int state);
void slot_load();
void slot_cancel();
void slot_copyProfile();
void slot_copyOnlySettingsOfProfile();
void indicatePackagesInstallOnConnect(QStringList packages);
protected:
bool eventFilter(QObject*, QEvent*) override;
void loadPasswordFromSettings(const QString& profile_name);
void ensurePasswordLoadedThenConnect(bool alsoConnect);
bool hasPendingKeychainOperation(const QString& profile_name) const;
private:
static bool copyFolder(const QString& sourceFolder, const QString& destFolder);
void dismissTutorialInvitation();
QString getDescription(const QString& profile_name) const;
bool validateConnect();
bool validateProfile();
void loadProfile(bool alsoConnect);
void copyProfileSettingsOnly(const QString& oldname, const QString& newname);
bool extractSettingsFromProfile(pugi::xml_document& newProfile, const QString& copySettingsFrom);
void saveProfileCopy(const QDir& newProfiledir, const pugi::xml_document& newProfileXml) const;
bool copyProfileWidget(QString& profile_name, QString& oldname, QListWidgetItem*& pItem) const;
struct CopiedProfileData
{
QString host;
QString port;
int sslTsl;
QString login;
QString website;
QString description;
};
CopiedProfileData captureProfileData() const;
void saveDefaultProfileCopy(const QString& profileName, const CopiedProfileData& data, const QString& oldPassword);
bool hasCustomIcon(const QString&) const;
void setProfileIcon() const;
void loadCustomProfile(const QString&) const;
void generateCustomProfile(const QString&) const;
void setCustomIcon(const QString&, QListWidgetItem*) const;
void setIconOfListedProfile(const QString& profileName, const QIcon& icon) const;
QString selectedProfileName() const;
template <typename L>
void loadSecuredPassword(const QString& profile, L callback);
void migrateSecuredPassword(const QString& oldProfile, const QString& newProfile);
void writeSecurePassword(const QString& profile, const QString& pass);
void deleteSecurePassword(const QString& profile);
void setupMudProfile(QListWidgetItem*, const QString& mudServer, const QString& serverDescription, const QString& iconFileName);
void reallyDeleteProfile(const QString& profile);
void showRemovalProblem(const QString& message);
void continueProfileSave(QListWidgetItem* pItem, const QString& newProfileName, const QString& newProfileHost, const QString& newProfilePort, const int newProfileSslTsl);
void setItemName(QListWidgetItem*, const QString&) const;
QIcon customIcon(const QString&, const std::optional<QColor>&) const;
void addLetterToProfileSearch(const int);
void clearNotificationArea();
void loadPasswordAsync(const QString& profileName);
void revealConnectionDetails();
bool showingOnlyMyProfiles() const;
static constexpr int scmMyGamesTab = 0;
static constexpr int scmAllGamesTab = 1;
// split into 3 properties so each one can be checked individually
// important for creation of a folder on disk, for example: name has
// to be valid, but other properties don't have to be
bool validName = false;
bool validUrl = false;
bool validPort = false;
QStringList mProfileList;
QPalette mRegularPalette;
QPalette mOKPalette;
QPalette mErrorPalette;
QPalette mReadOnlyPalette;
QAction* mpCopyProfile = nullptr;
// switches the profiles list between the user's own games and the full catalog
QTabBar* mpTabBar = nullptr;
QPushButton* offline_button = nullptr;
QPushButton* connect_button = nullptr;
QString mDiscordApplicationId;
QString mDiscordInviteURL;
QAction* mpAction_revealPassword;
// true for the duration of the 'Copy profile' action
bool mCopyingProfile = false;
// true while a profile is selected or refreshed programmatically, so that
// it is not mistaken for the user picking a game from the list
bool mProgrammaticProfileSelection = false;
// dialog height before it was shrunk to fit the welcome message
int mDialogHeightBeforeShrink = 0;
QString mDateTimeFormat;
QVector<QColor> mCustomIconColors;
QTimer mSearchTextTimer;
QString mSearchText;
QTimer* mPasswordSaveTimer = nullptr;
QPushButton* mpSkipToGamesButton = nullptr;
bool mTutorialDismissed = false;
// Async connection and password handling
QString mPendingPasswordSaveProfile;
QString mPendingProfileLoad; // Profile name waiting for password load
bool mPendingConnect = false; // Whether to connect (true) or just load (false)
bool mKeychainOperationInProgress = false; // Track if keychain op is active
private slots:
void slot_activeTabChanged(const int index);
void slot_skipToGamesList();
void slot_profileContextMenu(QPoint pos);
void slot_setCustomIcon();
void slot_setCustomColor();
void slot_resetCustomIcon();
void slot_togglePasswordVisibility(const bool);
void slot_reenableAllProfileItems();
void slot_loadPasswordAsync();
void slot_passwordTextChanged();
};
#endif // MUDLET_DLGCONNECTIONPROFILES_H