/* * Copyright (C) 2012 Felix Geyer * Copyright (C) 2017 KeePassXC Team * * 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 "AutoType.h" #include #include #include #include "config-keepassx.h" #include "autotype/AutoTypePlatformPlugin.h" #include "autotype/AutoTypeSelectDialog.h" #include "autotype/WildcardMatcher.h" #include "core/AutoTypeMatch.h" #include "core/Config.h" #include "core/Database.h" #include "core/Entry.h" #include "core/FilePath.h" #include "core/Group.h" #include "core/ListDeleter.h" #include "core/Tools.h" #include "gui/MessageBox.h" #ifdef Q_OS_MAC #include "gui/macutils/MacUtils.h" #endif AutoType* AutoType::m_instance = nullptr; AutoType::AutoType(QObject* parent, bool test) : QObject(parent) , m_autoTypeDelay(0) , m_currentGlobalKey(static_cast(0)) , m_currentGlobalModifiers(nullptr) , m_pluginLoader(new QPluginLoader(this)) , m_plugin(nullptr) , m_executor(nullptr) , m_windowForGlobal(0) { // prevent crash when the plugin has unresolved symbols m_pluginLoader->setLoadHints(QLibrary::ResolveAllSymbolsHint); QString pluginName = "keepassx-autotype-"; if (!test) { pluginName += QApplication::platformName(); } else { pluginName += "test"; } QString pluginPath = filePath()->pluginPath(pluginName); if (!pluginPath.isEmpty()) { #ifdef WITH_XC_AUTOTYPE loadPlugin(pluginPath); #endif } connect(qApp, SIGNAL(aboutToQuit()), SLOT(unloadPlugin())); } AutoType::~AutoType() { if (m_executor) { delete m_executor; m_executor = nullptr; } } void AutoType::loadPlugin(const QString& pluginPath) { m_pluginLoader->setFileName(pluginPath); QObject* pluginInstance = m_pluginLoader->instance(); if (pluginInstance) { m_plugin = qobject_cast(pluginInstance); m_executor = nullptr; if (m_plugin) { if (m_plugin->isAvailable()) { m_executor = m_plugin->createExecutor(); connect(pluginInstance, SIGNAL(globalShortcutTriggered()), SLOT(startGlobalAutoType())); } else { unloadPlugin(); } } } if (!m_plugin) { qWarning("Unable to load auto-type plugin:\n%s", qPrintable(m_pluginLoader->errorString())); } } void AutoType::unloadPlugin() { if (m_executor) { delete m_executor; m_executor = nullptr; } if (m_plugin) { m_plugin->unload(); m_plugin = nullptr; } } AutoType* AutoType::instance() { if (!m_instance) { m_instance = new AutoType(qApp); } return m_instance; } void AutoType::createTestInstance() { Q_ASSERT(!m_instance); m_instance = new AutoType(qApp, true); } QStringList AutoType::windowTitles() { if (!m_plugin) { return QStringList(); } return m_plugin->windowTitles(); } void AutoType::raiseWindow() { #if defined(Q_OS_MACOS) m_plugin->raiseOwnWindow(); #endif } bool AutoType::registerGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) { Q_ASSERT(key); Q_ASSERT(modifiers); if (!m_plugin) { return false; } if (key != m_currentGlobalKey || modifiers != m_currentGlobalModifiers) { if (m_currentGlobalKey && m_currentGlobalModifiers) { m_plugin->unregisterGlobalShortcut(m_currentGlobalKey, m_currentGlobalModifiers); } if (m_plugin->registerGlobalShortcut(key, modifiers)) { m_currentGlobalKey = key; m_currentGlobalModifiers = modifiers; return true; } return false; } return true; } void AutoType::unregisterGlobalShortcut() { if (m_plugin && m_currentGlobalKey && m_currentGlobalModifiers) { m_plugin->unregisterGlobalShortcut(m_currentGlobalKey, m_currentGlobalModifiers); } } int AutoType::callEventFilter(void* event) { if (!m_plugin) { return -1; } return m_plugin->platformEventFilter(event); } /** * Core Autotype function that will execute actions */ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, const QString& sequence, WId window) { if (!m_inAutoType.tryLock()) { return; } // no edit to the sequence beyond this point if (!verifyAutoTypeSyntax(sequence)) { emit autotypeRejected(); m_inAutoType.unlock(); return; } QList actions; ListDeleter actionsDeleter(&actions); if (!parseActions(sequence, entry, actions)) { emit autotypeRejected(); m_inAutoType.unlock(); return; } if (hideWindow) { #if defined(Q_OS_MACOS) // Check for accessibility permission if (!macUtils()->enableAccessibility()) { MessageBox::information(nullptr, tr("Permission Required"), tr("KeePassXC requires the Accessibility permission in order to perform entry " "level Auto-Type. If you already granted permission, you may have to restart " "KeePassXC.")); return; } macUtils()->raiseLastActiveWindow(); m_plugin->hideOwnWindow(); #else hideWindow->showMinimized(); #endif } Tools::wait(qMax(100, config()->get("AutoTypeStartDelay", 500).toInt())); // Used only for selected entry auto-type if (!window) { window = m_plugin->activeWindow(); } QCoreApplication::processEvents(QEventLoop::AllEvents, 10); for (AutoTypeAction* action : asConst(actions)) { if (m_plugin->activeWindow() != window) { qWarning("Active window changed, interrupting auto-type."); emit autotypeRejected(); m_inAutoType.unlock(); return; } action->accept(m_executor); QCoreApplication::processEvents(QEventLoop::AllEvents, 10); } m_windowForGlobal = 0; m_windowTitleForGlobal.clear(); // emit signal only if autotype performed correctly emit autotypePerformed(); m_inAutoType.unlock(); } /** * Single Autotype entry-point function * Perfom autotype sequence in the active window */ void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow) { if (!m_plugin) { return; } QList sequences = autoTypeSequences(entry); if (sequences.isEmpty()) { return; } executeAutoTypeActions(entry, hideWindow, sequences.first()); } void AutoType::startGlobalAutoType() { m_windowForGlobal = m_plugin->activeWindow(); m_windowTitleForGlobal = m_plugin->activeWindowTitle(); emit globalAutoTypeTriggered(); } /** * Global Autotype entry-point function * Perform global Auto-Type on the active window */ void AutoType::performGlobalAutoType(const QList>& dbList) { if (!m_plugin) { return; } if (!m_inGlobalAutoTypeDialog.tryLock()) { return; } if (m_windowTitleForGlobal.isEmpty()) { m_inGlobalAutoTypeDialog.unlock(); return; } QList matchList; for (const auto& db : dbList) { const QList dbEntries = db->rootGroup()->entriesRecursive(); for (Entry* entry : dbEntries) { const QSet sequences = autoTypeSequences(entry, m_windowTitleForGlobal).toSet(); for (const QString& sequence : sequences) { if (!sequence.isEmpty()) { matchList << AutoTypeMatch(entry, sequence); } } } } if (matchList.isEmpty()) { if (qobject_cast(QCoreApplication::instance())) { auto* msgBox = new QMessageBox(); msgBox->setAttribute(Qt::WA_DeleteOnClose); msgBox->setWindowTitle(tr("Auto-Type - KeePassXC")); msgBox->setText(tr("Couldn't find an entry that matches the window title:") .append("\n\n") .append(m_windowTitleForGlobal)); msgBox->setIcon(QMessageBox::Information); msgBox->setStandardButtons(QMessageBox::Ok); msgBox->show(); msgBox->raise(); msgBox->activateWindow(); } m_inGlobalAutoTypeDialog.unlock(); emit autotypeRejected(); } else if ((matchList.size() == 1) && !config()->get("security/autotypeask").toBool()) { executeAutoTypeActions(matchList.first().entry, nullptr, matchList.first().sequence, m_windowForGlobal); m_inGlobalAutoTypeDialog.unlock(); } else { auto* selectDialog = new AutoTypeSelectDialog(); // connect slots, both of which must unlock the m_inGlobalAutoTypeDialog mutex connect(selectDialog, SIGNAL(matchActivated(AutoTypeMatch)), SLOT(performAutoTypeFromGlobal(AutoTypeMatch))); connect(selectDialog, SIGNAL(rejected()), SLOT(autoTypeRejectedFromGlobal())); selectDialog->setMatchList(matchList); #ifdef Q_OS_MACOS m_plugin->raiseOwnWindow(); Tools::wait(200); #endif selectDialog->show(); selectDialog->raise(); // necessary when the main window is minimized selectDialog->activateWindow(); } } void AutoType::performAutoTypeFromGlobal(AutoTypeMatch match) { m_plugin->raiseWindow(m_windowForGlobal); executeAutoTypeActions(match.entry, nullptr, match.sequence, m_windowForGlobal); // make sure the mutex is definitely locked before we unlock it Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock()); m_inGlobalAutoTypeDialog.unlock(); } void AutoType::autoTypeRejectedFromGlobal() { // this slot can be called twice when the selection dialog is deleted, // so make sure the mutex is locked before we try unlocking it Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock()); m_inGlobalAutoTypeDialog.unlock(); m_windowForGlobal = 0; m_windowTitleForGlobal.clear(); emit autotypeRejected(); } /** * Parse an autotype sequence and resolve its Template/command inside as AutoTypeActions */ bool AutoType::parseActions(const QString& actionSequence, const Entry* entry, QList& actions) { QString tmpl; bool inTmpl = false; m_autoTypeDelay = qMax(config()->get("AutoTypeDelay").toInt(), 0); QString sequence = actionSequence; sequence.replace("{{}", "{LEFTBRACE}"); sequence.replace("{}}", "{RIGHTBRACE}"); for (const QChar& ch : sequence) { if (inTmpl) { if (ch == '{') { qWarning("Syntax error in Auto-Type sequence."); return false; } else if (ch == '}') { QList autoType = createActionFromTemplate(tmpl, entry); if (!autoType.isEmpty()) { actions.append(autoType); } inTmpl = false; tmpl.clear(); } else { tmpl += ch; } } else if (ch == '{') { inTmpl = true; } else if (ch == '}') { qWarning("Syntax error in Auto-Type sequence."); return false; } else { actions.append(new AutoTypeChar(ch)); } } if (m_autoTypeDelay > 0) { QList::iterator i; i = actions.begin(); while (i != actions.end()) { ++i; if (i != actions.end()) { i = actions.insert(i, new AutoTypeDelay(m_autoTypeDelay)); ++i; } } } return true; } /** * Convert an autotype Template/command to its AutoTypeAction that will be executed by the plugin executor */ QList AutoType::createActionFromTemplate(const QString& tmpl, const Entry* entry) { QString tmplName = tmpl; int num = -1; QList list; QRegExp delayRegEx("delay=(\\d+)", Qt::CaseInsensitive, QRegExp::RegExp2); if (delayRegEx.exactMatch(tmplName)) { num = delayRegEx.cap(1).toInt(); m_autoTypeDelay = std::max(0, std::min(num, 10000)); return list; } QRegExp repeatRegEx("(.+) (\\d+)", Qt::CaseInsensitive, QRegExp::RegExp2); if (repeatRegEx.exactMatch(tmplName)) { tmplName = repeatRegEx.cap(1); num = repeatRegEx.cap(2).toInt(); if (num == 0) { return list; } } if (tmplName.compare("tab", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Tab)); } else if (tmplName.compare("enter", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Enter)); } else if (tmplName.compare("space", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Space)); } else if (tmplName.compare("up", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Up)); } else if (tmplName.compare("down", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Down)); } else if (tmplName.compare("left", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Left)); } else if (tmplName.compare("right", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Right)); } else if (tmplName.compare("insert", Qt::CaseInsensitive) == 0 || tmplName.compare("ins", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Insert)); } else if (tmplName.compare("delete", Qt::CaseInsensitive) == 0 || tmplName.compare("del", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Delete)); } else if (tmplName.compare("home", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Home)); } else if (tmplName.compare("end", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_End)); } else if (tmplName.compare("pgup", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_PageUp)); } else if (tmplName.compare("pgdown", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_PageDown)); } else if (tmplName.compare("backspace", Qt::CaseInsensitive) == 0 || tmplName.compare("bs", Qt::CaseInsensitive) == 0 || tmplName.compare("bksp", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Backspace)); } else if (tmplName.compare("break", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Pause)); } else if (tmplName.compare("capslock", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_CapsLock)); } else if (tmplName.compare("esc", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Escape)); } else if (tmplName.compare("help", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Help)); } else if (tmplName.compare("numlock", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_NumLock)); } else if (tmplName.compare("ptrsc", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_Print)); } else if (tmplName.compare("scrolllock", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeKey(Qt::Key_ScrollLock)); } // Qt doesn't know about keypad keys so use the normal ones instead else if (tmplName.compare("add", Qt::CaseInsensitive) == 0 || tmplName.compare("+", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('+')); } else if (tmplName.compare("subtract", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('-')); } else if (tmplName.compare("multiply", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('*')); } else if (tmplName.compare("divide", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('/')); } else if (tmplName.compare("^", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('^')); } else if (tmplName.compare("%", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('%')); } else if (tmplName.compare("~", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('~')); } else if (tmplName.compare("(", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('(')); } else if (tmplName.compare(")", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar(')')); } else if (tmplName.compare("leftbrace", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('{')); } else if (tmplName.compare("rightbrace", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeChar('}')); } else { QRegExp fnRegexp("f(\\d+)", Qt::CaseInsensitive, QRegExp::RegExp2); if (fnRegexp.exactMatch(tmplName)) { int fnNo = fnRegexp.cap(1).toInt(); if (fnNo >= 1 && fnNo <= 16) { list.append(new AutoTypeKey(static_cast(Qt::Key_F1 - 1 + fnNo))); } } } if (!list.isEmpty()) { for (int i = 1; i < num; i++) { list.append(list.at(0)->clone()); } return list; } if (tmplName.compare("delay", Qt::CaseInsensitive) == 0 && num > 0) { list.append(new AutoTypeDelay(num)); } else if (tmplName.compare("clearfield", Qt::CaseInsensitive) == 0) { list.append(new AutoTypeClearField()); } else if (tmplName.compare("totp", Qt::CaseInsensitive) == 0) { QString totp = entry->totp(); if (!totp.isEmpty()) { for (const QChar& ch : totp) { list.append(new AutoTypeChar(ch)); } } } if (!list.isEmpty()) { return list; } const QString placeholder = QString("{%1}").arg(tmplName); const QString resolved = entry->resolvePlaceholder(placeholder); if (placeholder != resolved) { for (const QChar& ch : resolved) { if (ch == '\n') { list.append(new AutoTypeKey(Qt::Key_Enter)); } else if (ch == '\t') { list.append(new AutoTypeKey(Qt::Key_Tab)); } else { list.append(new AutoTypeChar(ch)); } } } return list; } /** * Retrive the autotype sequences matches for a given windowTitle * This returns a list with priority ordering. If you don't want duplicates call .toSet() on it. */ QList AutoType::autoTypeSequences(const Entry* entry, const QString& windowTitle) { QList sequenceList; const Group* group = entry->group(); if (!group || !entry->autoTypeEnabled()) { return sequenceList; } do { if (group->autoTypeEnabled() == Group::Disable) { return sequenceList; } else if (group->autoTypeEnabled() == Group::Enable) { break; } group = group->parentGroup(); } while (group); if (!windowTitle.isEmpty()) { const QList assocList = entry->autoTypeAssociations()->getAll(); for (const AutoTypeAssociations::Association& assoc : assocList) { const QString window = entry->resolveMultiplePlaceholders(assoc.window); if (windowMatches(windowTitle, window)) { if (!assoc.sequence.isEmpty()) { sequenceList.append(assoc.sequence); } else { sequenceList.append(entry->effectiveAutoTypeSequence()); } } } if (config()->get("AutoTypeEntryTitleMatch").toBool() && windowMatchesTitle(windowTitle, entry->resolvePlaceholder(entry->title()))) { sequenceList.append(entry->effectiveAutoTypeSequence()); } if (config()->get("AutoTypeEntryURLMatch").toBool() && windowMatchesUrl(windowTitle, entry->resolvePlaceholder(entry->url()))) { sequenceList.append(entry->effectiveAutoTypeSequence()); } if (sequenceList.isEmpty()) { return sequenceList; } } else { sequenceList.append(entry->effectiveAutoTypeSequence()); } return sequenceList; } /** * Checks if a window title matches a pattern */ bool AutoType::windowMatches(const QString& windowTitle, const QString& windowPattern) { if (windowPattern.startsWith("//") && windowPattern.endsWith("//") && windowPattern.size() >= 4) { QRegExp regExp(windowPattern.mid(2, windowPattern.size() - 4), Qt::CaseInsensitive, QRegExp::RegExp2); return (regExp.indexIn(windowTitle) != -1); } return WildcardMatcher(windowTitle).match(windowPattern); } /** * Checks if a window title matches an entry Title * The entry title should be Spr-compiled by the caller */ bool AutoType::windowMatchesTitle(const QString& windowTitle, const QString& resolvedTitle) { return !resolvedTitle.isEmpty() && windowTitle.contains(resolvedTitle, Qt::CaseInsensitive); } /** * Checks if a window title matches an entry URL * The entry URL should be Spr-compiled by the caller */ bool AutoType::windowMatchesUrl(const QString& windowTitle, const QString& resolvedUrl) { if (!resolvedUrl.isEmpty() && windowTitle.contains(resolvedUrl, Qt::CaseInsensitive)) { return true; } QUrl url(resolvedUrl); if (url.isValid() && !url.host().isEmpty()) { return windowTitle.contains(url.host(), Qt::CaseInsensitive); } return false; } /** * Checks if the overall syntax of an autotype sequence is fine */ bool AutoType::checkSyntax(const QString& string) { QString allowRepetition = "(?:\\s\\d+)?"; // the ":" allows custom commands with syntax S:Field // exclude BEEP otherwise will be checked as valid QString normalCommands = "(?!BEEP\\s)[A-Z:]*" + allowRepetition; QString specialLiterals = "[\\^\\%\\(\\)~\\{\\}\\[\\]\\+]" + allowRepetition; QString functionKeys = "(?:F[1-9]" + allowRepetition + "|F1[0-2])" + allowRepetition; QString numpad = "NUMPAD\\d" + allowRepetition; QString delay = "DELAY=\\d+"; QString beep = "BEEP\\s\\d+\\s\\d+"; QString vkey = "VKEY(?:-[EN]X)?\\s\\w+"; QString customAttributes = "S:(?:[^\\{\\}])+"; // these chars aren't in parentheses QString shortcutKeys = "[\\^\\%~\\+@]"; // a normal string not in parentheses QString fixedStrings = "[^\\^\\%~\\+@\\{\\}]*"; // clang-format off QRegularExpression autoTypeSyntax( "^(?:" + shortcutKeys + "|" + fixedStrings + "|\\{(?:" + normalCommands + "|" + specialLiterals + "|" + functionKeys + "|" + numpad + "|" + delay + "|" + beep + "|" + vkey + ")\\}|\\{" + customAttributes + "\\})*$", QRegularExpression::CaseInsensitiveOption); // clang-format on QRegularExpressionMatch match = autoTypeSyntax.match(string); return match.hasMatch(); } /** * Checks an autotype sequence for high delay */ bool AutoType::checkHighDelay(const QString& string) { // 5 digit numbers(10 seconds) are too much QRegularExpression highDelay("\\{DELAY\\s\\d{5,}\\}", QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch match = highDelay.match(string); return match.hasMatch(); } /** * Checks an autotype sequence for slow keypress */ bool AutoType::checkSlowKeypress(const QString& string) { // 3 digit numbers(100 milliseconds) are too much QRegularExpression slowKeypress("\\{DELAY=\\d{3,}\\}", QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch match = slowKeypress.match(string); return match.hasMatch(); } /** * Checks an autotype sequence for high repetition command */ bool AutoType::checkHighRepetition(const QString& string) { // 3 digit numbers are too much QRegularExpression highRepetition("\\{(?!DELAY\\s)\\w+\\s\\d{3,}\\}", QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch match = highRepetition.match(string); return match.hasMatch(); } /** * Verify if the syntax of an autotype sequence is correct and doesn't have silly parameters */ bool AutoType::verifyAutoTypeSyntax(const QString& sequence) { if (!AutoType::checkSyntax(sequence)) { QMessageBox::critical(nullptr, tr("Auto-Type"), tr("The Syntax of your Auto-Type statement is incorrect!")); return false; } else if (AutoType::checkHighDelay(sequence)) { QMessageBox::StandardButton reply; reply = QMessageBox::question( nullptr, tr("Auto-Type"), tr("This Auto-Type command contains a very long delay. Do you really want to proceed?")); if (reply == QMessageBox::No) { return false; } } else if (AutoType::checkSlowKeypress(sequence)) { QMessageBox::StandardButton reply; reply = QMessageBox::question( nullptr, tr("Auto-Type"), tr("This Auto-Type command contains very slow key presses. Do you really want to proceed?")); if (reply == QMessageBox::No) { return false; } } else if (AutoType::checkHighRepetition(sequence)) { QMessageBox::StandardButton reply; reply = QMessageBox::question(nullptr, tr("Auto-Type"), tr("This Auto-Type command contains arguments which are " "repeated very often. Do you really want to proceed?")); if (reply == QMessageBox::No) { return false; } } return true; }