9#include "notification.h"
10#include "notification_p.h"
14#include <QDBusArgument>
16#include <QImageReader>
17#include <QRegularExpression>
18#include <QXmlStreamReader>
20#include <KApplicationTrader>
22#include <KConfigGroup>
27using namespace NotificationManager;
28using namespace Qt::StringLiterals;
32Notification::Private::Private()
36Notification::Private::~Private()
62 static const QRegularExpression escapeExpr(QStringLiteral(
"&(?!(?:apos|quot|[gl]t|amp);|#)"));
79 t = u
"<html>" + std::move(t) + u
"</html>";
84 static constexpr std::array<QStringView, 10> allowedTags{u
"b", u
"i", u
"u", u
"img", u
"a", u
"html", u
"br", u
"table", u
"tr", u
"td"};
86 out.writeStartDocument();
92 if (std::ranges::find(allowedTags, name) == allowedTags.end()) {
95 out.writeStartElement(name);
97 const QString src = r.attributes().value(
"src").toString();
98 const QStringView alt = r.attributes().value(
"alt");
101 if (url.isLocalFile()) {
102 out.writeAttribute(QStringLiteral(
"src"), src);
107 out.writeAttribute(u
"alt", alt);
110 out.writeAttribute(u
"href", r.attributes().value(
"href"));
116 if (std::ranges::find(allowedTags, name) == allowedTags.end()) {
119 out.writeEndElement();
123 out.writeCharacters(r.text());
126 out.writeEndDocument();
129 qCWarning(NOTIFICATIONMANAGER) <<
"Notification to send to backend contains invalid XML: " << r.errorString() <<
"line" << r.lineNumber() <<
"col"
142 int width, height, rowStride, hasAlpha, bitsPerSample, channels;
151 arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels;
154#define SANITY_CHECK(condition) \
155 if (!(condition)) { \
156 qCWarning(NOTIFICATIONMANAGER) << "Image decoding sanity check failed on" << #condition; \
160 SANITY_CHECK(width > 0);
161 SANITY_CHECK(width < 2048);
162 SANITY_CHECK(height > 0);
163 SANITY_CHECK(height < 2048);
164 SANITY_CHECK(rowStride > 0);
168 auto copyLineRGB32 = [](QRgb *dst,
const char *src,
int width) {
169 const char *
end = src + width * 3;
170 for (; src !=
end; ++dst, src += 3) {
171 *dst = qRgb(src[0], src[1], src[2]);
175 auto copyLineARGB32 = [](QRgb *dst,
const char *src,
int width) {
176 const char *
end = src + width * 4;
177 for (; src !=
end; ++dst, src += 4) {
178 *dst = qRgba(src[0], src[1], src[2], src[3]);
183 void (*fcn)(QRgb *,
const char *, int) =
nullptr;
184 if (bitsPerSample == 8) {
187 fcn = copyLineARGB32;
188 }
else if (channels == 3) {
194 qCWarning(NOTIFICATIONMANAGER) <<
"Unsupported image format (hasAlpha:" << hasAlpha <<
"bitsPerSample:" << bitsPerSample <<
"channels:" << channels
199 QImage image(width, height, format);
201 end = ptr + pixels.length();
202 for (
int y = 0; y < height; ++y, ptr += rowStride) {
203 if (ptr + channels * width > end) {
204 qCWarning(NOTIFICATIONMANAGER) <<
"Image data is incomplete. y:" << y <<
"height:" << height;
207 fcn((QRgb *)image.scanLine(y), ptr, width);
213void Notification::Private::sanitizeImage(
QImage &image)
219 const QSize max = maximumImageSize();
225void Notification::Private::loadImagePath(
const QString &path)
230 s_imageCache.remove(
id);
237 imageUrl =
QUrl(path);
240 qCDebug(NOTIFICATIONMANAGER) <<
"Refused to load image from" <<
path <<
"which isn't a valid local location.";
252 reader.setAutoTransform(
true);
254 if (
QSize imageSize = reader.size(); imageSize.
isValid()) {
255 if (imageSize.width() > maximumImageSize().width() || imageSize.height() > maximumImageSize().height()) {
257 reader.setScaledSize(imageSize);
259 s_imageCache.insert(
id,
new QImage(reader.read()), imageSize.
width() * imageSize.height());
263QString Notification::Private::defaultComponentName()
266 return QStringLiteral(
"plasma_workspace");
269constexpr QSize Notification::Private::maximumImageSize()
271 return QSize(256, 256);
299 return renamedFrom.
contains(desktopId);
302 if (!services.isEmpty()) {
303 service = services.first();
310 const QString snapInstanceName = app->property<
QString>(QStringLiteral(
"X-SnapInstanceName"));
314 if (!services.isEmpty()) {
315 service = services.first();
322void Notification::Private::setDesktopEntry(
const QString &desktopEntry)
326 configurableService =
false;
328 KService::Ptr service = serviceForDesktopEntry(desktopEntry);
330 this->desktopEntry = service->desktopEntryName();
331 serviceName = service->name();
332 applicationIconName = service->icon();
333 configurableService = !service->noDisplay();
336 const bool isDefaultEvent = (notifyRcName == defaultComponentName());
337 configurableNotifyRc =
false;
338 if (!notifyRcName.isEmpty()) {
350 std::reverse(configSources.
begin(), configSources.
end());
351 config.addConfigSources(configSources);
355 const QString iconName = globalGroup.readEntry(
"IconName");
358 if (!iconName.
isEmpty() && (!isDefaultEvent || applicationIconName.isEmpty())) {
359 applicationIconName = iconName;
363 configurableNotifyRc = !config.groupList().filter(regexp).isEmpty();
369 if ((isDefaultEvent || applicationName.isEmpty()) && !serviceName.
isEmpty()) {
370 applicationName = serviceName;
374void Notification::Private::processHints(
const QVariantMap &hints)
376 auto end = hints.end();
378 notifyRcName = hints.value(QStringLiteral(
"x-kde-appname")).toString();
380 setDesktopEntry(hints.value(QStringLiteral(
"desktop-entry")).
toString());
384 const QString applicationDisplayName = hints.value(QStringLiteral(
"x-kde-display-appname")).toString();
385 if (!applicationDisplayName.
isEmpty()) {
386 applicationName = applicationDisplayName;
389 originName = hints.value(QStringLiteral(
"x-kde-origin-name")).toString();
391 eventId = hints.value(QStringLiteral(
"x-kde-eventId")).toString();
392 xdgTokenAppId = hints.value(QStringLiteral(
"x-kde-xdgTokenAppId")).toString();
395 const int urgency = hints.value(QStringLiteral(
"urgency")).toInt(&ok);
406 setUrgency(Notifications::CriticalUrgency);
411 resident = hints.value(QStringLiteral(
"resident")).toBool();
412 transient = hints.value(QStringLiteral(
"transient")).toBool();
414 userActionFeedback = hints.value(QStringLiteral(
"x-kde-user-action-feedback")).toBool();
415 if (userActionFeedback) {
422 replyPlaceholderText = hints.value(QStringLiteral(
"x-kde-reply-placeholder-text")).toString();
423 replySubmitButtonText = hints.value(QStringLiteral(
"x-kde-reply-submit-button-text")).toString();
424 replySubmitButtonIconName = hints.value(QStringLiteral(
"x-kde-reply-submit-button-icon-name")).toString();
426 category = hints.value(QStringLiteral(
"category")).toString();
431 auto it = hints.find(QStringLiteral(
"image-data"));
433 it = hints.find(QStringLiteral(
"image_data"));
439 it = hints.find(QStringLiteral(
"icon_data"));
445 if (!imageFromHint.isNull()) {
446 Q_ASSERT_X(imageFromHint.width() > 0 && imageFromHint.height() > 0,
449 sanitizeImage(imageFromHint);
450 image =
new QImage(imageFromHint);
451 s_imageCache.insert(
id, image, imageFromHint.width() * imageFromHint.height());
456 it = hints.find(QStringLiteral(
"image-path"));
458 it = hints.find(QStringLiteral(
"image_path"));
462 loadImagePath(it->toString());
471 this->urgency = urgency;
477 if (urgency == Notifications::CriticalUrgency) {
482Notification::Notification(uint
id)
490 : d(new Private(*other.d))
494Notification::Notification(
Notification &&other) noexcept
513Notification::~Notification()
518uint Notification::id()
const
523QString Notification::dBusService()
const
525 return d->dBusService;
528void Notification::setDBusService(
const QString &dBusService)
530 d->dBusService = dBusService;
538void Notification::setCreated(
const QDateTime &created)
540 d->created = created;
548void Notification::resetUpdated()
553bool Notification::read()
const
558void Notification::setRead(
bool read)
563QString Notification::summary()
const
568void Notification::setSummary(
const QString &summary)
570 d->summary = summary;
573QString Notification::body()
const
578void Notification::setBody(
const QString &body)
581 d->body = Private::sanitize(body.trimmed());
584QString Notification::rawBody()
const
589QString Notification::icon()
const
594void Notification::setIcon(
const QString &icon)
596 d->loadImagePath(icon);
599QImage Notification::image()
const
601 if (d->s_imageCache.contains(d->id)) {
602 return *d->s_imageCache.object(d->id);
607void Notification::setImage(
const QImage &image)
609 d->s_imageCache.insert(d->id,
new QImage(image));
612QString Notification::desktopEntry()
const
614 return d->desktopEntry;
617void Notification::setDesktopEntry(
const QString &desktopEntry)
619 d->setDesktopEntry(desktopEntry);
622QString Notification::notifyRcName()
const
624 return d->notifyRcName;
627QString Notification::eventId()
const
632QString Notification::applicationName()
const
634 return d->applicationName;
637void Notification::setApplicationName(
const QString &applicationName)
639 d->applicationName = applicationName;
642QString Notification::applicationIconName()
const
644 return d->applicationIconName;
647void Notification::setApplicationIconName(
const QString &applicationIconName)
649 d->applicationIconName = applicationIconName;
652QString Notification::originName()
const
654 return d->originName;
659 return d->actionNames;
664 return d->actionLabels;
667bool Notification::hasDefaultAction()
const
669 return d->hasDefaultAction;
672QString Notification::defaultActionLabel()
const
676 if (!d->notifyRcName.isEmpty() && d->notifyRcName != Private::defaultComponentName()) {
677 return d->defaultActionLabel;
683void Notification::setActions(
const QStringList &actions)
685 if (
actions.count() % 2 != 0) {
686 qCWarning(NOTIFICATIONMANAGER) <<
"List of actions must contain an even number of items, tried to set actions to" <<
actions;
690 d->hasDefaultAction =
false;
691 d->hasConfigureAction =
false;
692 d->hasReplyAction =
false;
697 for (
int i = 0; i <
actions.count(); i += 2) {
701 if (!d->hasDefaultAction && name == QLatin1String(
"default")) {
702 d->hasDefaultAction =
true;
703 d->defaultActionLabel =
label;
707 if (!d->hasConfigureAction && name == QLatin1String(
"settings")) {
708 d->hasConfigureAction =
true;
709 d->configureActionLabel =
label;
713 if (!d->hasReplyAction && name == QLatin1String(
"inline-reply")) {
714 d->hasReplyAction =
true;
715 d->replyActionLabel =
label;
723 d->actionNames = names;
724 d->actionLabels = labels;
742bool Notification::userActionFeedback()
const
744 return d->userActionFeedback;
747int Notification::timeout()
const
752void Notification::setTimeout(
int timeout)
754 d->timeout = timeout;
757bool Notification::configurable()
const
759 return d->hasConfigureAction || d->configurableNotifyRc || d->configurableService;
762QString Notification::configureActionLabel()
const
764 return d->configureActionLabel;
767bool Notification::hasReplyAction()
const
769 return d->hasReplyAction;
772QString Notification::replyActionLabel()
const
774 return d->replyActionLabel;
777QString Notification::replyPlaceholderText()
const
779 return d->replyPlaceholderText;
782QString Notification::replySubmitButtonText()
const
784 return d->replySubmitButtonText;
787QString Notification::replySubmitButtonIconName()
const
789 return d->replySubmitButtonIconName;
792QString Notification::category()
const
797bool Notification::expired()
const
802void Notification::setExpired(
bool expired)
804 d->expired = expired;
807bool Notification::dismissed()
const
812void Notification::setDismissed(
bool dismissed)
814 d->dismissed = dismissed;
817bool Notification::resident()
const
822void Notification::setResident(
bool resident)
824 d->resident = resident;
827bool Notification::transient()
const
832void Notification::setTransient(
bool transient)
834 d->transient = transient;
837QVariantMap Notification::hints()
const
842void Notification::setHints(
const QVariantMap &hints)
847void Notification::processHints(
const QVariantMap &hints)
849 d->processHints(hints);
852bool Notification::wasAddedDuringInhibition()
const
854 return d->wasAddedDuringInhibition;
857void Notification::setWasAddedDuringInhibition(
bool value)
859 d->wasAddedDuringInhibition = value;
static Ptr serviceByDesktopName(const QString &_name)
QExplicitlySharedDataPointer< KService > Ptr
static Ptr serviceByDesktopPath(const QString &_path)
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)
QString name(StandardAction id)
Category category(StandardShortcut id)
QString label(StandardShortcut id)
const QList< QKeySequence > & end()
QDateTime currentDateTimeUtc()
ElementType currentType() const const
bool isNull() const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
bool isEmpty() const const
bool isValid() 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 number(double n, char format, int precision)
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
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