9#include "notification.h"
10#include "notification_p.h"
12#include <QDBusArgument>
14#include <QImageReader>
15#include <QRegularExpression>
16#include <QXmlStreamReader>
18#include <KApplicationTrader>
20#include <KConfigGroup>
25using namespace NotificationManager;
26using namespace Qt::StringLiterals;
28Notification::Private::Private()
32Notification::Private::~Private() =
default;
50 static const QRegularExpression escapeExpr(QStringLiteral(
"&(?!(?:apos|quot|[gl]t|amp);|#)"));
58 QXmlStreamReader r(QStringLiteral(
"<html>") + t + QStringLiteral(
"</html>"));
62 const QList<QString> allowedTags = {
"b",
"i",
"u",
"img",
"a",
"html",
"br",
"table",
"tr",
"td"};
64 out.writeStartDocument();
73 out.writeStartElement(name);
75 auto src = r.attributes().value(
"src").toString();
76 auto alt = r.attributes().value(
"alt").toString();
79 if (url.isLocalFile()) {
80 out.writeAttribute(QStringLiteral(
"src"), src);
85 out.writeAttribute(QStringLiteral(
"alt"), alt);
88 out.writeAttribute(QStringLiteral(
"href"), r.attributes().value(
"href").toString());
97 out.writeEndElement();
101 const auto text = r.text().toString();
102 out.writeCharacters(text);
105 out.writeEndDocument();
108 qCWarning(NOTIFICATIONMANAGER) <<
"Notification to send to backend contains invalid XML: " << r.errorString() <<
"line" << r.lineNumber() <<
"col"
121 int width, height, rowStride, hasAlpha, bitsPerSample, channels;
130 arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels;
133#define SANITY_CHECK(condition) \
134 if (!(condition)) { \
135 qCWarning(NOTIFICATIONMANAGER) << "Image decoding sanity check failed on" << #condition; \
139 SANITY_CHECK(width > 0);
140 SANITY_CHECK(width < 2048);
141 SANITY_CHECK(height > 0);
142 SANITY_CHECK(height < 2048);
143 SANITY_CHECK(rowStride > 0);
147 auto copyLineRGB32 = [](QRgb *dst,
const char *src,
int width) {
148 const char *
end = src + width * 3;
149 for (; src !=
end; ++dst, src += 3) {
150 *dst = qRgb(src[0], src[1], src[2]);
154 auto copyLineARGB32 = [](QRgb *dst,
const char *src,
int width) {
155 const char *
end = src + width * 4;
156 for (; src !=
end; ++dst, src += 4) {
157 *dst = qRgba(src[0], src[1], src[2], src[3]);
162 void (*fcn)(QRgb *,
const char *, int) =
nullptr;
163 if (bitsPerSample == 8) {
166 fcn = copyLineARGB32;
167 }
else if (channels == 3) {
173 qCWarning(NOTIFICATIONMANAGER) <<
"Unsupported image format (hasAlpha:" << hasAlpha <<
"bitsPerSample:" << bitsPerSample <<
"channels:" << channels
178 QImage image(width, height, format);
180 end = ptr + pixels.length();
181 for (
int y = 0; y < height; ++y, ptr += rowStride) {
182 if (ptr + channels * width > end) {
183 qCWarning(NOTIFICATIONMANAGER) <<
"Image data is incomplete. y:" << y <<
"height:" << height;
186 fcn((QRgb *)image.scanLine(y), ptr, width);
192void Notification::Private::sanitizeImage(
QImage &image)
198 const QSize max = maximumImageSize();
204void Notification::Private::loadImagePath(
const QString &path)
216 imageUrl =
QUrl(path);
219 qCDebug(NOTIFICATIONMANAGER) <<
"Refused to load image from" <<
path <<
"which isn't a valid local location.";
231 reader.setAutoTransform(
true);
233 const QSize imageSize = reader.size();
234 if (imageSize.
isValid() && (imageSize.
width() > maximumImageSize().width() || imageSize.
height() > maximumImageSize().height())) {
236 reader.setScaledSize(thumbnailSize);
239 image = reader.read();
242QString Notification::Private::defaultComponentName()
245 return QStringLiteral(
"plasma_workspace");
248QSize Notification::Private::maximumImageSize()
250 return QSize(256, 256);
278 return renamedFrom.
contains(desktopId);
281 if (!services.isEmpty()) {
282 service = services.first();
289 const QString snapInstanceName = app->property<
QString>(QStringLiteral(
"X-SnapInstanceName"));
293 if (!services.isEmpty()) {
294 service = services.first();
301void Notification::Private::setDesktopEntry(
const QString &desktopEntry)
305 configurableService =
false;
307 KService::Ptr service = serviceForDesktopEntry(desktopEntry);
309 this->desktopEntry = service->desktopEntryName();
310 serviceName = service->name();
311 applicationIconName = service->icon();
312 configurableService = !service->noDisplay();
315 const bool isDefaultEvent = (notifyRcName == defaultComponentName());
316 configurableNotifyRc =
false;
317 if (!notifyRcName.isEmpty()) {
327 config.addConfigSources(configSources);
331 const QString iconName = globalGroup.readEntry(
"IconName");
334 if (!iconName.
isEmpty() && (!isDefaultEvent || applicationIconName.isEmpty())) {
335 applicationIconName = iconName;
339 configurableNotifyRc = !config.groupList().filter(regexp).isEmpty();
345 if ((isDefaultEvent || applicationName.isEmpty()) && !serviceName.
isEmpty()) {
346 applicationName = serviceName;
350void Notification::Private::processHints(
const QVariantMap &hints)
352 auto end = hints.end();
354 notifyRcName = hints.value(QStringLiteral(
"x-kde-appname")).toString();
356 setDesktopEntry(hints.value(QStringLiteral(
"desktop-entry")).
toString());
360 const QString applicationDisplayName = hints.value(QStringLiteral(
"x-kde-display-appname")).toString();
361 if (!applicationDisplayName.
isEmpty()) {
362 applicationName = applicationDisplayName;
365 originName = hints.value(QStringLiteral(
"x-kde-origin-name")).toString();
367 eventId = hints.value(QStringLiteral(
"x-kde-eventId")).toString();
368 xdgTokenAppId = hints.value(QStringLiteral(
"x-kde-xdgTokenAppId")).toString();
371 const int urgency = hints.value(QStringLiteral(
"urgency")).toInt(&ok);
382 setUrgency(Notifications::CriticalUrgency);
387 resident = hints.value(QStringLiteral(
"resident")).toBool();
388 transient = hints.value(QStringLiteral(
"transient")).toBool();
390 userActionFeedback = hints.value(QStringLiteral(
"x-kde-user-action-feedback")).toBool();
391 if (userActionFeedback) {
398 replyPlaceholderText = hints.value(QStringLiteral(
"x-kde-reply-placeholder-text")).toString();
399 replySubmitButtonText = hints.value(QStringLiteral(
"x-kde-reply-submit-button-text")).toString();
400 replySubmitButtonIconName = hints.value(QStringLiteral(
"x-kde-reply-submit-button-icon-name")).toString();
402 category = hints.value(QStringLiteral(
"category")).toString();
407 auto it = hints.find(QStringLiteral(
"image-data"));
409 it = hints.find(QStringLiteral(
"image_data"));
415 it = hints.find(QStringLiteral(
"icon_data"));
419 image = decodeNotificationSpecImageHint(it->value<
QDBusArgument>());
423 it = hints.find(QStringLiteral(
"image-path"));
425 it = hints.find(QStringLiteral(
"image_path"));
429 loadImagePath(it->toString());
433 sanitizeImage(image);
438 this->urgency = urgency;
444 if (urgency == Notifications::CriticalUrgency) {
449Notification::Notification(uint
id)
457 : d(new Private(*other.d))
461Notification::Notification(
Notification &&other) noexcept
480Notification::~Notification()
485uint Notification::id()
const
490QString Notification::dBusService()
const
492 return d->dBusService;
495void Notification::setDBusService(
const QString &dBusService)
497 d->dBusService = dBusService;
505void Notification::setCreated(
const QDateTime &created)
507 d->created = created;
515void Notification::resetUpdated()
520bool Notification::read()
const
525void Notification::setRead(
bool read)
530QString Notification::summary()
const
535void Notification::setSummary(
const QString &summary)
537 d->summary = summary;
540QString Notification::body()
const
545void Notification::setBody(
const QString &body)
548 d->body = Private::sanitize(body.
trimmed());
551QString Notification::rawBody()
const
556QString Notification::icon()
const
561void Notification::setIcon(
const QString &icon)
563 d->loadImagePath(icon);
564 Private::sanitizeImage(d->image);
567QImage Notification::image()
const
572void Notification::setImage(
const QImage &image)
577QString Notification::desktopEntry()
const
579 return d->desktopEntry;
582void Notification::setDesktopEntry(
const QString &desktopEntry)
584 d->setDesktopEntry(desktopEntry);
587QString Notification::notifyRcName()
const
589 return d->notifyRcName;
592QString Notification::eventId()
const
597QString Notification::applicationName()
const
599 return d->applicationName;
602void Notification::setApplicationName(
const QString &applicationName)
604 d->applicationName = applicationName;
607QString Notification::applicationIconName()
const
609 return d->applicationIconName;
612void Notification::setApplicationIconName(
const QString &applicationIconName)
614 d->applicationIconName = applicationIconName;
617QString Notification::originName()
const
619 return d->originName;
624 return d->actionNames;
629 return d->actionLabels;
632bool Notification::hasDefaultAction()
const
634 return d->hasDefaultAction;
637QString Notification::defaultActionLabel()
const
639 return d->defaultActionLabel;
642void Notification::setActions(
const QStringList &actions)
644 if (actions.
count() % 2 != 0) {
645 qCWarning(NOTIFICATIONMANAGER) <<
"List of actions must contain an even number of items, tried to set actions to" << actions;
649 d->hasDefaultAction =
false;
650 d->hasConfigureAction =
false;
651 d->hasReplyAction =
false;
656 for (
int i = 0; i < actions.
count(); i += 2) {
660 if (!d->hasDefaultAction && name ==
QLatin1String(
"default")) {
661 d->hasDefaultAction =
true;
662 d->defaultActionLabel =
label;
666 if (!d->hasConfigureAction && name ==
QLatin1String(
"settings")) {
667 d->hasConfigureAction =
true;
668 d->configureActionLabel =
label;
672 if (!d->hasReplyAction && name ==
QLatin1String(
"inline-reply")) {
673 d->hasReplyAction =
true;
674 d->replyActionLabel =
label;
682 d->actionNames = names;
683 d->actionLabels = labels;
701bool Notification::userActionFeedback()
const
703 return d->userActionFeedback;
706int Notification::timeout()
const
711void Notification::setTimeout(
int timeout)
713 d->timeout = timeout;
716bool Notification::configurable()
const
718 return d->hasConfigureAction || d->configurableNotifyRc || d->configurableService;
721QString Notification::configureActionLabel()
const
723 return d->configureActionLabel;
726bool Notification::hasReplyAction()
const
728 return d->hasReplyAction;
731QString Notification::replyActionLabel()
const
733 return d->replyActionLabel;
736QString Notification::replyPlaceholderText()
const
738 return d->replyPlaceholderText;
741QString Notification::replySubmitButtonText()
const
743 return d->replySubmitButtonText;
746QString Notification::replySubmitButtonIconName()
const
748 return d->replySubmitButtonIconName;
751QString Notification::category()
const
756bool Notification::expired()
const
761void Notification::setExpired(
bool expired)
763 d->expired = expired;
766bool Notification::dismissed()
const
771void Notification::setDismissed(
bool dismissed)
773 d->dismissed = dismissed;
776bool Notification::resident()
const
781void Notification::setResident(
bool resident)
783 d->resident = resident;
786bool Notification::transient()
const
791void Notification::setTransient(
bool transient)
796QVariantMap Notification::hints()
const
801void Notification::setHints(
const QVariantMap &hints)
806void Notification::processHints(
const QVariantMap &hints)
808 d->processHints(hints);
static Ptr serviceByDesktopName(const QString &_name)
static Ptr serviceByDesktopPath(const QString &_path)
Represents a single notification.
Urgency
The notification urgency.
@ LowUrgency
The notification has low urgency, it is not important and may not be shown or added to a history.
@ NormalUrgency
The notification has normal urgency. This is also the default if no urgecny is supplied.
char * toString(const EngineQuery &query)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
QVariant read(const QByteArray &data, int versionOverride=0)
QString path(const QString &relativePath)
void transient(const QString &message, const QString &title)
Category category(StandardShortcut id)
QString label(StandardShortcut id)
const QList< QKeySequence > & end()
QString name(StandardShortcut id)
QDateTime currentDateTimeUtc()
ElementType currentType() const const
bool isNull() const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
const_reference at(qsizetype i) const const
bool contains(const AT &value) const const
qsizetype count() const const
bool isEmpty() const const
bool isValid() const const
QSize scaled(const QSize &s, Qt::AspectRatioMode mode) const const
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString simplified() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toLower() const const
QString trimmed() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QUrl fromLocalFile(const QString &localFile)
QList< QUrl > fromStringList(const QStringList &urls, ParsingMode mode)
bool isLocalFile() const const
bool isValid() const const
QString toLocalFile() const const