Blob Blame History Raw
diff --git a/plasma/generic/dataengines/notifications/CMakeLists.txt b/plasma/generic/dataengines/notifications/CMakeLists.txt
index cf34971..e1d567f 100644
--- a/plasma/generic/dataengines/notifications/CMakeLists.txt
+++ b/plasma/generic/dataengines/notifications/CMakeLists.txt
@@ -2,6 +2,7 @@ set(notifications_engine_SRCS
     notificationsengine.cpp
     notificationservice.cpp
     notificationaction.cpp
+    notificationsanitizer.cpp
 )

 qt4_add_dbus_adaptor( notifications_engine_SRCS org.freedesktop.Notifications.xml notificationsengine.h  NotificationsEngine )
@@ -13,3 +14,15 @@ target_link_libraries(plasma_engine_notifications ${KDE4_PLASMA_LIBS} ${KDE4_KDE
 install(TARGETS plasma_engine_notifications DESTINATION ${PLUGIN_INSTALL_DIR})
 install(FILES plasma-dataengine-notifications.desktop DESTINATION ${SERVICES_INSTALL_DIR} )
 install(FILES notifications.operations DESTINATION ${DATA_INSTALL_DIR}/plasma/services)
+
+set(notificationstest_SRCS notificationsanitizer.cpp notifications_test.cpp)
+
+kde4_add_unit_test( notificationstest
+    TESTNAME notifications-notificationstest
+    ${notificationstest_SRCS}
+)
+
+target_link_libraries(notificationstest
+    ${QT_QTTEST_LIBRARY}
+    ${KDE4_KDECORE_LIBS}
+)
\ No newline at end of file
diff --git a/plasma/generic/dataengines/notifications/notifications_test.cpp b/plasma/generic/dataengines/notifications/notifications_test.cpp
new file mode 100644
index 0000000..ffa5187
--- /dev/null
+++ b/plasma/generic/dataengines/notifications/notifications_test.cpp
@@ -0,0 +1,68 @@
+#include <QtTest>
+#include <QObject>
+#include <QDebug>
+#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<QString>("messageIn");
+    QTest::addColumn<QString>("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 <b>the</b> notification" << "I am <b>the</b> notification";
+    QTest::newRow("nested html") << "I am <i><b>the</b></i> notification" << "I am <i><b>the</b></i> notification";
+
+    QTest::newRow("no extra tags") << "I am <blink>the</blink> notification" << "I am the notification";
+    QTest::newRow("no extra attrs") << "I am <b style=\"font-weight:20\">the</b> notification" << "I am <b>the</b> notification";
+
+    QTest::newRow("newlines") << "I am\nthe\nnotification" << "I am<br/>the<br/>notification";
+    QTest::newRow("multinewlines") << "I am\n\nthe\n\n\nnotification" << "I am<br/>the<br/>notification";
+
+    QTest::newRow("amp") << "me&you" << "me&amp;you";
+    QTest::newRow("double escape") << "foo &amp; &lt;bar&gt;" << "foo &amp; &lt;bar&gt;";
+
+    QTest::newRow("quotes") << "&apos;foo&apos;" << "'foo'";//as label can't handle this normally valid entity
+
+    QTest::newRow("image normal") << "This is <img src=\"file:://foo/boo.png\" alt=\"cheese\"/> and more text" << "This is <img src=\"file:://foo/boo.png\" alt=\"cheese\"/> and more text";
+
+    //this input is technically wrong, so the output is also wrong, but QTextHtmlParser does the "right" thing
+    QTest::newRow("image normal no close") << "This is <img src=\"file:://foo/boo.png\" alt=\"cheese\"> and more text" << "This is <img src=\"file:://foo/boo.png\" alt=\"cheese\"> and more text</img>";
+
+    QTest::newRow("image remote URL") << "This is <img src=\"http://foo.com/boo.png\" alt=\"cheese\" /> and more text" << "This is <img alt=\"cheese\"/> and more text";
+
+    //more bad formatted options. To some extent actual output doesn't matter. Garbage in, garbabe out.
+    //the important thing is that it doesn't contain anything that could be parsed as the remote URL
+    QTest::newRow("image remote URL no close") << "This is <img src=\"http://foo.com/boo.png>\" alt=\"cheese\">  and more text" << "This is <img alt=\"cheese\"> and more text</img>";
+    QTest::newRow("image remote URL double open") << "This is <<img src=\"http://foo.com/boo.png>\"  and more text" << "This is ";
+    QTest::newRow("image remote URL no entitiy close") << "This is <img src=\"http://foo.com/boo.png\"  and more text" << "This is ";
+    QTest::newRow("image remote URL space in element name") << "This is < img src=\"http://foo.com/boo.png\" alt=\"cheese\" /> and more text" << "This is ";
+
+    QTest::newRow("link") << "This is a link <a href=\"http://foo.com/boo\"/> and more text" << "This is a link <a href=\"http://foo.com/boo\"/> and more text";
+}
+
+void NotificationTest::parse()
+{
+    QFETCH(QString, messageIn);
+    QFETCH(QString, expectedOut);
+
+    const QString out = NotificationSanitizer::parse(messageIn);
+    expectedOut = "<?xml version=\"1.0\"?><html>"  + expectedOut + "</html>\n";
+    QCOMPARE(out, expectedOut);
+}
+
+
+QTEST_MAIN(NotificationTest)
+
+#include "notificationtest.moc"
\ No newline at end of file
diff --git a/plasma/generic/dataengines/notifications/notificationsanitizer.cpp b/plasma/generic/dataengines/notifications/notificationsanitizer.cpp
new file mode 100644
index 0000000..8750958
--- /dev/null
+++ b/plasma/generic/dataengines/notifications/notificationsanitizer.cpp
@@ -0,0 +1,106 @@
+/*
+ *   Copyright (C) 2017 David Edmundson <davidedmundson@kde.org>
+ *
+ * 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 <QXmlStreamReader>
+#include <QXmlStreamWriter>
+#include <QRegExp>
+#include <QDebug>
+#include <QUrl>
+
+QString NotificationSanitizer::parse(const QString &text)
+{
+    // replace all \ns with <br/>
+    QString t = text;
+
+    t.replace(QLatin1String("\n"), QLatin1String("<br/>"));
+    // Now remove all inner whitespace (\ns are already <br/>s)
+    t = t.simplified();
+    // Finally, check if we don't have multiple <br/>s following,
+    // can happen for example when "\n       \n" is sent, this replaces
+    // all <br/>s in succsession with just one
+    t.replace(QRegExp(QLatin1String("<br/>\\s*<br/>(\\s|<br/>)*")), QLatin1String("<br/>"));
+    // 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 &#123 character references will be allowed
+    t.replace(QRegExp(QLatin1String("&(?!(?:apos|quot|[gl]t|amp);|#)")), QLatin1String("&amp;"));
+
+    QXmlStreamReader r(QLatin1String("<html>") + t + QLatin1String("</html>"));
+    QString result;
+    QXmlStreamWriter out(&result);
+
+    QVector<QString> allowedTags;
+    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")) {
+                QString src = r.attributes().value("src").toString();
+                QString alt = r.attributes().value("alt").toString();
+
+                const QUrl url(src);
+                if (url.isLocalFile()) {
+                    out.writeAttribute(QLatin1String("src"), src);
+                } else {
+                    //image denied for security reasons! Do not copy the image src here!
+                }
+
+                out.writeAttribute(QLatin1String("alt"), alt);
+            }
+            if (name == QLatin1String("a")) {
+                out.writeAttribute(QLatin1String("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 QString 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 &apos; is html4 stuff
+    // so we need to replace it here otherwise it will not render at all.
+    result = result.replace(QLatin1String("&apos;"), QChar('\''));
+
+    return result;
+}
diff --git a/plasma/generic/dataengines/notifications/notificationsanitizer.h b/plasma/generic/dataengines/notifications/notificationsanitizer.h
new file mode 100644
index 0000000..b0c3ccd
--- /dev/null
+++ b/plasma/generic/dataengines/notifications/notificationsanitizer.h
@@ -0,0 +1,35 @@
+/*
+ *   Copyright (C) 2017 David Edmundson <davidedmundson@kde.org>
+ *
+ * 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 <QString>
+
+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 <br/>
+     * Double newlines are compressed
+     *
+     * Image src is only copied when referring to a local file
+     */
+    QString parse(const QString &in);
+}