QtPass Developer documentation
QtPass test coverage
Current view: top level - src/passworddialog.h Coverage Total Hit
Test: QtPass Lines: 100.0 % 1 1
Test Date: 2026-11-14 13:19:18 Functions: - 0 0

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2015 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Dialog for creating or editing one password entry, including the
       6              :  * generator, template fields, OTP handling and the asynchronous save.
       7              :  */
       8              : #ifndef SRC_PASSWORDDIALOG_H_
       9              : #define SRC_PASSWORDDIALOG_H_
      10              : 
      11              : #include "appsettings.h"
      12              : 
      13              : #include <QDialog>
      14              : #include <QPointer>
      15              : #include <QScopedPointer>
      16              : 
      17              : namespace Ui {
      18              : class PasswordDialog;
      19              : }
      20              : 
      21              : class Pass;
      22              : class FieldLabel;
      23              : class QAction;
      24              : class QLineEdit;
      25              : class SecretLineEdit;
      26              : class QShortcut;
      27              : class QWidget;
      28              : 
      29              : /**
      30              :  * @class PasswordDialog
      31              :  * @ingroup ui
      32              :  * @brief Dialog for creating a password entry or editing an existing one.
      33              :  *
      34              :  * The first line is the password, with a generator beside it; below it are
      35              :  * one field per template line when templates are in use, and a free text
      36              :  * box for everything else. For an existing entry the dialog asks the
      37              :  * backend to Show() it and fills the fields when the content arrives. On OK
      38              :  * it puts the entry together again and calls Pass::Insert(), then stays
      39              :  * open with its fields locked until the backend reports: it closes on
      40              :  * Pass::finishedInsert() and gives the fields back with the reason on
      41              :  * Pass::insertFailed(), so a failed save never loses what was typed.
      42              :  */
      43              : class PasswordDialog : public QDialog {
      44          242 :   Q_OBJECT
      45              : 
      46              : public:
      47              :   /**
      48              :    * @brief Construct a PasswordDialog for entering a new password.
      49              :    * @param passConfig Password generation configuration.
      50              :    * @param parent Optional parent widget.
      51              :    */
      52              :   explicit PasswordDialog(PasswordConfiguration passConfig,
      53              :                           QWidget *parent = nullptr);
      54              :   /**
      55              :    * @brief Construct a PasswordDialog for editing an existing password file.
      56              :    * @param pass Backend used to show, insert, and generate passwords.
      57              :    * @param s Application settings snapshot (password config, template, pwgen).
      58              :    * @param file Path to the password file being edited.
      59              :    * @param isNew true if creating a new entry, false if editing existing.
      60              :    * @param parent Optional parent widget.
      61              :    */
      62              :   PasswordDialog(Pass *pass, const AppSettings &s, QString file,
      63              :                  const bool &isNew, QWidget *parent = nullptr);
      64              :   /**
      65              :    * @brief Hides the window before QDialog tears it down, so the focus-out
      66              :    * emitted on hiding cannot reach slots of a half-destroyed dialog.
      67              :    */
      68              :   ~PasswordDialog() override;
      69              : 
      70              :   /**
      71              :    * @brief OK: for a new entry, resolve and check the name first and stay
      72              :    * open when it is not usable; then insert the entry and close as accepted
      73              :    * only once the insert has succeeded.
      74              :    *
      75              :    * The insert is asynchronous. Until it reports back the dialog stays open
      76              :    * with its fields locked; when it fails (a missing recipient key, an
      77              :    * unusable `.gpg-id`, a refused path) the fields come back with what was
      78              :    * typed and the reason is shown, instead of the entry being lost with the
      79              :    * closed dialog.
      80              :    */
      81              :   void accept() override;
      82              : 
      83              :   /**
      84              :    * @brief Cancel, Esc and the window's close button. While an insert is in
      85              :    * flight they ask first: closing then means a failure can no longer hand
      86              :    * back what was typed, but a save that never reports must not leave
      87              :    * quitting as the only way out.
      88              :    */
      89              :   void reject() override;
      90              : 
      91              :   /**
      92              :    * @brief Let a new entry be named inside the dialog: a folder picker over
      93              :    * the store's folders and a name field, instead of a separate prompt.
      94              :    *
      95              :    * The dialog validates as the user types (empty, escaping the store,
      96              :    * already taken) and keeps OK off until the name is good; accept() then
      97              :    * creates any subfolder the name needs and inserts the entry at
      98              :    * entryPath(). Only meaningful for a dialog constructed with isNew.
      99              :    * @param storeRoot Absolute path of the password store.
     100              :    * @param folders Folders relative to the store, "" for the root; shown in
     101              :    *        the picker in this order.
     102              :    * @param currentFolder The folder to preselect (relative, may be "").
     103              :    */
     104              :   void setNewEntryLocation(const QString &storeRoot, const QStringList &folders,
     105              :                            const QString &currentFolder);
     106              : 
     107              :   /**
     108              :    * @brief The entry the dialog writes to, relative to the store and
     109              :    * without the `.gpg` suffix (for example `work/vpn`).
     110              :    * @return The path; for an existing entry the one given at construction.
     111              :    */
     112              :   auto entryPath() const -> QString { return m_file; }
     113              : 
     114              :   /**
     115              :    * @brief Populate the dialog's password field with the given text.
     116              :    * @param password Password text to display.
     117              :    * @sa getPassword
     118              :    */
     119              :   void setPassword(const QString &password);
     120              : 
     121              :   /**
     122              :    * @brief Retrieve the current text from the dialog's password field.
     123              :    * @return Current password as a QString.
     124              :    * @sa setPassword
     125              :    */
     126              :   auto getPassword() -> QString;
     127              : 
     128              :   /**
     129              :    * @brief Set the template fields and whether templating is enabled.
     130              :    * @param rawFields Template text containing field definitions.
     131              :    * @param useTemplate true to enable the template, false to disable.
     132              :    */
     133              :   void setTemplate(const QString &rawFields, bool useTemplate);
     134              : 
     135              :   /**
     136              :    * @brief Whether every `key: value` line becomes a field of its own.
     137              :    *
     138              :    * Mirrors the "Template all fields" setting. Off, only the template's
     139              :    * fields get widgets and every other line stays in the free-text body,
     140              :    * where the user can edit key and value alike.
     141              :    * @param templateAll true to split every tokenisable line into a field.
     142              :    */
     143              :   void templateAll(bool templateAll);
     144              : 
     145              :   /**
     146              :    * @brief Set the desired password length shown in the dialog.
     147              :    * @param length Desired password length.
     148              :    */
     149              :   void setLength(int length);
     150              : 
     151              :   /**
     152              :    * @brief Set the password character template index.
     153              :    * @param templateIndex Index identifying the character template to use.
     154              :    */
     155              :   void setPasswordCharTemplate(int templateIndex);
     156              : 
     157              :   /**
     158              :    * @brief Enable or disable pwgen-style password generation mode.
     159              :    * @param usePwgen true to enable pwgen mode, false to disable.
     160              :    */
     161              :   void usePwgen(bool usePwgen);
     162              : 
     163              :   /**
     164              :    * @brief Set available templates from file and select default.
     165              :    * @param templates Hash of template name to field list.
     166              :    * @param defaultTemplate Name of default template.
     167              :    */
     168              :   void setAvailableTemplates(const QHash<QString, QStringList> &templates,
     169              :                              const QString &defaultTemplate);
     170              : 
     171              : signals:
     172              :   /**
     173              :    * @brief Text was copied out of the password, a field or the body, marked
     174              :    * as a secret: for ClipboardManager::trackCopied(), so autoclear clears it.
     175              :    * @param text The text copied.
     176              :    */
     177              :   void copied(const QString &text);
     178              : 
     179              : public slots:
     180              :   /**
     181              :    * @brief Cycle to next template (Ctrl+T).
     182              :    */
     183              :   void cycleTemplate();
     184              : 
     185              :   /**
     186              :    * @brief Populate the dialog from a decrypted entry (Pass::finishedShow).
     187              :    *
     188              :    * Entries with more than FileContent::kMaxDisplayLines lines are not loaded
     189              :    * at all; the status label says so and the editor stays locked.
     190              :    * @param output Decrypted content of the entry.
     191              :    * @param file The entry the output belongs to; output for any other entry
     192              :    *        than the one being edited is ignored. Empty means unspecified.
     193              :    */
     194              :   void setPass(const QString &output, const QString &file = QString());
     195              : 
     196              : private slots:
     197              :   /**
     198              :    * @brief Show or mask the password field.
     199              :    *
     200              :    * Connected explicitly to QCheckBox::toggled in both constructors; the box
     201              :    * is two-state, so the deprecated stateChanged(int) is not needed.
     202              :    * @param show true to display the password in clear text.
     203              :    */
     204              :   void setPasswordVisible(bool show);
     205              :   /**
     206              :    * @brief "Generate" button: asks the backend for a password of the chosen
     207              :    * length from the selected character set (Pass::generatePassword, which
     208              :    * runs pwgen when that mode is on) and puts it in the password field. An
     209              :    * empty result leaves the field alone.
     210              :    */
     211              :   void on_createPasswordButton_clicked();
     212              :   /**
     213              :    * @brief Connected to QDialog::rejected: blanks the password and field
     214              :    * widgets so the decrypted secret does not linger in a dialog that is
     215              :    * being discarded.
     216              :    */
     217              :   void on_rejected();
     218              : 
     219              :   /**
     220              :    * @brief The insert started by accept() succeeded: close as accepted.
     221              :    */
     222              :   void onInsertFinished();
     223              :   /**
     224              :    * @brief The insert started by accept() failed: unlock the fields, which
     225              :    * still hold what was typed, and show why.
     226              :    * @param err The reason reported by the backend.
     227              :    * @param written true when the entry was written and only a later step
     228              :    *        (git) failed; a new entry then closes the dialog.
     229              :    */
     230              :   void onInsertFailed(const QString &err, bool written);
     231              : 
     232              :   /**
     233              :    * @brief Handle a process error while waiting for an entry's decrypt.
     234              :    *
     235              :    * When the asynchronous Show() fails, keep the dialog open and surface the
     236              :    * reason in statusLabel instead of closing silently: Ok stays disabled and
     237              :    * the editor keeps its lock so the user can only Cancel/withdraw.
     238              :    * @param exitCode Exit code of the failed process (unused).
     239              :    * @param err Error output to present to the user.
     240              :    */
     241              :   void onShowError(int exitCode, const QString &err);
     242              : 
     243              : private:
     244              :   /**
     245              :    * @brief Forwards the line edit's SecretLineEdit::copied to this dialog's
     246              :    * copied(), so every copy out of any field reaches ClipboardManager.
     247              :    * @param line A password or field editor just created.
     248              :    */
     249              :   void reportCopies(SecretLineEdit *line);
     250              :   /**
     251              :    * @brief Locate the field that holds the one-time password configuration.
     252              :    * @return The matching QLineEdit, or nullptr when the entry has no OTP field.
     253              :    */
     254              :   [[nodiscard]] auto otpLineEdit() const -> QLineEdit *;
     255              :   /**
     256              :    * @brief Enabled or grey out every editable field of the dialog.
     257              :    *
     258              :    * Used while an existing entry's decrypted content is being fetched, so an
     259              :    * early Ok cannot report success while writing nothing and a late setPass()
     260              :    * cannot clobber edits made on the still-empty fields.
     261              :    * @param enabled true to restore the editor, false to lock it.
     262              :    */
     263              :   void setEditorEnabled(bool enabled);
     264              :   /**
     265              :    * @brief Start inserting what the dialog holds and wait for the result.
     266              :    */
     267              :   void save();
     268              :   /**
     269              :    * @brief Lock or unlock the dialog while an insert is in flight.
     270              :    * @param saving true while waiting for the backend.
     271              :    */
     272              :   void setSaving(bool saving);
     273              :   /**
     274              :    * @brief Whether the insert report now arriving is this dialog's own.
     275              :    *
     276              :    * False when the dialog is not saving. Otherwise it lets the reports of the
     277              :    * inserts that were already pending when save() ran pass first (one per
     278              :    * call), so an earlier dialog's result does not close or fail this one.
     279              :    * @return true when the arriving report belongs to this dialog's insert.
     280              :    */
     281              :   auto isOwnInsertReport() -> bool;
     282              :   /**
     283              :    * @brief Say why the entry was not saved and let the user try again; the
     284              :    * fields keep what was typed.
     285              :    * @param err The reason reported by the backend.
     286              :    */
     287              :   void showNotSaved(const QString &err);
     288              :   /**
     289              :    * @brief Connect validation and normalisation to the OTP field, if present.
     290              :    *
     291              :    * Called whenever the field widgets are rebuilt, since both setTemplate()
     292              :    * and setPassword() recreate them.
     293              :    */
     294              :   void hookOtpField();
     295              :   /**
     296              :    * @brief Flag an OTP value that is neither a URI nor valid base32.
     297              :    */
     298              :   void validateOtpField();
     299              :   /**
     300              :    * @brief Remember, on the line edit that sent textEdited(), that the user
     301              :    * typed in it: only such a field has a bare secret canonicalised, a loaded
     302              :    * value is kept as it was. The mark lives on the widget, not on the dialog,
     303              :    * so typing into a field that used to be the OTP one (renamed away) says
     304              :    * nothing about the field that is the OTP one now. A member function:
     305              :    * Qt::UniqueConnection, which hookOtpField() needs because it runs more
     306              :    * than once for the same field, is refused for a lambda (a debug build
     307              :    * asserts, a release build makes no connection).
     308              :    */
     309              :   void markOtpFieldEdited();
     310              :   /**
     311              :    * @brief Rewrite the OTP field as a canonical otpauth URI.
     312              :    *
     313              :    * A value that cannot be parsed is left exactly as the user typed it, so
     314              :    * nothing is silently destroyed.
     315              :    */
     316              :   void normalizeOtpField();
     317              : 
     318              :   /** @brief Generated form (`passworddialog.ui`); owned. */
     319              :   QScopedPointer<Ui::PasswordDialog> ui;
     320              :   /** @brief Generator settings: character sets and default length. */
     321              :   PasswordConfiguration m_passConfig;
     322              :   /** @brief Backend for Show, Insert and generatePassword; not owned. */
     323              :   Pass *m_pass{nullptr};
     324              :   /** @brief Field names of the current template, in template order. */
     325              :   QStringList m_fields;
     326              :   /** @brief Entry path relative to the store, without `.gpg`; empty for a
     327              :    * new entry until accept() resolves the typed name. */
     328              :   QString m_file;
     329              :   /// Absolute store root while a new entry is being named; empty otherwise.
     330              :   QString m_storeRoot;
     331              :   /// Folder chosen when the dialog opened; the template default follows the
     332              :   /// picker, so this is what setAvailableTemplates() was computed for.
     333              :   QString m_newEntryFolder;
     334              : 
     335              :   /**
     336              :    * @brief The name the user typed, resolved against the picked folder.
     337              :    * @param problem Receives a translated reason when the name is unusable.
     338              :    * @return The relative entry path, or an empty string on a problem.
     339              :    */
     340              :   auto resolveNewEntry(QString *problem) const -> QString;
     341              :   /**
     342              :    * @brief Re-runs resolveNewEntry() as the user types or picks a folder:
     343              :    * shows the problem (if any) in the status label and enables OK only for a
     344              :    * usable name.
     345              :    */
     346              :   void validateNewEntry();
     347              :   /**
     348              :    * @brief Give a `key: value` field a new key (#132). Refused, with the
     349              :    *        reason in the status label, when another field has that name.
     350              :    *        Re-runs hookOtpField(), as the field may have become or stopped
     351              :    *        being the OTP one.
     352              :    * @param line The field's editor; its object name is the key.
     353              :    * @param label The field's label, updated to show the new key.
     354              :    * @param to The new key.
     355              :    */
     356              :   void renameField(QLineEdit *line, FieldLabel *label, const QString &to);
     357              :   /**
     358              :    * @brief Drop a `key: value` field and its row from the form. The widgets
     359              :    * are deleted once control is back in the event loop: the request comes
     360              :    * from the line's own trailing action or its label's context menu, both
     361              :    * still on the stack.
     362              :    * @param line The field's editor, one of m_otherLines.
     363              :    */
     364              :   void removeField(QLineEdit *line);
     365              :   /** @brief Whether template fields get widgets of their own (the
     366              :    * "Use template" setting). */
     367              :   bool m_templating{};
     368              :   /** @brief Whether every `key: value` line becomes a field (the "Template
     369              :    * all fields" setting), as given to FileContent::parse(). */
     370              :   bool m_allFields{};
     371              :   /** @brief True when creating an entry, false when editing one; decides
     372              :    * whether the name row and the decrypt-before-edit lock apply. */
     373              :   bool m_isNew{};
     374              :   /// True once the existing entry's decrypted content has been loaded, so
     375              :   /// save() can refuse to overwrite it with empty fields before the
     376              :   /// asynchronous Show completes.
     377              :   bool m_contentLoaded{};
     378              :   /// True from accept() starting an insert until the backend reports back.
     379              :   bool m_saving{};
     380              :   /// Inserts already pending when save() ran, whose reports must pass before
     381              :   /// one is taken as this dialog's own (counted down in isOwnInsertReport()).
     382              :   int m_insertsAhead{};
     383              :   /// Last pwgen mode passed to usePwgen(), so the policy can be re-applied
     384              :   /// after setEditorEnabled() re-enables the character-set widgets.
     385              :   bool m_usePwgen{};
     386              :   /// Editors for the template's fields, in template order; rebuilt by
     387              :   /// setTemplate(). Their labels are plain QLabels: the names are fixed.
     388              :   QList<QLineEdit *> m_templateLines;
     389              :   /// Fields that came from the entry rather than the template; their names
     390              :   /// are the user's, so their labels are FieldLabels that can be renamed.
     391              :   QList<QLineEdit *> m_otherLines;
     392              :   /// Named templates from `.templates`, name to field list, as given to
     393              :   /// setAvailableTemplates(); empty when the store has none.
     394              :   QHash<QString, QStringList> m_availableTemplates;
     395              :   /// Name of the template currently applied; cycleTemplate() steps from it.
     396              :   QString m_currentTemplateName;
     397              :   /// Ctrl+T, created once templates are available; owned by the dialog.
     398              :   QShortcut *m_templateShortcut{nullptr};
     399              :   /// Warning indicator shown inside the OTP field; owned by that field.
     400              :   ///
     401              :   /// QPointer, not a raw pointer: the QAction is parented to the QLineEdit, and
     402              :   /// setPassword() deletes the m_otherLines widgets before calling
     403              :   /// hookOtpField(), so a raw pointer would already dangle there.
     404              :   QPointer<QAction> m_otpWarning;
     405              :   /// The line edit hookOtpField() last wired up, so its signals can be
     406              :   /// disconnected when another field becomes the OTP one (a rename, a
     407              :   /// removal): a stale editingFinished from the old field must not rewrite
     408              :   /// the new one.
     409              :   QPointer<QLineEdit> m_otpLine;
     410              : 
     411              :   /**
     412              :    * @brief Switch to a named template: records it, selects it in the combo
     413              :    * box and rebuilds the field rows through setTemplate(). Unknown names
     414              :    * are ignored, and nothing changes while a save is in flight.
     415              :    * @param templateName A key of m_availableTemplates.
     416              :    */
     417              :   void applyTemplate(const QString &templateName);
     418              :   /**
     419              :    * @brief Hides the template row until setAvailableTemplates() fills it and
     420              :    * wires the combo box so that a user selection calls applyTemplate().
     421              :    */
     422              :   void setupTemplateBox();
     423              : };
     424              : 
     425              : #endif // SRC_PASSWORDDIALOG_H_
        

Generated by: LCOV version 2.0-1