From 07dd8c560bcb73d0e1a76bf330771624cca82f47 Mon Sep 17 00:00:00 2001 From: kornes <28986062+kornes@users.noreply.github.com> Date: Sun, 8 May 2022 10:27:25 +0000 Subject: [PATCH] Prevent user from entering incorrect characters in Live Notifications channels list (#3715) Co-authored-by: Sidd Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + chatterino.pro | 2 + src/CMakeLists.txt | 2 + src/util/Twitch.cpp | 22 ++++ src/util/Twitch.hpp | 13 +++ src/widgets/helper/EditableModelView.cpp | 5 + src/widgets/helper/EditableModelView.hpp | 1 + src/widgets/helper/RegExpItemDelegate.cpp | 24 ++++ src/widgets/helper/RegExpItemDelegate.hpp | 22 ++++ .../settingspages/NotificationPage.cpp | 2 + tests/src/UtilTwitch.cpp | 105 ++++++++++++++++++ 11 files changed, 199 insertions(+) create mode 100644 src/widgets/helper/RegExpItemDelegate.cpp create mode 100644 src/widgets/helper/RegExpItemDelegate.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index ac1221302..d93796ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Minor: Sorted usernames in /vips message to be case-insensitive. (#3696) - Minor: Added option to open a user's chat in a new tab from the usercard profile picture context menu. (#3625) - Minor: Fixed tag parsing for consecutive escaped characters. (#3711) +- Minor: Prevent user from entering incorrect characters in Live Notifications channels list. (#3715) - Minor: Fixed automod caught message notice appearing twice for mods. (#3717) - Bugfix: Fixed live notifications for usernames containing uppercase characters. (#3646) - Bugfix: Fixed live notifications not getting updated for closed streams going offline. (#3678) diff --git a/chatterino.pro b/chatterino.pro index 5cccdc935..d0ee09f7e 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -305,6 +305,7 @@ SOURCES += \ src/widgets/helper/NotebookButton.cpp \ src/widgets/helper/NotebookTab.cpp \ src/widgets/helper/QColorPicker.cpp \ + src/widgets/helper/RegExpItemDelegate.cpp \ src/widgets/helper/ResizingTextEdit.cpp \ src/widgets/helper/ScrollbarHighlight.cpp \ src/widgets/helper/SearchPopup.cpp \ @@ -588,6 +589,7 @@ HEADERS += \ src/widgets/helper/NotebookButton.hpp \ src/widgets/helper/NotebookTab.hpp \ src/widgets/helper/QColorPicker.hpp \ + src/widgets/helper/RegExpItemDelegate.hpp \ src/widgets/helper/ResizingTextEdit.hpp \ src/widgets/helper/ScrollbarHighlight.hpp \ src/widgets/helper/SearchPopup.hpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aeb89de4e..d37369cd9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -420,6 +420,8 @@ set(SOURCE_FILES widgets/helper/NotebookTab.hpp widgets/helper/QColorPicker.cpp widgets/helper/QColorPicker.hpp + widgets/helper/RegExpItemDelegate.cpp + widgets/helper/RegExpItemDelegate.hpp widgets/helper/ResizingTextEdit.cpp widgets/helper/ResizingTextEdit.hpp widgets/helper/ScrollbarHighlight.cpp diff --git a/src/util/Twitch.cpp b/src/util/Twitch.cpp index 10f6d711a..3bf821929 100644 --- a/src/util/Twitch.cpp +++ b/src/util/Twitch.cpp @@ -5,6 +5,12 @@ namespace chatterino { +namespace { + + const auto TWITCH_USER_LOGIN_PATTERN = R"(^[a-z0-9]\w{0,24}$)"; + +} // namespace + void openTwitchUsercard(QString channel, QString username) { QDesktopServices::openUrl("https://www.twitch.tv/popout/" + channel + @@ -35,4 +41,20 @@ void stripChannelName(QString &channelName) } } +QRegularExpression twitchUserNameRegexp() +{ + static QRegularExpression re( + TWITCH_USER_LOGIN_PATTERN, + QRegularExpression::PatternOption::CaseInsensitiveOption); + + return re; +} + +QRegularExpression twitchUserLoginRegexp() +{ + static QRegularExpression re(TWITCH_USER_LOGIN_PATTERN); + + return re; +} + } // namespace chatterino diff --git a/src/util/Twitch.hpp b/src/util/Twitch.hpp index 16f75d62a..08cb8eb57 100644 --- a/src/util/Twitch.hpp +++ b/src/util/Twitch.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace chatterino { @@ -12,4 +13,16 @@ void stripUserName(QString &userName); // stripChannelName removes any @ prefix or , suffix to make it more suitable for command use void stripChannelName(QString &channelName); +// Matches a strict Twitch user login. +// May contain lowercase a-z, 0-9, and underscores +// Must contain between 1 and 25 characters +// Must not start with an underscore +QRegularExpression twitchUserLoginRegexp(); + +// Matches a loose Twitch user login name. +// May contain lowercase and uppercase a-z, 0-9, and underscores +// Must contain between 1 and 25 characters +// Must not start with an underscore +QRegularExpression twitchUserNameRegexp(); + } // namespace chatterino diff --git a/src/widgets/helper/EditableModelView.cpp b/src/widgets/helper/EditableModelView.cpp index e8add8e79..a002d8209 100644 --- a/src/widgets/helper/EditableModelView.cpp +++ b/src/widgets/helper/EditableModelView.cpp @@ -1,4 +1,5 @@ #include "EditableModelView.hpp" +#include "widgets/helper/RegExpItemDelegate.hpp" #include #include @@ -90,6 +91,10 @@ EditableModelView::EditableModelView(QAbstractTableModel *model, bool movable) // finish button layout buttons->addStretch(1); } +void EditableModelView::setValidationRegexp(QRegularExpression regexp) +{ + this->tableView_->setItemDelegate(new RegExpItemDelegate(this, regexp)); +} void EditableModelView::setTitles(std::initializer_list titles) { diff --git a/src/widgets/helper/EditableModelView.hpp b/src/widgets/helper/EditableModelView.hpp index 87ea09397..bfaf049f8 100644 --- a/src/widgets/helper/EditableModelView.hpp +++ b/src/widgets/helper/EditableModelView.hpp @@ -16,6 +16,7 @@ public: EditableModelView(QAbstractTableModel *model, bool movable = true); void setTitles(std::initializer_list titles); + void setValidationRegexp(QRegularExpression regexp); QTableView *getTableView(); QAbstractTableModel *getModel(); diff --git a/src/widgets/helper/RegExpItemDelegate.cpp b/src/widgets/helper/RegExpItemDelegate.cpp new file mode 100644 index 000000000..d51eb52a2 --- /dev/null +++ b/src/widgets/helper/RegExpItemDelegate.cpp @@ -0,0 +1,24 @@ +#include "widgets/helper/RegExpItemDelegate.hpp" + +#include + +namespace chatterino { + +RegExpItemDelegate::RegExpItemDelegate(QObject *parent, + QRegularExpression regexp) + : QStyledItemDelegate(parent) + , regexp_(regexp) +{ +} + +QWidget *RegExpItemDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + auto *editor = new QLineEdit(parent); + editor->setValidator( + new QRegularExpressionValidator(this->regexp_, editor)); + return editor; +} + +} // namespace chatterino diff --git a/src/widgets/helper/RegExpItemDelegate.hpp b/src/widgets/helper/RegExpItemDelegate.hpp new file mode 100644 index 000000000..0ac46aae7 --- /dev/null +++ b/src/widgets/helper/RegExpItemDelegate.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace chatterino { + +class RegExpItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + RegExpItemDelegate(QObject *parent, QRegularExpression regexp); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + const QRegularExpression regexp_; +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/NotificationPage.cpp b/src/widgets/settingspages/NotificationPage.cpp index 69c23c68e..8f88cf15c 100644 --- a/src/widgets/settingspages/NotificationPage.cpp +++ b/src/widgets/settingspages/NotificationPage.cpp @@ -6,6 +6,7 @@ #include "singletons/Settings.hpp" #include "singletons/Toasts.hpp" #include "util/LayoutCreator.hpp" +#include "util/Twitch.hpp" #include "widgets/helper/EditableModelView.hpp" #include @@ -96,6 +97,7 @@ NotificationPage::NotificationPage() nullptr, Platform::Twitch)) .getElement(); view->setTitles({"Twitch channels"}); + view->setValidationRegexp(twitchUserNameRegexp()); view->getTableView()->horizontalHeader()->setSectionResizeMode( QHeaderView::Fixed); diff --git a/tests/src/UtilTwitch.cpp b/tests/src/UtilTwitch.cpp index e96048d35..36450490f 100644 --- a/tests/src/UtilTwitch.cpp +++ b/tests/src/UtilTwitch.cpp @@ -159,3 +159,108 @@ TEST(UtilTwitch, StripChannelName) << qUtf8Printable(expectedChannelName); } } + +TEST(UtilTwitch, UserLoginRegexp) +{ + struct TestCase { + QString inputUserLogin; + bool matches; + }; + + std::vector tests{ + { + "pajlada", + true, + }, + { + // Login names must not start with an underscore + "_pajlada", + false, + }, + { + "@pajlada", + false, + }, + { + "pajlada!", + false, + }, + { + "pajlada,", + false, + }, + { + // Login names must not contain capital letters + "Pajlada", + false, + }, + {"k", true}, + {"testaccount_420", true}, + {"3v", true}, + {"ron", true}, + {"bits", true}, + }; + + const auto ®exp = twitchUserLoginRegexp(); + + for (const auto &[inputUserLogin, expectedMatch] : tests) + { + const auto &match = regexp.match(inputUserLogin); + auto actual = regexp.match(inputUserLogin); + + EXPECT_EQ(match.hasMatch(), expectedMatch) + << qUtf8Printable(inputUserLogin) << " did not match as expected"; + } +} + +TEST(UtilTwitch, UserNameRegexp) +{ + struct TestCase { + QString inputUserLogin; + bool matches; + }; + + std::vector tests{ + {"PAJLADA", true}, + { + // User names must not start with an underscore + "_pajlada", + false, + }, + { + "@pajlada", + false, + }, + { + "pajlada!", + false, + }, + { + "pajlada,", + false, + }, + { + "Pajlada", + true, + }, + {"k", true}, + {"testaccount_420", true}, + {"3v", true}, + {"ron", true}, + {"bits", true}, + {"3V", true}, + {"Ron", true}, + {"Bits", true}, + }; + + const auto ®exp = twitchUserNameRegexp(); + + for (const auto &[inputUserLogin, expectedMatch] : tests) + { + const auto &match = regexp.match(inputUserLogin); + auto actual = regexp.match(inputUserLogin); + + EXPECT_EQ(match.hasMatch(), expectedMatch) + << qUtf8Printable(inputUserLogin) << " did not match as expected"; + } +}