diff --git a/dataengines/notifications/CMakeLists.txt b/dataengines/notifications/CMakeLists.txt --- a/dataengines/notifications/CMakeLists.txt +++ b/dataengines/notifications/CMakeLists.txt @@ -4,6 +4,7 @@ notificationsengine.cpp notificationservice.cpp notificationaction.cpp + notificationsanitizer.cpp ) qt5_add_dbus_adaptor( notifications_engine_SRCS org.freedesktop.Notifications.xml notificationsengine.h NotificationsEngine ) @@ -26,3 +27,10 @@ install(TARGETS plasma_engine_notifications DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) install(FILES plasma-dataengine-notifications.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) install(FILES notifications.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) + + +#unit test + +add_executable(notification_test notificationsanitizer.cpp notifications_test.cpp) +target_link_libraries(notification_test Qt5::Test Qt5::Core) +ecm_mark_as_test(notification_test) diff --git a/dataengines/notifications/notifications_test.cpp b/dataengines/notifications/notifications_test.cpp new file mode 100644 --- /dev/null +++ b/dataengines/notifications/notifications_test.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include "notificationsanitizer.h" + +class NotificationTest : public QObject +{ + Q_OBJECT +public: + NotificationTest() {} +private Q_SLOTS: + void parse_data(); + void parse(); +}; + +void NotificationTest::parse_data() +{ + QTest::addColumn("messageIn"); + QTest::addColumn("expectedOut"); + + QTest::newRow("basic no HTML") << "I am a notification" << "I am a notification"; + QTest::newRow("whitespace") << " I am a notification " << "I am a notification"; + + QTest::newRow("basic html") << "I am the notification" << "I am the notification"; + QTest::newRow("nested html") << "I am the notification" << "I am the notification"; + + QTest::newRow("no extra tags") << "I am the notification" << "I am the notification"; + QTest::newRow("no extra attrs") << "I am the notification" << "I am the notification"; + + QTest::newRow("newlines") << "I am\nthe\nnotification" << "I am
the
notification"; + QTest::newRow("multinewlines") << "I am\n\nthe\n\n\nnotification" << "I am
the
notification"; + + QTest::newRow("amp") << "me&you" << "me&you"; + QTest::newRow("double escape") << "foo & <bar>" << "foo & <bar>"; + + QTest::newRow("quotes") << "'foo'" << "'foo'";//as label can't handle this normally valid entity + + QTest::newRow("image normal") << "This is \"cheese\"/ and more text" << "This is \"cheese\"/ and more text"; + + //this input is technically wrong, so the output is also wrong, but QTextHtmlParser does the same thing + QTest::newRow("image normal no close") << "This is \"cheese\" and more text" << "This is \"cheese\" and more text"; + + QTest::newRow("image remote URL") << "This is \"cheese\" and more text" << "This is \"cheese\"/ and more text"; + QTest::newRow("image remote URL no close") << "This is \"cheese\" and more text" << "This is \"cheese\" and more text"; + + QTest::newRow("link") << "This is a link and more text" << "This is a link and more text"; +} + +void NotificationTest::parse() +{ + QFETCH(QString, messageIn); + QFETCH(QString, expectedOut); + + const QString out = NotificationSanitizer::parse(messageIn); + expectedOut = "" + expectedOut + "\n"; + QCOMPARE(out, expectedOut); +} + + +QTEST_GUILESS_MAIN(NotificationTest) + +#include "notifications_test.moc" diff --git a/dataengines/notifications/notificationsanitizer.h b/dataengines/notifications/notificationsanitizer.h new file mode 100644 --- /dev/null +++ b/dataengines/notifications/notificationsanitizer.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 David Edmundson + * + * This program is free software you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. +*/ + +#include + +namespace NotificationSanitizer +{ + /* + * This turns generic random text of either plain text of any degree of faux-HTML into HTML allowed + * in the notification spec namely: + * a, img, b, i, u and br + * All other tags and attributes are stripped + * Whitespace is stripped and converted to
+ * Double newlines are compressed + * + * Image src is only copied when referring to a local file + */ + QString parse(const QString &in); +} diff --git a/dataengines/notifications/notificationsanitizer.cpp b/dataengines/notifications/notificationsanitizer.cpp new file mode 100644 --- /dev/null +++ b/dataengines/notifications/notificationsanitizer.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2017 David Edmundson + * + * This program is free software you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. +*/ + +#include "notificationsanitizer.h" + +#include +#include +#include +#include +#include + +QString NotificationSanitizer::parse(const QString &text) +{ + // replace all \ns with
+ QString t = text; + + t.replace(QLatin1String("\n"), QStringLiteral("
")); + // Now remove all inner whitespace (\ns are already
s) + t = t.simplified(); + // Finally, check if we don't have multiple
s following, + // can happen for example when "\n \n" is sent, this replaces + // all
s in succsession with just one + t.replace(QRegularExpression(QStringLiteral("
\\s*
(\\s|
)*")), QLatin1String("
")); + // This fancy RegExp escapes every occurence of & since QtQuick Text will blatantly cut off + // text where it finds a stray ampersand. + // Only &{apos, quot, gt, lt, amp}; as well as { character references will be allowed + t.replace(QRegularExpression(QStringLiteral("&(?!(?:apos|quot|[gl]t|amp);|#)")), QLatin1String("&")); + + QXmlStreamReader r(QStringLiteral("") + t + QStringLiteral("")); + QString result; + QXmlStreamWriter out(&result); + + QVector allowedTags = {"b", "i", "u", "img", "a", "html", "br"}; + + out.writeStartDocument(); + while (!r.atEnd()) { + r.readNext(); + + if (r.tokenType() == QXmlStreamReader::StartElement) { + const QString name = r.name().toString(); + if (!allowedTags.contains(name)) { + continue; + } + out.writeStartElement(name); + if (name == QLatin1String("img")) { + auto src = r.attributes().value("src").toString(); + auto alt = r.attributes().value("alt").toString(); + + const QUrl url(src); + if (url.isLocalFile()) { + out.writeAttribute(QStringLiteral("src"), src); + } else { + //image denied for security reasons! Do not copy the image src here! + } + + out.writeAttribute(QStringLiteral("alt"), alt); + } + if (name == QLatin1String("a")) { + out.writeAttribute(QStringLiteral("href"), r.attributes().value("href").toString()); + } + } + + if (r.tokenType() == QXmlStreamReader::EndElement) { + const QString name = r.name().toString(); + if (!allowedTags.contains(name)) { + continue; + } + out.writeEndElement(); + } + + if (r.tokenType() == QXmlStreamReader::Characters) { + const auto text = r.text().toString(); + out.writeCharacters(text); //this auto escapes chars -> HTML entities + } + } + out.writeEndDocument(); + + if (r.hasError()) { + qWarning() << "Notification to send to backend contains invalid XML: " + << r.errorString() << "line" << r.lineNumber() + << "col" << r.columnNumber(); + } + + // The Text.StyledText format handles only html3.2 stuff and ' is html4 stuff + // so we need to replace it here otherwise it will not render at all. + result = result.replace(QLatin1String("'"), QChar('\'')); + + + return result; +} diff --git a/dataengines/notifications/notificationsengine.cpp b/dataengines/notifications/notificationsengine.cpp --- a/dataengines/notifications/notificationsengine.cpp +++ b/dataengines/notifications/notificationsengine.cpp @@ -20,6 +20,7 @@ #include "notificationsengine.h" #include "notificationservice.h" #include "notificationsadaptor.h" +#include "notificationsanitizer.h" #include #include @@ -261,23 +262,7 @@ const QString source = QStringLiteral("notification %1").arg(id); QString bodyFinal = (partOf == 0 ? body : _body); - // First trim whitespace from beginning and end - bodyFinal = bodyFinal.trimmed(); - // Now replace all \ns with
- bodyFinal = bodyFinal.replace(QLatin1String("\n"), QLatin1String("
")); - // Now remove all inner whitespace (\ns are already
s - bodyFinal = bodyFinal.simplified(); - // Finally, check if we don't have multiple
s following, - // can happen for example when "\n \n" is sent, this replaces - // all
s in succsession with just one - bodyFinal.replace(QRegularExpression(QStringLiteral("
\\s*
(\\s|
)*")), QLatin1String("
")); - // This fancy RegExp escapes every occurence of & since QtQuick Text will blatantly cut off - // text where it finds a stray ampersand. - // Only &{apos, quot, gt, lt, amp}; as well as { character references will be allowed - bodyFinal.replace(QRegularExpression(QStringLiteral("&(?!(?:apos|quot|[gl]t|amp);|#)")), QLatin1String("&")); - // The Text.StyledText format handles only html3.2 stuff and ' is html4 stuff - // so we need to replace it here otherwise it will not render at all. - bodyFinal.replace(QLatin1String("'"), QChar('\'')); + bodyFinal = NotificationSanitizer::parse(bodyFinal); Plasma::DataEngine::Data notificationData; notificationData.insert(QStringLiteral("id"), QString::number(id));