/* * Copyright (C) 2010 Felix Geyer * * 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 or (at your option) * version 3 of the License. * * 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, see . */ #include "EntryModel.h" #include #include #include #include #include #include "core/Config.h" #include "core/DatabaseIcons.h" #include "core/Entry.h" #include "core/Global.h" #include "core/Group.h" #include "core/Metadata.h" #include "core/Resources.h" #ifdef Q_OS_MACOS #include "gui/osutils/macutils/MacUtils.h" #endif EntryModel::EntryModel(QObject* parent) : QAbstractTableModel(parent) , m_group(nullptr) , m_hideUsernames(false) , m_hidePasswords(true) , HiddenContentDisplay(QString("\u25cf").repeated(6)) , DateFormat(Qt::DefaultLocaleShortDate) { } Entry* EntryModel::entryFromIndex(const QModelIndex& index) const { Q_ASSERT(index.isValid() && index.row() < m_entries.size()); return m_entries.at(index.row()); } QModelIndex EntryModel::indexFromEntry(Entry* entry) const { int row = m_entries.indexOf(entry); Q_ASSERT(row != -1); return index(row, 1); } void EntryModel::setGroup(Group* group) { if (!group || group == m_group) { return; } beginResetModel(); severConnections(); m_group = group; m_allGroups.clear(); m_entries = group->entries(); m_orgEntries.clear(); makeConnections(group); endResetModel(); } void EntryModel::setEntries(const QList& entries) { beginResetModel(); severConnections(); m_group = nullptr; m_allGroups.clear(); m_entries = entries; m_orgEntries = entries; QSet databases; for (Entry* entry : asConst(m_entries)) { databases.insert(entry->group()->database()); } for (Database* db : asConst(databases)) { Q_ASSERT(db); const QList groupList = db->rootGroup()->groupsRecursive(true); for (const Group* group : groupList) { m_allGroups.append(group); } if (db->metadata()->recycleBin()) { m_allGroups.removeOne(db->metadata()->recycleBin()); } } for (const Group* group : asConst(m_allGroups)) { makeConnections(group); } endResetModel(); } int EntryModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } else { return m_entries.size(); } } int EntryModel::columnCount(const QModelIndex& parent) const { // Advised by Qt documentation if (parent.isValid()) { return 0; } return 14; } QVariant EntryModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } Entry* entry = entryFromIndex(index); EntryAttributes* attr = entry->attributes(); if (role == Qt::DisplayRole) { QString result; switch (index.column()) { case ParentGroup: if (entry->group()) { return entry->group()->name(); } break; case Title: result = entry->resolveMultiplePlaceholders(entry->title()); if (attr->isReference(EntryAttributes::TitleKey)) { result.prepend(tr("Ref: ", "Reference abbreviation")); } return result; case Username: if (m_hideUsernames) { result = EntryModel::HiddenContentDisplay; } else { result = entry->resolveMultiplePlaceholders(entry->username()); } if (attr->isReference(EntryAttributes::UserNameKey)) { result.prepend(tr("Ref: ", "Reference abbreviation")); } return result; case Password: if (m_hidePasswords) { result = EntryModel::HiddenContentDisplay; } else { result = entry->resolveMultiplePlaceholders(entry->password()); } if (attr->isReference(EntryAttributes::PasswordKey)) { result.prepend(tr("Ref: ", "Reference abbreviation")); } if (entry->password().isEmpty() && !config()->get(Config::Security_PasswordEmptyPlaceholder).toBool()) { result = ""; } return result; case Url: result = entry->resolveMultiplePlaceholders(entry->displayUrl()); if (attr->isReference(EntryAttributes::URLKey)) { result.prepend(tr("Ref: ", "Reference abbreviation")); } return result; case Notes: // Display only first line of notes in simplified format if not hidden if (config()->get(Config::Security_HideNotes).toBool()) { result = EntryModel::HiddenContentDisplay; } else { result = entry->notes().section("\n", 0, 0).simplified(); } if (attr->isReference(EntryAttributes::NotesKey)) { result.prepend(tr("Ref: ", "Reference abbreviation")); } return result; case Expires: // Display either date of expiry or 'Never' result = entry->timeInfo().expires() ? entry->timeInfo().expiryTime().toLocalTime().toString(EntryModel::DateFormat) : tr("Never"); return result; case Created: result = entry->timeInfo().creationTime().toLocalTime().toString(EntryModel::DateFormat); return result; case Modified: result = entry->timeInfo().lastModificationTime().toLocalTime().toString(EntryModel::DateFormat); return result; case Accessed: result = entry->timeInfo().lastAccessTime().toLocalTime().toString(EntryModel::DateFormat); return result; case Attachments: { // Display comma-separated list of attachments QList attachments = entry->attachments()->keys(); for (const auto& attachment : attachments) { if (result.isEmpty()) { result.append(attachment); continue; } result.append(QString(", ") + attachment); } return result; } case Size: { const int unitsSize = 4; QString units[unitsSize] = {"B", "KiB", "MiB", "GiB"}; float resultInt = entry->size(); for (int i = 0; i < unitsSize; i++) { if (resultInt < 1024 || i == unitsSize - 1) { resultInt = qRound(resultInt * 100) / 100.0; result = QStringLiteral("%1 %2").arg(QString::number(resultInt), units[i]); break; } resultInt /= 1024.0; } return result; } } } else if (role == Qt::UserRole) { // Qt::UserRole is used as sort role, see EntryView::EntryView() switch (index.column()) { case Username: return entry->resolveMultiplePlaceholders(entry->username()); case Password: return entry->resolveMultiplePlaceholders(entry->password()); case Expires: // There seems to be no better way of expressing 'infinity' return entry->timeInfo().expires() ? entry->timeInfo().expiryTime() : QDateTime(QDate(9999, 1, 1)); case Created: return entry->timeInfo().creationTime(); case Modified: return entry->timeInfo().lastModificationTime(); case Accessed: return entry->timeInfo().lastAccessTime(); case Paperclip: // Display entries with attachments above those without when // sorting ascendingly (and vice versa when sorting descendingly) return !entry->attachments()->isEmpty(); case Totp: return entry->hasTotp(); case Size: return entry->size(); default: // For all other columns, simply use data provided by Qt::Display- // Role for sorting return data(index, Qt::DisplayRole); } } else if (role == Qt::DecorationRole) { switch (index.column()) { case ParentGroup: if (entry->group()) { return entry->group()->iconScaledPixmap(); } break; case Title: if (entry->isExpired()) { return databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex); } return entry->iconScaledPixmap(); case Paperclip: if (!entry->attachments()->isEmpty()) { return resources()->icon("paperclip"); } break; case Totp: if (entry->hasTotp()) { return resources()->icon("chronometer"); } break; } } else if (role == Qt::FontRole) { QFont font; if (entry->isExpired()) { font.setStrikeOut(true); } return font; } else if (role == Qt::ForegroundRole) { QColor foregroundColor; foregroundColor.setNamedColor(entry->foregroundColor()); if (entry->hasReferences()) { QPalette p; foregroundColor = p.color(QPalette::Current, QPalette::Text); int lightness = qMin(255, qMax(0, foregroundColor.lightness() + (foregroundColor.lightness() < 110 ? 85 : -51))); foregroundColor.setHsl(foregroundColor.hue(), foregroundColor.saturation(), lightness); return QVariant(foregroundColor); } else if (foregroundColor.isValid()) { return QVariant(foregroundColor); } } else if (role == Qt::BackgroundRole) { QColor backgroundColor; backgroundColor.setNamedColor(entry->backgroundColor()); if (backgroundColor.isValid()) { return QVariant(backgroundColor); } } else if (role == Qt::TextAlignmentRole) { if (index.column() == Paperclip) { return Qt::AlignCenter; } } return QVariant(); } QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(orientation); if (role == Qt::DisplayRole) { switch (section) { case ParentGroup: return tr("Group"); case Title: return tr("Title"); case Username: return tr("Username"); case Password: return tr("Password"); case Url: return tr("URL"); case Notes: return tr("Notes"); case Expires: return tr("Expires"); case Created: return tr("Created"); case Modified: return tr("Modified"); case Accessed: return tr("Accessed"); case Attachments: return tr("Attachments"); case Size: return tr("Size"); } } else if (role == Qt::DecorationRole) { switch (section) { case Paperclip: return resources()->icon("paperclip"); case Totp: return resources()->icon("chronometer"); } } else if (role == Qt::ToolTipRole) { switch (section) { case ParentGroup: return tr("Group name"); case Title: return tr("Entry title"); case Username: return tr("Username"); case Password: return tr("Password"); case Url: return tr("URL"); case Notes: return tr("Entry notes"); case Expires: return tr("Entry expires at"); case Created: return tr("Creation date"); case Modified: return tr("Last modification date"); case Accessed: return tr("Last access date"); case Attachments: return tr("Attached files"); case Size: return tr("Entry size"); case Paperclip: return tr("Has attachments"); case Totp: return tr("Has TOTP one-time password"); } } return {}; } Qt::DropActions EntryModel::supportedDropActions() const { return Qt::IgnoreAction; } Qt::DropActions EntryModel::supportedDragActions() const { return (Qt::MoveAction | Qt::CopyAction); } Qt::ItemFlags EntryModel::flags(const QModelIndex& modelIndex) const { if (!modelIndex.isValid()) { return Qt::NoItemFlags; } else { return QAbstractItemModel::flags(modelIndex) | Qt::ItemIsDragEnabled; } } QStringList EntryModel::mimeTypes() const { QStringList types; types << QLatin1String("application/x-keepassx-entry"); return types; } QMimeData* EntryModel::mimeData(const QModelIndexList& indexes) const { if (indexes.isEmpty()) { return nullptr; } QMimeData* data = new QMimeData(); QByteArray encoded; QDataStream stream(&encoded, QIODevice::WriteOnly); QSet seenEntries; for (const QModelIndex& index : indexes) { if (!index.isValid()) { continue; } Entry* entry = entryFromIndex(index); if (!seenEntries.contains(entry)) { // make sure we don't add entries multiple times when we get indexes // with the same row but different columns stream << entry->group()->database()->uuid() << entry->uuid(); seenEntries.insert(entry); } } if (seenEntries.isEmpty()) { delete data; return nullptr; } else { data->setData(mimeTypes().at(0), encoded); return data; } } void EntryModel::entryAboutToAdd(Entry* entry) { if (!m_group && !m_orgEntries.contains(entry)) { return; } beginInsertRows(QModelIndex(), m_entries.size(), m_entries.size()); if (!m_group) { m_entries.append(entry); } } void EntryModel::entryAdded(Entry* entry) { if (!m_group && !m_orgEntries.contains(entry)) { return; } if (m_group) { m_entries = m_group->entries(); } endInsertRows(); } void EntryModel::entryAboutToRemove(Entry* entry) { beginRemoveRows(QModelIndex(), m_entries.indexOf(entry), m_entries.indexOf(entry)); if (!m_group) { m_entries.removeAll(entry); } } void EntryModel::entryRemoved() { if (m_group) { m_entries = m_group->entries(); } endRemoveRows(); } void EntryModel::entryAboutToMoveUp(int row) { beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); if (m_group) { m_entries.move(row, row - 1); } } void EntryModel::entryMovedUp() { if (m_group) { m_entries = m_group->entries(); } endMoveRows(); } void EntryModel::entryAboutToMoveDown(int row) { beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); if (m_group) { m_entries.move(row, row + 1); } } void EntryModel::entryMovedDown() { if (m_group) { m_entries = m_group->entries(); } endMoveRows(); } void EntryModel::entryDataChanged(Entry* entry) { int row = m_entries.indexOf(entry); emit dataChanged(index(row, 0), index(row, columnCount() - 1)); } void EntryModel::severConnections() { if (m_group) { disconnect(m_group, nullptr, this, nullptr); } for (const Group* group : asConst(m_allGroups)) { disconnect(group, nullptr, this, nullptr); } } void EntryModel::makeConnections(const Group* group) { connect(group, SIGNAL(entryAboutToAdd(Entry*)), SLOT(entryAboutToAdd(Entry*))); connect(group, SIGNAL(entryAdded(Entry*)), SLOT(entryAdded(Entry*))); connect(group, SIGNAL(entryAboutToRemove(Entry*)), SLOT(entryAboutToRemove(Entry*))); connect(group, SIGNAL(entryRemoved(Entry*)), SLOT(entryRemoved())); connect(group, SIGNAL(entryAboutToMoveUp(int)), SLOT(entryAboutToMoveUp(int))); connect(group, SIGNAL(entryMovedUp()), SLOT(entryMovedUp())); connect(group, SIGNAL(entryAboutToMoveDown(int)), SLOT(entryAboutToMoveDown(int))); connect(group, SIGNAL(entryMovedDown()), SLOT(entryMovedDown())); connect(group, SIGNAL(entryDataChanged(Entry*)), SLOT(entryDataChanged(Entry*))); } /** * Get current state of 'Hide Usernames' setting */ bool EntryModel::isUsernamesHidden() const { return m_hideUsernames; } /** * Set state of 'Hide Usernames' setting and signal change */ void EntryModel::setUsernamesHidden(bool hide) { m_hideUsernames = hide; emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); emit usernamesHiddenChanged(); } /** * Get current state of 'Hide Passwords' setting */ bool EntryModel::isPasswordsHidden() const { return m_hidePasswords; } /** * Set state of 'Hide Passwords' setting and signal change */ void EntryModel::setPasswordsHidden(bool hide) { m_hidePasswords = hide; emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); emit passwordsHiddenChanged(); }