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);|#)"));
70 t = u
"<html>" + std::move(t) + u
"</html>";
75 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"};
77 out.writeStartDocument();
83 if (std::ranges::find(allowedTags, name) == allowedTags.end()) {
86 out.writeStartElement(name);
88 const QString src = r.attributes().value(
"src").toString();
89 const QStringView alt = r.attributes().value(
"alt");
92 if (url.isLocalFile()) {
93 out.writeAttribute(QStringLiteral(
"src"), src);
98 out.writeAttribute(u
"alt", alt);
101 out.writeAttribute(u
"href", r.attributes().value(
"href"));
107 if (std::ranges::find(allowedTags, name) == allowedTags.end()) {
110 out.writeEndElement();
114 out.writeCharacters(r.text());
117 out.writeEndDocument();
120 qCWarning(NOTIFICATIONMANAGER) <<
"Notification to send to backend contains invalid XML: " << r.errorString() <<
"line" << r.lineNumber() <<
"col"
133 int width, height, rowStride, hasAlpha, bitsPerSample, channels;
142 arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels;
145#define SANITY_CHECK(condition) \
146 if (!(condition)) { \
147 qCWarning(NOTIFICATIONMANAGER) << "Image decoding sanity check failed on" << #condition; \
151 SANITY_CHECK(width > 0);
152 SANITY_CHECK(width < 2048);
153 SANITY_CHECK(height > 0);
154 SANITY_CHECK(height < 2048);
155 SANITY_CHECK(rowStride > 0);
159 auto copyLineRGB32 = [](QRgb *dst,
const char *src,
int width) {
160 const char *
end = src + width * 3;
161 for (; src !=
end; ++dst, src += 3) {
162 *dst = qRgb(src[0], src[1], src[2]);
166 auto copyLineARGB32 = [](QRgb *dst,
const char *src,
int width) {
167 const char *
end = src + width * 4;
168 for (; src !=
end; ++dst, src += 4) {
169 *dst = qRgba(src[0], src[1], src[2], src[3]);
174 void (*fcn)(QRgb *,
const char *, int) =
nullptr;
175 if (bitsPerSample == 8) {
178 fcn = copyLineARGB32;
179 }
else if (channels == 3) {
185 qCWarning(NOTIFICATIONMANAGER) <<
"Unsupported image format (hasAlpha:" << hasAlpha <<
"bitsPerSample:" << bitsPerSample <<
"channels:" << channels
190 QImage image(width, height, format);
192 end = ptr + pixels.length();
193 for (
int y = 0; y < height; ++y, ptr += rowStride) {
194 if (ptr + channels * width > end) {
195 qCWarning(NOTIFICATIONMANAGER) <<
"Image data is incomplete. y:" << y <<
"height:" << height;
198 fcn((QRgb *)image.scanLine(y), ptr, width);
204void Notification::Private::sanitizeImage(
QImage &image)
210 const QSize max = maximumImageSize();
216void Notification::Private::loadImagePath(
const QString &path)
221 s_imageCache.remove(
id);
228 imageUrl =
QUrl(path);
231 qCDebug(NOTIFICATIONMANAGER) <<
"Refused to load image from" <<
path <<
"which isn't a valid local location.";
243 reader.setAutoTransform(
true);
245 if (
QSize imageSize = reader.size(); imageSize.
isValid()) {
246 if (imageSize.width() > maximumImageSize().width() || imageSize.height() > maximumImageSize().height()) {
248 reader.setScaledSize(imageSize);
250 s_imageCache.insert(
id,
new QImage(reader.read()), imageSize.
width() * imageSize.height());
254QString Notification::Private::defaultComponentName()
257 return QStringLiteral(
"plasma_workspace");
260constexpr QSize Notification::Private::maximumImageSize()
262 return QSize(256, 256);
290 return renamedFrom.
contains(desktopId);
293 if (!services.isEmpty()) {
294 service = services.first();
301 const QString snapInstanceName = app->property<
QString>(QStringLiteral(
"X-SnapInstanceName"));
305 if (!services.isEmpty()) {
306 service = services.first();
313void Notification::Private::setDesktopEntry(
const QString &desktopEntry)
317 configurableService =
false;
319 KService::Ptr service = serviceForDesktopEntry(desktopEntry);
321 this->desktopEntry = service->desktopEntryName();
322 serviceName = service->name();
323 applicationIconName = service->icon();
324 configurableService = !service->noDisplay();
327 const bool isDefaultEvent = (notifyRcName == defaultComponentName());
328 configurableNotifyRc =
false;
329 if (!notifyRcName.isEmpty()) {
341 std::reverse(configSources.
begin(), configSources.
end());
342 config.addConfigSources(configSources);
346 const QString iconName = globalGroup.readEntry(
"IconName");
349 if (!iconName.
isEmpty() && (!isDefaultEvent || applicationIconName.isEmpty())) {
350 applicationIconName = iconName;
354 configurableNotifyRc = !config.groupList().filter(regexp).isEmpty();
360 if ((isDefaultEvent || applicationName.isEmpty()) && !serviceName.
isEmpty()) {
361 applicationName = serviceName;
365void Notification::Private::processHints(
const QVariantMap &hints)
367 auto end = hints.end();
369 notifyRcName = hints.value(QStringLiteral(
"x-kde-appname")).toString();
371 setDesktopEntry(hints.value(QStringLiteral(
"desktop-entry")).
toString());
375 const QString applicationDisplayName = hints.value(QStringLiteral(
"x-kde-display-appname")).toString();
376 if (!applicationDisplayName.
isEmpty()) {
377 applicationName = applicationDisplayName;
380 originName = hints.value(QStringLiteral(
"x-kde-origin-name")).toString();
382 eventId = hints.value(QStringLiteral(
"x-kde-eventId")).toString();
383 xdgTokenAppId = hints.value(QStringLiteral(
"x-kde-xdgTokenAppId")).toString();
386 const int urgency = hints.value(QStringLiteral(
"urgency")).toInt(&ok);
397 setUrgency(Notifications::CriticalUrgency);
402 resident = hints.value(QStringLiteral(
"resident")).toBool();
403 transient = hints.value(QStringLiteral(
"transient")).toBool();
405 userActionFeedback = hints.value(QStringLiteral(
"x-kde-user-action-feedback")).toBool();
406 if (userActionFeedback) {
413 replyPlaceholderText = hints.value(QStringLiteral(
"x-kde-reply-placeholder-text")).toString();
414 replySubmitButtonText = hints.value(QStringLiteral(
"x-kde-reply-submit-button-text")).toString();
415 replySubmitButtonIconName = hints.value(QStringLiteral(
"x-kde-reply-submit-button-icon-name")).toString();
417 category = hints.value(QStringLiteral(
"category")).toString();
422 auto it = hints.find(QStringLiteral(
"image-data"));
424 it = hints.find(QStringLiteral(
"image_data"));
430 it = hints.find(QStringLiteral(
"icon_data"));
436 if (!imageFromHint.isNull()) {
437 Q_ASSERT_X(imageFromHint.width() > 0 && imageFromHint.height() > 0,
440 sanitizeImage(imageFromHint);
441 image =
new QImage(imageFromHint);
442 s_imageCache.insert(
id, image, imageFromHint.width() * imageFromHint.height());
447 it = hints.find(QStringLiteral(
"image-path"));
449 it = hints.find(QStringLiteral(
"image_path"));
453 loadImagePath(it->toString());
460 this->urgency = urgency;
466 if (urgency == Notifications::CriticalUrgency) {
471Notification::Notification(uint
id)
479 : d(new Private(*other.d))
483Notification::Notification(
Notification &&other) noexcept
502Notification::~Notification()
507uint Notification::id()
const
512QString Notification::dBusService()
const
514 return d->dBusService;
517void Notification::setDBusService(
const QString &dBusService)
519 d->dBusService = dBusService;
527void Notification::setCreated(
const QDateTime &created)
529 d->created = created;
537void Notification::resetUpdated()
542bool Notification::read()
const
547void Notification::setRead(
bool read)
552QString Notification::summary()
const
557void Notification::setSummary(
const QString &summary)
559 d->summary = summary;
562QString Notification::body()
const
567void Notification::setBody(
const QString &body)
570 d->body = Private::sanitize(body.trimmed());
573QString Notification::rawBody()
const
578QString Notification::icon()
const
583void Notification::setIcon(
const QString &icon)
585 d->loadImagePath(icon);
588QImage Notification::image()
const
590 if (d->s_imageCache.contains(d->id)) {
591 return *d->s_imageCache.object(d->id);
596void Notification::setImage(
const QImage &image)
598 d->s_imageCache.insert(d->id,
new QImage(image));
601QString Notification::desktopEntry()
const
603 return d->desktopEntry;
606void Notification::setDesktopEntry(
const QString &desktopEntry)
608 d->setDesktopEntry(desktopEntry);
611QString Notification::notifyRcName()
const
613 return d->notifyRcName;
616QString Notification::eventId()
const
621QString Notification::applicationName()
const
623 return d->applicationName;
626void Notification::setApplicationName(
const QString &applicationName)
628 d->applicationName = applicationName;
631QString Notification::applicationIconName()
const
633 return d->applicationIconName;
636void Notification::setApplicationIconName(
const QString &applicationIconName)
638 d->applicationIconName = applicationIconName;
641QString Notification::originName()
const
643 return d->originName;
648 return d->actionNames;
653 return d->actionLabels;
656bool Notification::hasDefaultAction()
const
658 return d->hasDefaultAction;
661QString Notification::defaultActionLabel()
const
663 return d->defaultActionLabel;
666void Notification::setActions(
const QStringList &actions)
668 if (
actions.count() % 2 != 0) {
669 qCWarning(NOTIFICATIONMANAGER) <<
"List of actions must contain an even number of items, tried to set actions to" <<
actions;
673 d->hasDefaultAction =
false;
674 d->hasConfigureAction =
false;
675 d->hasReplyAction =
false;
680 for (
int i = 0; i <
actions.count(); i += 2) {
684 if (!d->hasDefaultAction && name == QLatin1String(
"default")) {
685 d->hasDefaultAction =
true;
686 d->defaultActionLabel =
label;
690 if (!d->hasConfigureAction && name == QLatin1String(
"settings")) {
691 d->hasConfigureAction =
true;
692 d->configureActionLabel =
label;
696 if (!d->hasReplyAction && name == QLatin1String(
"inline-reply")) {
697 d->hasReplyAction =
true;
698 d->replyActionLabel =
label;
706 d->actionNames = names;
707 d->actionLabels = labels;
725bool Notification::userActionFeedback()
const
727 return d->userActionFeedback;
730int Notification::timeout()
const
735void Notification::setTimeout(
int timeout)
737 d->timeout = timeout;
740bool Notification::configurable()
const
742 return d->hasConfigureAction || d->configurableNotifyRc || d->configurableService;
745QString Notification::configureActionLabel()
const
747 return d->configureActionLabel;
750bool Notification::hasReplyAction()
const
752 return d->hasReplyAction;
755QString Notification::replyActionLabel()
const
757 return d->replyActionLabel;
760QString Notification::replyPlaceholderText()
const
762 return d->replyPlaceholderText;
765QString Notification::replySubmitButtonText()
const
767 return d->replySubmitButtonText;
770QString Notification::replySubmitButtonIconName()
const
772 return d->replySubmitButtonIconName;
775QString Notification::category()
const
780bool Notification::expired()
const
785void Notification::setExpired(
bool expired)
787 d->expired = expired;
790bool Notification::dismissed()
const
795void Notification::setDismissed(
bool dismissed)
797 d->dismissed = dismissed;
800bool Notification::resident()
const
805void Notification::setResident(
bool resident)
807 d->resident = resident;
810bool Notification::transient()
const
815void Notification::setTransient(
bool transient)
817 d->transient = transient;
820QVariantMap Notification::hints()
const
825void Notification::setHints(
const QVariantMap &hints)
830void Notification::processHints(
const QVariantMap &hints)
832 d->processHints(hints);
835bool Notification::wasAddedDuringInhibition()
const
837 return d->wasAddedDuringInhibition;
840void Notification::setWasAddedDuringInhibition(
bool value)
842 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)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
QString name(const QVariant &location)
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)
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