You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

549 lines
15 KiB

/*
* Copyright 2008 Dmitry Suzdalev <dimsuz@gmail.com>
* Copyright 2017 David Edmundson <davidedmundson@kde.org>
* Copyright 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) version 3, or any
* later version accepted by the membership of KDE e.V. (or its
* successor approved by the membership of KDE e.V.), which shall
* act as a proxy defined in Section 6 of version 3 of the license.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "notification.h"
#include "notification_p.h"
#include "notifications.h"
#include <QDBusArgument>
#include <QDateTime>
#include <QDebug>
#include <QImage>
#include <QRegularExpression>
#include <QXmlStreamReader>
#include <KConfig>
#include <KConfigGroup>
#include <KIconLoader>
#include <KService>
#include "notifications.h"
using namespace NotificationManager;
Notification::Private::Private()
{
}
Notification::Private::~Private() = default;
QString Notification::Private::sanitize(const QString &text)
{
// replace all \ns with <br/>
QString t = text;
t.replace(QLatin1String("\n"), QStringLiteral("<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(QRegularExpression(QStringLiteral("<br/>\\s*<br/>(\\s|<br/>)*")), QLatin1String("<br/>"));
// This fancy RegExp escapes every occurrence 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(QRegularExpression(QStringLiteral("&(?!(?:apos|quot|[gl]t|amp);|#)")), QLatin1String("&amp;"));
// Don't bother adding some HTML structure if the body is now empty
if (t.isEmpty()) {
return t;
}
QXmlStreamReader r(QStringLiteral("<html>") + t + QStringLiteral("</html>"));
QString result;
QXmlStreamWriter out(&result);
const QVector<QString> allowedTags = {"b", "i", "u", "img", "a", "html", "br", "table", "tr", "td"};
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()) { // FIXME FIXME FIXME
/*qCWarning(NOTIFICATIONS) << "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;
}
QImage Notification::Private::decodeNotificationSpecImageHint(const QDBusArgument &arg)
{
int width, height, rowStride, hasAlpha, bitsPerSample, channels;
QByteArray pixels;
char* ptr;
char* end;
arg.beginStructure();
arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels;
arg.endStructure();
#define SANITY_CHECK(condition) \
if (!(condition)) { \
qWarning() << "Sanity check failed on" << #condition; \
return QImage(); \
}
SANITY_CHECK(width > 0);
SANITY_CHECK(width < 2048);
SANITY_CHECK(height > 0);
SANITY_CHECK(height < 2048);
SANITY_CHECK(rowStride > 0);
#undef SANITY_CHECK
auto copyLineRGB32 = [](QRgb* dst, const char* src, int width)
{
const char* end = src + width * 3;
for (; src != end; ++dst, src+=3) {
*dst = qRgb(src[0], src[1], src[2]);
}
};
auto copyLineARGB32 = [](QRgb* dst, const char* src, int width)
{
const char* end = src + width * 4;
for (; src != end; ++dst, src+=4) {
*dst = qRgba(src[0], src[1], src[2], src[3]);
}
};
QImage::Format format = QImage::Format_Invalid;
void (*fcn)(QRgb*, const char*, int) = nullptr;
if (bitsPerSample == 8) {
if (channels == 4) {
format = QImage::Format_ARGB32;
fcn = copyLineARGB32;
} else if (channels == 3) {
format = QImage::Format_RGB32;
fcn = copyLineRGB32;
}
}
if (format == QImage::Format_Invalid) {
qWarning() << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels << ")";
return QImage();
}
QImage image(width, height, format);
ptr = pixels.data();
end = ptr + pixels.length();
for (int y=0; y<height; ++y, ptr += rowStride) {
if (ptr + channels * width > end) {
qWarning() << "Image data is incomplete. y:" << y << "height:" << height;
break;
}
fcn((QRgb*)image.scanLine(y), ptr, width);
}
return image;
}
QString Notification::Private::findImageForSpecImagePath(const QString &_path)
{
QString path = _path;
if (path.startsWith(QLatin1String("file:"))) {
QUrl url(path);
path = url.toLocalFile();
}
return KIconLoader::global()->iconPath(path, -KIconLoader::SizeHuge,
true /* canReturnNull */);
}
void Notification::Private::processHints(const QVariantMap &hints)
{
auto end = hints.end();
desktopEntry = hints.value(QStringLiteral("desktop-entry")).toString();
if (!desktopEntry.isEmpty()) {
KService::Ptr service = KService::serviceByStorageId(desktopEntry);
if (service) {
serviceName = service->name();
applicationIconName = service->icon();
}
}
notifyRcName = hints.value(QStringLiteral("x-kde-appname")).toString();
if (!notifyRcName.isEmpty()) {
// Check whether the application actually has notifications we can configure
KConfig config(notifyRcName + QStringLiteral(".notifyrc"), KConfig::NoGlobals);
config.addConfigSources(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
QStringLiteral("knotifications5/") + notifyRcName + QStringLiteral(".notifyrc")));
KConfigGroup globalGroup(&config, "Global");
const QString iconName = globalGroup.readEntry("IconName");
if (!iconName.isEmpty()) {
applicationIconName = iconName;
}
const QRegularExpression regexp(QStringLiteral("^Event/([^/]*)$"));
configurableNotifyRc = !config.groupList().filter(regexp).isEmpty();
}
const QString applicationDisplayName = hints.value(QStringLiteral("x-kde-display-app-name")).toString();
if (!applicationDisplayName.isEmpty()) {
applicationName = applicationDisplayName;
}
eventId = hints.value(QStringLiteral("x-kde-eventId")).toString();
bool ok;
const int urgency = hints.value(QStringLiteral("urgency")).toInt(&ok); // DBus type is actually "byte"
if (ok) {
// FIXME use separate enum again
switch (urgency) {
case 0:
setUrgency(Notifications::LowUrgency);
break;
case 1:
setUrgency(Notifications::NormalUrgency);
break;
case 2:
setUrgency(Notifications::CriticalUrgency);
break;
}
}
urls = QUrl::fromStringList(hints.value(QStringLiteral("x-kde-urls")).toStringList());
// Underscored hints was in use in version 1.1 of the spec but has been
// replaced by dashed hints in version 1.2. We need to support it for
// users of the 1.2 version of the spec.
auto it = hints.find(QStringLiteral("image-data"));
if (it == end) {
it = hints.find(QStringLiteral("image_data"));
}
if (it == end) {
// This hint was in use in version 1.0 of the spec but has been
// replaced by "image_data" in version 1.1. We need to support it for
// users of the 1.0 version of the spec.
it = hints.find(QStringLiteral("icon_data"));
}
if (it != end) {
image = decodeNotificationSpecImageHint(it->value<QDBusArgument>());
}
if (image.isNull()) {
it = hints.find(QStringLiteral("image-path"));
if (it == end) {
it = hints.find(QStringLiteral("image_path"));
}
if (it != end) {
const QString path = findImageForSpecImagePath(it->toString());
if (!path.isEmpty()) {
image.load(path);
}
}
}
}
void Notification::Private::setUrgency(Notifications::Urgencies urgency)
{
this->urgency = urgency;
// Critical notifications must not time out
// TODO should we really imply this here?
if (urgency == Notifications::CriticalUrgency) {
timeout = 0;
}
}
Notification::Notification(uint id)
: d(new Private())
{
d->id = id;
d->created = QDateTime::currentDateTimeUtc();
}
Notification::Notification(const Notification &other)
: d(new Private(*other.d))
{
}
Notification::Notification(Notification &&other)
: d(other.d)
{
other.d = nullptr;
}
Notification &Notification::operator=(const Notification &other)
{
d = new Private(*other.d);
return *this;
}
Notification::~Notification()
{
delete d;
}
uint Notification::id() const
{
return d->id;
}
QDateTime Notification::created() const
{
return d->created;
}
QDateTime Notification::updated() const
{
return d->updated;
}
void Notification::setUpdated()
{
d->updated = QDateTime::currentDateTimeUtc();
}
QString Notification::summary() const
{
return d->summary;
}
void Notification::setSummary(const QString &summary)
{
d->summary = summary;
}
QString Notification::body() const
{
return d->body;
}
void Notification::setBody(const QString &body)
{
d->body = Private::sanitize(body.trimmed());
}
QString Notification::iconName() const
{
return d->iconName;
}
void Notification::setIconName(const QString &iconName)
{
d->iconName = iconName;
}
QImage Notification::image() const
{
return d->image;
}
void Notification::setImage(const QImage &image)
{
d->image = image;
}
QString Notification::desktopEntry() const
{
return d->desktopEntry;
}
QString Notification::applicationName() const
{
return d->applicationName;
}
void Notification::setApplicationName(const QString &applicationName)
{
d->applicationName = applicationName;
}
QString Notification::applicationIconName() const
{
return d->applicationIconName;
}
void Notification::setApplicationIconName(const QString &applicationIconName)
{
d->applicationIconName = applicationIconName;
}
QStringList Notification::actionNames() const
{
return d->actionNames;
}
QStringList Notification::actionLabels() const
{
return d->actionLabels;
}
bool Notification::hasDefaultAction() const
{
return d->hasDefaultAction;
}
/*bool Notification::hasConfigureAction() const
{
return m_hasConfigureAction;
}*/
void Notification::setActions(const QStringList &actions)
{
if (actions.count() % 2 != 0) {
// FIXME qCWarning
qWarning() << "List of actions must contain an even number of items, tried to set actions to" << actions;
return;
}
d->hasDefaultAction = false;
d->hasConfigureAction = false;
QStringList names;
QStringList labels;
for (int i = 0; i < actions.count(); i += 2) {
const QString &name = actions.at(i);
const QString &label = actions.at(i + 1);
if (!d->hasDefaultAction && name == QLatin1String("default")) {
d->hasDefaultAction = true;
continue;
}
if (!d->hasConfigureAction && name == QLatin1String("settings")) {
d->hasConfigureAction = true;
d->configureActionLabel = label;
continue;
}
names << name;
labels << label;
}
d->actionNames = names;
d->actionLabels = labels;
}
QList<QUrl> Notification::urls() const
{
return d->urls;
}
void Notification::setUrls(const QList<QUrl> &urls)
{
d->urls = urls;
}
Notifications::Urgencies Notification::urgency() const
{
return d->urgency;
}
int Notification::timeout() const
{
return d->timeout;
}
void Notification::setTimeout(int timeout)
{
d->timeout = timeout;
}
bool Notification::configurable() const
{
return d->hasConfigureAction || d->configurableNotifyRc;
}
QString Notification::configureActionLabel() const
{
return d->configureActionLabel;
}
bool Notification::expired() const
{
return d->expired;
}
void Notification::setExpired(bool expired)
{
d->expired = expired;
}
bool Notification::dismissed() const
{
return d->dismissed;
}
void Notification::setDismissed(bool dismissed)
{
d->dismissed = dismissed;
}
/*bool Notification::operator==(const Notification &other) const
{
return other.d->id == d->id;
}*/